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 res[purchase.id] = False
94 if purchase.order_line:
95 min_date=purchase.order_line[0].date_planned
96 for line in purchase.order_line:
97 if line.date_planned < min_date:
98 min_date=line.date_planned
99 res[purchase.id]=min_date
102 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
104 for purchase in self.browse(cursor, user, ids, context=context):
106 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
107 tot += purchase.invoice_id.amount_untaxed
108 if purchase.amount_untaxed:
109 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
111 res[purchase.id] = 0.0
114 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
115 if not ids: return {}
120 p.purchase_id,sum(m.product_qty), m.state
124 stock_picking p on (p.id=m.picking_id)
126 p.purchase_id in ('''+','.join(map(str,ids))+''')
127 GROUP BY m.state, p.purchase_id''')
128 for oid,nbr,state in cr.fetchall():
132 res[oid][0] += nbr or 0.0
133 res[oid][1] += nbr or 0.0
135 res[oid][1] += nbr or 0.0
140 res[r] = 100.0 * res[r][0] / res[r][1]
144 'name': fields.char('Order Reference', size=64, required=True, select=True),
145 'origin': fields.char('Origin', size=64),
146 'partner_ref': fields.char('Partner Ref.', size=64),
147 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
148 'date_approve':fields.date('Date Approved'),
149 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True),
150 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
152 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]}),
153 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
154 'location_id': fields.many2one('stock.location', 'Destination', required=True),
156 '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."),
158 '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),
159 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
160 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
161 'notes': fields.text('Notes'),
162 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
163 '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"),
164 'shipped':fields.boolean('Received', readonly=True, select=True),
165 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
166 'invoiced':fields.boolean('Invoiced & Paid', readonly=True, select=True),
167 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
168 'invoice_method': fields.selection([('manual','Manual'),('order','From order'),('picking','From picking')], 'Invoicing Control', required=True),
169 'minimum_planned_date':fields.function(_minimum_planned_date, method=True,store=True, string='Planned Date', type='date', help="This is computed as the minimum scheduled date of all purchase order lines' products."),
170 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
171 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
172 'amount_total': fields.function(_amount_total, method=True, string='Total'),
175 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
176 'state': lambda *a: 'draft',
177 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
178 'shipped': lambda *a: 0,
179 'invoice_method': lambda *a: 'order',
180 'invoiced': lambda *a: 0,
181 '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'],
182 '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,
184 _name = "purchase.order"
185 _description = "Purchase order"
188 def button_dummy(self, cr, uid, ids, context={}):
191 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
194 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
195 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
196 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
198 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
201 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
202 return {'value':{'location_id': res, 'dest_address_id': False}}
204 def onchange_partner_id(self, cr, uid, ids, part):
206 return {'value':{'partner_address_id': False}}
207 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
208 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist_purchase.id
209 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist}}
211 def wkf_approve_order(self, cr, uid, ids):
212 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
215 def wkf_confirm_order(self, cr, uid, ids, context={}):
216 for po in self.browse(cr, uid, ids):
217 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
218 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})
219 current_name = self.name_get(cr, uid, ids)[0][1]
221 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
224 def wkf_warn_buyer(self, cr, uid, ids):
225 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
226 request = pooler.get_pool(cr.dbname).get('res.request')
227 for po in self.browse(cr, uid, ids):
229 for oline in po.order_line:
230 manager = oline.product_id.product_manager
231 if manager and not (manager.id in managers):
232 managers.append(manager.id)
233 for manager_id in managers:
234 request.create(cr, uid,
235 {'name' : "Purchase amount over the limit",
237 'act_to' : manager_id,
238 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
239 'ref_partner_id': po.partner_id.id,
240 'ref_doc1': 'purchase.order,%d' % (po.id,),
242 def inv_line_create(self,a,ol):
246 'price_unit': ol.price_unit or 0.0,
247 'quantity': ol.product_qty,
248 'product_id': ol.product_id.id or False,
249 'uos_id': ol.product_uom.id or False,
250 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
251 'account_analytic_id': ol.account_analytic_id.id,
254 def action_invoice_create(self, cr, uid, ids, *args):
256 for o in self.browse(cr, uid, ids):
258 for ol in o.order_line:
261 a = ol.product_id.product_tmpl_id.property_account_expense.id
263 a = ol.product_id.categ_id.property_account_expense_categ.id
265 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,))
267 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
268 il.append(self.inv_line_create(a,ol))
269 # il.append((0, False, {
272 # 'price_unit': ol.price_unit or 0.0,
273 # 'quantity': ol.product_qty,
274 # 'product_id': ol.product_id.id or False,
275 # 'uos_id': ol.product_uom.id or False,
276 # 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
277 # 'account_analytic_id': ol.account_analytic_id.id,
280 a = o.partner_id.property_account_payable.id
282 'name': o.partner_ref or o.name,
283 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
285 'type': 'in_invoice',
286 'partner_id': o.partner_id.id,
287 'currency_id': o.pricelist_id.currency_id.id,
288 'address_invoice_id': o.partner_address_id.id,
289 'address_contact_id': o.partner_address_id.id,
293 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
294 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
296 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
300 def has_stockable_product(self,cr, uid, ids, *args):
301 for order in self.browse(cr, uid, ids):
302 for order_line in order.order_line:
303 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
307 def action_picking_create(self,cr, uid, ids, *args):
309 for order in self.browse(cr, uid, ids):
310 loc_id = order.partner_id.property_stock_supplier.id
312 if order.invoice_method=='picking':
313 istate = '2binvoiced'
314 picking_id = self.pool.get('stock.picking').create(cr, uid, {
315 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
317 'address_id': order.dest_address_id.id or order.partner_address_id.id,
318 'invoice_state': istate,
319 'purchase_id': order.id,
321 for order_line in order.order_line:
322 if not order_line.product_id:
324 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
325 dest = order.location_id.id
326 self.pool.get('stock.move').create(cr, uid, {
327 'name': 'PO:'+order_line.name,
328 'product_id': order_line.product_id.id,
329 'product_qty': order_line.product_qty,
330 'product_uos_qty': order_line.product_qty,
331 'product_uom': order_line.product_uom.id,
332 'product_uos': order_line.product_uom.id,
333 'date_planned': order_line.date_planned,
334 'location_id': loc_id,
335 'location_dest_id': dest,
336 'picking_id': picking_id,
337 'move_dest_id': order_line.move_dest_id.id,
339 'purchase_line_id': order_line.id,
341 if order_line.move_dest_id:
342 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
343 wf_service = netsvc.LocalService("workflow")
344 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
346 def copy(self, cr, uid, id, default=None,context={}):
355 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
357 return super(purchase_order, self).copy(cr, uid, id, default, context)
361 class purchase_order_line(osv.osv):
362 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
364 cur_obj=self.pool.get('res.currency')
365 for line in self.browse(cr, uid, ids):
366 cur = line.order_id.pricelist_id.currency_id
367 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
371 'name': fields.char('Description', size=64, required=True),
372 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
373 'date_planned': fields.date('Scheduled date', required=True),
374 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
375 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
376 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
377 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
378 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
379 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
380 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
381 'notes': fields.text('Notes'),
382 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
383 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
386 'product_qty': lambda *a: 1.0
388 _table = 'purchase_order_line'
389 _name = 'purchase.order.line'
390 _description = 'Purchase Order lines'
391 def copy(self, cr, uid, id, default=None,context={}):
394 default.update({'state':'draft', 'move_id':False})
395 return super(purchase_order_line, self).copy(cr, uid, id, default, context)
397 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
398 partner_id, date_order=False):
400 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
402 return {'value': {'price_unit': 0.0, 'name':'','notes':''}, 'domain':{'product_uom':[]}}
405 lang=self.pool.get('res.partner').read(cr, uid, [partner_id])[0]['lang']
406 context={'lang':lang}
408 prod = self.pool.get('product.product').read(cr, uid, [product], ['supplier_taxes_id','name','seller_delay','uom_po_id','description_purchase'])[0]
409 prod_uom_po = prod['uom_po_id'][0]
413 date_order = time.strftime('%Y-%m-%d')
414 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
415 product, qty or 1.0, partner_id, {
419 dt = (DateTime.now() + DateTime.RelativeDateTime(days=prod['seller_delay'] or 0.0)).strftime('%Y-%m-%d')
420 prod_name = self.pool.get('product.product').name_get(cr, uid, [product], context=context)[0][1]
422 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':prod['supplier_taxes_id'], 'date_planned': dt,'notes':prod['description_purchase'], 'product_uom': uom}}
425 if res['value']['taxes_id']:
426 taxes = self.pool.get('account.tax').browse(cr, uid,
427 [x.id for x in product.supplier_taxes_id])
430 taxep = self.pool.get('res.partner').browse(cr, uid,
431 partner_id).property_account_supplier_tax
432 if not taxep or not taxep.id:
433 res['value']['taxes_id'] = [x.id for x in product.taxes_id]
437 if not t.tax_group==taxep.tax_group:
439 res['value']['taxes_id'] = res5
441 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
442 res3 = self.pool.get('product.uom').read(cr, uid, [prod_uom_po], ['category_id'])
443 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
444 if res2[0]['category_id'] != res3[0]['category_id']:
445 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'))
447 res['domain'] = domain
450 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
451 partner_id, date_order=False):
452 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
453 partner_id, date_order=date_order)
454 if 'product_uom' in res['value']:
455 del res['value']['product_uom']
457 res['value']['price_unit'] = 0.0
459 purchase_order_line()
461 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: