1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from mx import DateTime
23 from osv import fields
29 from datetime import datetime
30 from dateutil.relativedelta import relativedelta
32 from tools import config
33 from tools.translate import _
35 import decimal_precision as dp
36 from osv.orm import browse_record, browse_null
41 class purchase_order(osv.osv):
42 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
44 for order in self.browse(cr, uid, ids):
46 for oline in order.order_line:
47 res[order.id] += oline.price_unit * oline.product_qty
50 def _amount_all(self, cr, uid, ids, field_name, arg, context):
52 cur_obj=self.pool.get('res.currency')
53 for order in self.browse(cr, uid, ids):
55 'amount_untaxed': 0.0,
60 cur=order.pricelist_id.currency_id
61 for line in order.order_line:
62 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):
64 val1 += line.price_subtotal
65 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
66 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
67 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
70 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context):
71 if not value: return False
72 if type(ids)!=type([]):
74 for po in self.browse(cr, uid, ids, context):
75 cr.execute("""update purchase_order_line set
79 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
82 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context):
84 purchase_obj=self.browse(cr, uid, ids, context=context)
85 for purchase in purchase_obj:
86 res[purchase.id] = False
87 if purchase.order_line:
88 min_date=purchase.order_line[0].date_planned
89 for line in purchase.order_line:
90 if line.date_planned < min_date:
91 min_date=line.date_planned
92 res[purchase.id]=min_date
95 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
97 for purchase in self.browse(cursor, user, ids, context=context):
99 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
100 tot += purchase.invoice_id.amount_untaxed
101 if purchase.amount_untaxed:
102 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
104 res[purchase.id] = 0.0
107 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
108 if not ids: return {}
113 p.purchase_id,sum(m.product_qty), m.state
117 stock_picking p on (p.id=m.picking_id)
119 p.purchase_id = ANY(%s) GROUP BY m.state, p.purchase_id''',(ids,))
120 for oid,nbr,state in cr.fetchall():
124 res[oid][0] += nbr or 0.0
125 res[oid][1] += nbr or 0.0
127 res[oid][1] += nbr or 0.0
132 res[r] = 100.0 * res[r][0] / res[r][1]
135 def _get_order(self, cr, uid, ids, context={}):
137 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
138 result[line.order_id.id] = True
141 def _invoiced(self, cursor, user, ids, name, arg, context=None):
143 for purchase in self.browse(cursor, user, ids, context=context):
144 if purchase.invoice_id.reconciled:
145 res[purchase.id] = purchase.invoice_id.reconciled
147 res[purchase.id] = False
151 'name': fields.char('Order Reference', size=64, required=True, select=True),
152 'origin': fields.char('Source Document', size=64,
153 help="Reference of the document that generated this purchase order request."
155 'partner_ref': fields.char('Supplier Reference', size=64),
156 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, help="Date on which this document has been created."),
157 'date_approve':fields.date('Date Approved', readonly=1),
158 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, change_default=True),
159 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
160 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]},
161 help="Put an address if you want to deliver directly from the supplier to the customer." \
162 "In this case, it will remove the warehouse link and set the customer location."
164 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
165 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')]),
166 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, help="The pricelist sets the currency used for this purchase order. It also computes the supplier price for the selected products/quantities."),
167 'state': fields.selection([('draft', 'Request for Quotation'), ('wait', 'Waiting'), ('confirmed', 'Waiting Supplier Ack'), ('approved', 'Approved'),('except_picking', 'Shipping Exception'), ('except_invoice', 'Invoice Exception'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', 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),
168 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
169 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
170 'notes': fields.text('Notes', translate=True),
171 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
172 '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"),
173 'shipped':fields.boolean('Received', readonly=True, select=True),
174 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
175 'invoiced': fields.function(_invoiced, method=True, string='Invoiced & Paid', type='boolean'),
176 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
177 'invoice_method': fields.selection([('manual','Manual'),('order','From Order'),('picking','From Picking')], 'Invoicing Control', required=True,
178 help="From Order: a draft invoice will be pre-generated based on the purchase order. The accountant " \
179 "will just have to validate this invoice for control.\n" \
180 "From Picking: a draft invoice will be pre-generated based on validated receptions.\n" \
181 "Manual: no invoice will be pre-generated. The accountant will have to encode manually."
183 'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, method=True,store=True, string='Expected Date', type='datetime', help="This is computed as the minimum scheduled date of all purchase order lines' products."),
184 'amount_untaxed': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Untaxed Amount',
186 'purchase.order.line': (_get_order, None, 10),
188 'amount_tax': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Taxes',
190 'purchase.order.line': (_get_order, None, 10),
192 'amount_total': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Total',
194 'purchase.order.line': (_get_order, None, 10),
196 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
197 'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
198 'create_uid': fields.many2one('res.users', 'Responsible'),
199 'company_id': fields.many2one('res.company','Company',required=True,select=1),
202 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
203 'state': lambda *a: 'draft',
204 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
205 'shipped': lambda *a: 0,
206 'invoice_method': lambda *a: 'order',
207 'invoiced': lambda *a: 0,
208 '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'],
209 '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,
210 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
212 _name = "purchase.order"
213 _description = "Purchase Order"
217 def unlink(self, cr, uid, ids, context=None):
218 purchase_orders = self.read(cr, uid, ids, ['state'])
220 for s in purchase_orders:
221 if s['state'] in ['draft','cancel']:
222 unlink_ids.append(s['id'])
224 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!' % s['state']))
226 # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
227 # automatically sending subflow.delete upon deletion
228 wf_service = netsvc.LocalService("workflow")
229 for id in unlink_ids:
230 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
232 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
234 def button_dummy(self, cr, uid, ids, context={}):
237 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
240 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
241 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
242 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
244 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
247 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
248 return {'value':{'location_id': res, 'dest_address_id': False}}
250 def onchange_partner_id(self, cr, uid, ids, part):
253 return {'value':{'partner_address_id': False, 'fiscal_position': False}}
254 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
255 part = self.pool.get('res.partner').browse(cr, uid, part)
256 pricelist = part.property_product_pricelist_purchase.id
257 fiscal_position = part.property_account_position and part.property_account_position.id or False
258 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
260 def wkf_approve_order(self, cr, uid, ids, context={}):
261 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
264 #TODO: implement messages system
265 def wkf_confirm_order(self, cr, uid, ids, context={}):
267 for po in self.browse(cr, uid, ids):
268 if not po.order_line:
269 raise osv.except_osv(_('Error !'),_('You can not confirm purchase order without Purchase Order Lines.'))
270 for line in po.order_line:
271 if line.state=='draft':
273 current_name = self.name_get(cr, uid, ids)[0][1]
274 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
276 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
279 def wkf_warn_buyer(self, cr, uid, ids):
280 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
281 request = pooler.get_pool(cr.dbname).get('res.request')
282 for po in self.browse(cr, uid, ids):
284 for oline in po.order_line:
285 manager = oline.product_id.product_manager
286 if manager and not (manager.id in managers):
287 managers.append(manager.id)
288 for manager_id in managers:
289 request.create(cr, uid,
290 {'name' : "Purchase amount over the limit",
292 'act_to' : manager_id,
293 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
294 'ref_partner_id': po.partner_id.id,
295 'ref_doc1': 'purchase.order,%d' % (po.id,),
297 def inv_line_create(self, cr, uid, a, ol):
301 'price_unit': ol.price_unit or 0.0,
302 'quantity': ol.product_qty,
303 'product_id': ol.product_id.id or False,
304 'uos_id': ol.product_uom.id or False,
305 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
306 'account_analytic_id': ol.account_analytic_id.id,
309 def action_cancel_draft(self, cr, uid, ids, *args):
312 self.write(cr, uid, ids, {'state':'draft','shipped':0})
313 wf_service = netsvc.LocalService("workflow")
315 # Deleting the existing instance of workflow for PO
316 wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
317 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
320 def action_invoice_create(self, cr, uid, ids, *args):
323 journal_obj = self.pool.get('account.journal')
324 for o in self.browse(cr, uid, ids):
327 for ol in o.order_line:
330 a = ol.product_id.product_tmpl_id.property_account_expense.id
332 a = ol.product_id.categ_id.property_account_expense_categ.id
334 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,))
336 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
337 fpos = o.fiscal_position or False
338 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
339 il.append(self.inv_line_create(cr, uid, a, ol))
341 a = o.partner_id.property_account_payable.id
342 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', o.company_id.id)], limit=1)
344 raise osv.except_osv(_('Error !'),
345 _('There is no purchase journal defined for this company: "%s" (id:%d)') % (o.company_id.name, o.company_id.id))
347 'name': o.partner_ref or o.name,
348 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
350 'type': 'in_invoice',
351 'partner_id': o.partner_id.id,
352 'currency_id': o.pricelist_id.currency_id.id,
353 'address_invoice_id': o.partner_address_id.id,
354 'address_contact_id': o.partner_address_id.id,
355 'journal_id': len(journal_ids) and journal_ids[0] or False,
358 'fiscal_position': o.partner_id.property_account_position.id,
359 'payment_term': o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
360 'company_id': o.company_id.id,
362 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
363 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
364 self.pool.get('purchase.order.line').write(cr, uid, todo, {'invoiced':True})
365 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
369 def has_stockable_product(self,cr, uid, ids, *args):
370 for order in self.browse(cr, uid, ids):
371 for order_line in order.order_line:
372 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
376 def action_cancel(self, cr, uid, ids, context={}):
378 purchase_order_line_obj = self.pool.get('purchase.order.line')
379 for purchase in self.browse(cr, uid, ids):
380 for pick in purchase.picking_ids:
381 if pick.state not in ('draft','cancel'):
382 raise osv.except_osv(
383 _('Could not cancel purchase order !'),
384 _('You must first cancel all picking attached to this purchase order.'))
385 for pick in purchase.picking_ids:
386 wf_service = netsvc.LocalService("workflow")
387 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
388 inv = purchase.invoice_id
389 if inv and inv.state not in ('cancel','draft'):
390 raise osv.except_osv(
391 _('Could not cancel this purchase order !'),
392 _('You must first cancel all invoices attached to this purchase order.'))
394 wf_service = netsvc.LocalService("workflow")
395 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
396 self.write(cr,uid,ids,{'state':'cancel'})
399 def action_picking_create(self,cr, uid, ids, *args):
401 for order in self.browse(cr, uid, ids):
402 loc_id = order.partner_id.property_stock_supplier.id
404 if order.invoice_method=='picking':
405 istate = '2binvoiced'
406 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in')
407 picking_id = self.pool.get('stock.picking').create(cr, uid, {
409 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
411 'address_id': order.dest_address_id.id or order.partner_address_id.id,
412 'invoice_state': istate,
413 'purchase_id': order.id,
414 'company_id': order.company_id.id,
417 for order_line in order.order_line:
418 if not order_line.product_id:
420 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
421 dest = order.location_id.id
422 move = self.pool.get('stock.move').create(cr, uid, {
423 'name': 'PO:'+order_line.name,
424 'product_id': order_line.product_id.id,
425 'product_qty': order_line.product_qty,
426 'product_uos_qty': order_line.product_qty,
427 'product_uom': order_line.product_uom.id,
428 'product_uos': order_line.product_uom.id,
429 'date_planned': order_line.date_planned,
430 'location_id': loc_id,
431 'location_dest_id': dest,
432 'picking_id': picking_id,
433 'move_dest_id': order_line.move_dest_id.id,
435 'purchase_line_id': order_line.id,
436 'company_id': order.company_id.id,
438 if order_line.move_dest_id:
439 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
440 todo_moves.append(move)
441 self.pool.get('stock.move').action_confirm(cr, uid, todo_moves)
442 self.pool.get('stock.move').force_assign(cr, uid, todo_moves)
443 wf_service = netsvc.LocalService("workflow")
444 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
447 def copy(self, cr, uid, id, default=None,context={}):
456 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
458 return super(purchase_order, self).copy(cr, uid, id, default, context)
461 def do_merge(self, cr, uid, ids, context):
463 To merge similar type of purchase orders.
464 Orders will only be merged if:
465 * Purchase Orders are in draft
466 * Purchase Orders belong to the same partner
467 * Purchase Orders are have same stock location, same pricelist
468 Lines will only be merged if:
469 * Order lines are exactly the same except for the quantity and unit
471 @param self: The object pointer.
472 @param cr: A database cursor
473 @param uid: ID of the user currently logged in
474 @param ids: the ID or list of IDs
475 @param context: A standard dictionary
477 @return: new purchase order id
480 wf_service = netsvc.LocalService("workflow")
481 def make_key(br, fields):
484 field_val = getattr(br, field)
485 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
488 if isinstance(field_val, browse_record):
489 field_val = field_val.id
490 elif isinstance(field_val, browse_null):
492 elif isinstance(field_val, list):
493 field_val = ((6, 0, tuple([v.id for v in field_val])),)
494 list_key.append((field, field_val))
496 return tuple(list_key)
498 # compute what the new orders should contain
502 for porder in [order for order in self.browse(cr, uid, ids) if order.state == 'draft']:
503 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
504 new_order = new_orders.setdefault(order_key, ({}, []))
505 new_order[1].append(porder.id)
506 order_infos = new_order[0]
509 'origin': porder.origin,
510 'date_order': time.strftime('%Y-%m-%d'),
511 'partner_id': porder.partner_id.id,
512 'partner_address_id': porder.partner_address_id.id,
513 'dest_address_id': porder.dest_address_id.id,
514 'warehouse_id': porder.warehouse_id.id,
515 'location_id': porder.location_id.id,
516 'pricelist_id': porder.pricelist_id.id,
519 'notes': '%s' % (porder.notes or '',),
520 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
523 #order_infos['name'] += ', %s' % porder.name
525 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
527 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
529 for order_line in porder.order_line:
530 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
531 o_line = order_infos['order_line'].setdefault(line_key, {})
533 # merge the line with an existing line
534 o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
536 # append a new "standalone" line
537 for field in ('product_qty', 'product_uom'):
538 field_val = getattr(order_line, field)
539 if isinstance(field_val, browse_record):
540 field_val = field_val.id
541 o_line[field] = field_val
542 o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
547 for order_key, (order_data, old_ids) in new_orders.iteritems():
548 # skip merges with only one order
550 allorders += (old_ids or [])
553 # cleanup order line data
554 for key, value in order_data['order_line'].iteritems():
555 del value['uom_factor']
556 value.update(dict(key))
557 order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
559 # create the new order
560 neworder_id = self.create(cr, uid, order_data)
561 allorders.append(neworder_id)
563 # make triggers pointing to the old orders point to the new order
564 for old_id in old_ids:
565 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
566 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
571 class purchase_order_line(osv.osv):
572 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
574 cur_obj=self.pool.get('res.currency')
575 for line in self.browse(cr, uid, ids):
576 cur = line.order_id.pricelist_id.currency_id
577 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
581 'name': fields.char('Description', size=256, required=True),
582 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
583 'date_planned': fields.datetime('Scheduled date', required=True),
584 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
585 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
586 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
587 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
588 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
589 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
590 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
591 'notes': fields.text('Notes', translate=True),
592 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
593 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
594 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company'),
595 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
596 help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
597 \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
598 \n* The \'Done\' state is set automatically when purchase order is set as done. \
599 \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
600 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
601 'invoiced': fields.boolean('Invoiced', readonly=True),
602 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner"),
603 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
607 'product_qty': lambda *a: 1.0,
608 'state': lambda *args: 'draft',
609 'invoiced': lambda *a: 0,
611 _table = 'purchase_order_line'
612 _name = 'purchase.order.line'
613 _description = 'Purchase Order Line'
614 def copy_data(self, cr, uid, id, default=None,context={}):
617 default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
618 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
620 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
621 partner_id, date_order=False, fiscal_position=False, date_planned=False,
622 name=False, price_unit=False, notes=False):
624 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist or a supplier in the purchase form !\nPlease set one before choosing a product.'))
626 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
628 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
629 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
630 prod= self.pool.get('product.product').browse(cr, uid,product)
633 lang=self.pool.get('res.partner').read(cr, uid, partner_id)['lang']
634 context={'lang':lang}
635 context['partner_id'] = partner_id
637 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
638 prod_uom_po = prod.uom_po_id.id
642 date_order = time.strftime('%Y-%m-%d')
645 for s in prod.seller_ids:
646 if s.name.id == partner_id:
647 seller_delay = s.delay
648 temp_qty = s.qty # supplier _qty assigned to temp
649 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
654 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
655 product, qty or 1.0, partner_id, {
659 dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
660 prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id])[0][1]
663 res = {'value': {'price_unit': price, 'name': name or prod_name,
664 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
665 'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
670 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
671 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
672 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
673 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
675 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
676 res3 = prod.uom_id.category_id.id
677 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
678 if res2[0]['category_id'][0] != res3:
679 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'))
681 res['domain'] = domain
684 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
685 partner_id, date_order=False,fiscal_position=False):
686 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
687 partner_id, date_order=date_order,fiscal_position=fiscal_position)
688 if 'product_uom' in res['value']:
689 del res['value']['product_uom']
691 res['value']['price_unit'] = 0.0
693 def action_confirm(self, cr, uid, ids, context={}):
694 self.write(cr, uid, ids, {'state': 'confirmed'}, context)
696 purchase_order_line()
698 class procurement_order(osv.osv):
699 _inherit = 'procurement.order'
701 'purchase_id': fields.many2one('purchase.order', 'Latest Requisition'),
704 def action_po_assign(self, cr, uid, ids, context={}):
705 """ This is action which call from workflow to assign purchase order to procurements
708 res = self.make_po(cr, uid, ids, context=context)
710 return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
712 def make_po(self, cr, uid, ids, context={}):
713 """ Make purchase order from procurement
714 @return: New created Purchase Orders procurement wise
717 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
718 partner_obj = self.pool.get('res.partner')
719 uom_obj = self.pool.get('product.uom')
720 pricelist_obj = self.pool.get('product.pricelist')
721 prod_obj = self.pool.get('product.product')
722 acc_pos_obj = self.pool.get('account.fiscal.position')
723 po_obj = self.pool.get('purchase.order')
724 for procurement in self.browse(cr, uid, ids):
725 res_id = procurement.move_id.id
726 partner = procurement.product_id.seller_ids[0].name
727 partner_id = partner.id
728 address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
729 pricelist_id = partner.property_product_pricelist_purchase.id
731 uom_id = procurement.product_id.uom_po_id.id
733 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
734 if procurement.product_id.seller_ids[0].qty:
735 qty = max(qty,procurement.product_id.seller_ids[0].qty)
737 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
739 newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
740 newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
741 newdate = newdate - procurement.product_id.seller_ids[0].delay
743 #Passing partner_id to context for purchase order line integrity of Line name
744 context.update({'lang': partner.lang, 'partner_id': partner_id})
746 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
749 'name': product.partner_ref,
751 'product_id': procurement.product_id.id,
752 'product_uom': uom_id,
754 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
755 'move_dest_id': res_id,
756 'notes': product.description_purchase,
759 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
760 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
762 'taxes_id': [(6,0,taxes)]
764 purchase_id = po_obj.create(cr, uid, {
765 'origin': procurement.origin,
766 'partner_id': partner_id,
767 'partner_address_id': address_id,
768 'location_id': procurement.location_id.id,
769 'pricelist_id': pricelist_id,
770 'order_line': [(0,0,line)],
771 'company_id': procurement.company_id.id,
772 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
774 res[procurement.id] = purchase_id
775 self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': purchase_id})
779 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: