1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ##############################################################################
31 from osv import fields
37 from mx import DateTime
39 from tools import config
40 from tools.translate import _
45 class purchase_order(osv.osv):
46 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
48 for order in self.browse(cr, uid, ids):
50 for oline in order.order_line:
51 res[order.id] += oline.price_unit * oline.product_qty
54 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
56 cur_obj=self.pool.get('res.currency')
57 for purchase in self.browse(cr, uid, ids):
58 res[purchase.id] = 0.0
59 for line in purchase.order_line:
60 res[purchase.id] += line.price_subtotal
61 cur = purchase.pricelist_id.currency_id
62 res[purchase.id] = cur_obj.round(cr, uid, cur, res[purchase.id])
66 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
68 cur_obj=self.pool.get('res.currency')
69 for order in self.browse(cr, uid, ids):
71 cur=order.pricelist_id.currency_id
72 for line in order.order_line:
73 for c in self.pool.get('account.tax').compute(cr, uid, line.taxes_id, line.price_unit, line.product_qty, order.partner_address_id.id, line.product_id, order.partner_id):
74 val+= cur_obj.round(cr, uid, cur, c['amount'])
75 res[order.id]=cur_obj.round(cr, uid, cur, val)
78 def _amount_total(self, cr, uid, ids, field_name, arg, context):
80 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
81 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
82 cur_obj=self.pool.get('res.currency')
84 order=self.browse(cr, uid, [id])[0]
85 cur=order.pricelist_id.currency_id
86 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
89 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context):
91 purchase_obj=self.browse(cr, uid, ids, context=context)
92 for purchase in purchase_obj:
93 if purchase.order_line:
94 min_date=purchase.order_line[0].date_planned
95 for line in purchase.order_line:
96 if line.date_planned < min_date:
97 min_date=line.date_planned
98 res[purchase.id]=min_date
101 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
103 for purchase in self.browse(cursor, user, ids, context=context):
105 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
106 tot += purchase.invoice_id.amount_untaxed
107 if purchase.amount_untaxed:
108 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
110 res[purchase.id] = 0.0
113 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
114 if not ids: return {}
119 p.purchase_id,sum(m.product_qty), m.state
123 stock_picking p on (p.id=m.picking_id)
125 p.purchase_id in ('''+','.join(map(str,ids))+''')
126 GROUP BY m.state, p.purchase_id''')
127 for oid,nbr,state in cr.fetchall():
131 res[oid][0] += nbr or 0.0
132 res[oid][1] += nbr or 0.0
134 res[oid][1] += nbr or 0.0
139 res[r] = 100.0 * res[r][0] / res[r][1]
143 'name': fields.char('Order Reference', size=64, required=True, select=True),
144 'origin': fields.char('Origin', size=64),
145 'partner_ref': fields.char('Partner Ref.', size=64),
146 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
147 'date_approve':fields.date('Date Approved'),
148 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True),
149 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
151 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]}),
152 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
153 'location_id': fields.many2one('stock.location', 'Delivery destination', required=True),
155 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, help="The pricelist sets the currency used for this purchase order. It also computes the supplier price for the selected products/quantities."),
157 'state': fields.selection([('draft', 'Request for Quotation'), ('wait', 'Waiting'), ('confirmed', 'Confirmed'), ('approved', 'Approved'),('except_picking', 'Shipping Exception'), ('except_invoice', 'Invoice Exception'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Order Status', readonly=True, help="The state of the purchase order or the quotation request. A quotation is a purchase order in a 'Draft' state. Then the order has to be confirmed by the user, the state switch to 'Confirmed'. Then the supplier must confirm the order to change the state to 'Approved'. When the purchase order is paid and received, the state becomes 'Done'. If a cancel action occurs in the invoice or in the reception of goods, the state becomes in exception.", select=True),
158 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
159 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
160 'notes': fields.text('Notes'),
161 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
162 'picking_ids': fields.one2many('stock.picking', 'purchase_id', 'Picking List', readonly=True, help="This is the list of picking list that have been generated for this purchase"),
163 'shipped':fields.boolean('Received', readonly=True, select=True),
164 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
165 'invoiced':fields.boolean('Invoiced & Paid', readonly=True, select=True),
166 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
167 'invoice_method': fields.selection([('manual','Manual'),('order','From order'),('picking','From picking')], 'Invoicing Control', required=True),
168 'minimum_planned_date':fields.function(_minimum_planned_date, method=True,store=True, string='Minimum Planned Date', type='date', help="Minimum schedule date of all products."),
169 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
170 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
171 'amount_total': fields.function(_amount_total, method=True, string='Total'),
174 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
175 'state': lambda *a: 'draft',
176 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
177 'shipped': lambda *a: 0,
178 'invoice_method': lambda *a: 'order',
179 'invoiced': lambda *a: 0,
180 'partner_address_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['default'])['default'],
181 'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist_purchase.id,
183 _name = "purchase.order"
184 _description = "Purchase order"
187 def button_dummy(self, cr, uid, ids, context={}):
190 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
193 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
194 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
195 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
197 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
200 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
201 return {'value':{'location_id': res, 'dest_address_id': False}}
203 def onchange_partner_id(self, cr, uid, ids, part):
205 return {'value':{'partner_address_id': False}}
206 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
207 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist_purchase.id
208 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist}}
210 def wkf_approve_order(self, cr, uid, ids):
211 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
214 def wkf_confirm_order(self, cr, uid, ids, context={}):
215 for po in self.browse(cr, uid, ids):
216 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
217 self.pool.get('res.partner.event').create(cr, uid, {'name':'Purchase Order: '+po.name, 'partner_id':po.partner_id.id, 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id':uid, 'partner_type':'retailer', 'probability': 1.0, 'planned_cost':po.amount_untaxed})
218 current_name = self.name_get(cr, uid, ids)[0][1]
220 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
223 def wkf_warn_buyer(self, cr, uid, ids):
224 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
225 request = pooler.get_pool(cr.dbname).get('res.request')
226 for po in self.browse(cr, uid, ids):
228 for oline in po.order_line:
229 manager = oline.product_id.product_manager
230 if manager and not (manager.id in managers):
231 managers.append(manager.id)
232 for manager_id in managers:
233 request.create(cr, uid,
234 {'name' : "Purchase amount over the limit",
236 'act_to' : manager_id,
237 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
238 'ref_partner_id': po.partner_id.id,
239 'ref_doc1': 'purchase.order,%d' % (po.id,),
241 def inv_line_create(self,a,ol):
245 'price_unit': ol.price_unit or 0.0,
246 'quantity': ol.product_qty,
247 'product_id': ol.product_id.id or False,
248 'uos_id': ol.product_uom.id or False,
249 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
250 'account_analytic_id': ol.account_analytic_id.id,
253 def action_invoice_create(self, cr, uid, ids, *args):
255 for o in self.browse(cr, uid, ids):
257 for ol in o.order_line:
260 a = ol.product_id.product_tmpl_id.property_account_expense.id
262 a = ol.product_id.categ_id.property_account_expense_categ.id
264 raise osv.except_osv(_('Error !'), _('There is no expense account defined for this product: "%s" (id:%d)') % (ol.product_id.name, ol.product_id.id,))
266 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
267 il.append(self.inv_line_create(a,ol))
268 # il.append((0, False, {
271 # 'price_unit': ol.price_unit or 0.0,
272 # 'quantity': ol.product_qty,
273 # 'product_id': ol.product_id.id or False,
274 # 'uos_id': ol.product_uom.id or False,
275 # 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
276 # 'account_analytic_id': ol.account_analytic_id.id,
279 a = o.partner_id.property_account_payable.id
281 'name': o.partner_ref or o.name,
282 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
284 'type': 'in_invoice',
285 'partner_id': o.partner_id.id,
286 'currency_id': o.pricelist_id.currency_id.id,
287 'address_invoice_id': o.partner_address_id.id,
288 'address_contact_id': o.partner_address_id.id,
292 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
293 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
295 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
299 def has_stockable_product(self,cr, uid, ids, *args):
300 for order in self.browse(cr, uid, ids):
301 for order_line in order.order_line:
302 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
306 def action_picking_create(self,cr, uid, ids, *args):
308 for order in self.browse(cr, uid, ids):
309 loc_id = order.partner_id.property_stock_supplier.id
311 if order.invoice_method=='picking':
312 istate = '2binvoiced'
313 picking_id = self.pool.get('stock.picking').create(cr, uid, {
314 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
316 'address_id': order.dest_address_id.id or order.partner_address_id.id,
317 'invoice_state': istate,
318 'purchase_id': order.id,
320 for order_line in order.order_line:
321 if not order_line.product_id:
323 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
324 dest = order.location_id.id
325 self.pool.get('stock.move').create(cr, uid, {
326 'name': 'PO:'+order_line.name,
327 'product_id': order_line.product_id.id,
328 'product_qty': order_line.product_qty,
329 'product_uos_qty': order_line.product_qty,
330 'product_uom': order_line.product_uom.id,
331 'product_uos': order_line.product_uom.id,
332 'date_planned': order_line.date_planned,
333 'location_id': loc_id,
334 'location_dest_id': dest,
335 'picking_id': picking_id,
336 'move_dest_id': order_line.move_dest_id.id,
338 'purchase_line_id': order_line.id,
340 if order_line.move_dest_id:
341 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
342 wf_service = netsvc.LocalService("workflow")
343 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
345 def copy(self, cr, uid, id, default=None,context={}):
354 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
356 return super(purchase_order, self).copy(cr, uid, id, default, context)
360 class purchase_order_line(osv.osv):
361 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
363 cur_obj=self.pool.get('res.currency')
364 for line in self.browse(cr, uid, ids):
365 cur = line.order_id.pricelist_id.currency_id
366 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
370 'name': fields.char('Description', size=64, required=True),
371 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
372 'date_planned': fields.date('Scheduled date', required=True),
373 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
374 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
375 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
376 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
377 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
378 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
379 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
380 'notes': fields.text('Notes'),
381 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
382 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
385 'product_qty': lambda *a: 1.0
387 _table = 'purchase_order_line'
388 _name = 'purchase.order.line'
389 _description = 'Purchase Order lines'
390 def copy(self, cr, uid, id, default=None,context={}):
393 default.update({'state':'draft', 'move_id':False})
394 return super(purchase_order_line, self).copy(cr, uid, id, default, context)
396 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
397 partner_id, date_order=False):
399 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
401 return {'value': {'price_unit': 0.0, 'name':'','notes':''}, 'domain':{'product_uom':[]}}
404 lang=self.pool.get('res.partner').read(cr, uid, [partner_id])[0]['lang']
405 context={'lang':lang}
407 prod = self.pool.get('product.product').read(cr, uid, [product], ['supplier_taxes_id','name','seller_delay','uom_po_id','description_purchase'])[0]
408 prod_uom_po = prod['uom_po_id'][0]
412 date_order = time.strftime('%Y-%m-%d')
413 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
414 product, qty or 1.0, partner_id, {
418 dt = (DateTime.now() + DateTime.RelativeDateTime(days=prod['seller_delay'] or 0.0)).strftime('%Y-%m-%d')
419 prod_name = self.pool.get('product.product').name_get(cr, uid, [product], context=context)[0][1]
421 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':prod['supplier_taxes_id'], 'date_planned': dt,'notes':prod['description_purchase'], 'product_uom': uom}}
424 if res['value']['taxes_id']:
425 taxes = self.pool.get('account.tax').browse(cr, uid,
426 [x.id for x in product.supplier_taxes_id])
429 taxep = self.pool.get('res.partner').browse(cr, uid,
430 partner_id).property_account_supplier_tax
431 if not taxep or not taxep.id:
432 res['value']['taxes_id'] = [x.id for x in product.taxes_id]
436 if not t.tax_group==taxep.tax_group:
438 res['value']['taxes_id'] = res5
440 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
441 res3 = self.pool.get('product.uom').read(cr, uid, [prod_uom_po], ['category_id'])
442 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
443 if res2[0]['category_id'] != res3[0]['category_id']:
444 raise osv.except_osv(_('Wrong Product UOM !'), _('You have to select a product UOM in the same category than the purchase UOM of the product'))
446 res['domain'] = domain
449 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
450 partner_id, date_order=False):
451 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
452 partner_id, date_order=date_order)
453 if 'product_uom' in res['value']:
454 del res['value']['product_uom']
456 res['value']['price_unit'] = 0.0
458 purchase_order_line()
460 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: