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 ##############################################################################
23 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
26 from osv import osv, fields
29 from tools.translate import _
30 import decimal_precision as dp
31 from osv.orm import browse_record, browse_null
36 class purchase_order(osv.osv):
38 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
40 for order in self.browse(cr, uid, ids):
42 for oline in order.order_line:
43 res[order.id] += oline.price_unit * oline.product_qty
46 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
48 cur_obj=self.pool.get('res.currency')
49 for order in self.browse(cr, uid, ids, context=context):
51 'amount_untaxed': 0.0,
56 cur = order.pricelist_id.currency_id
57 for line in order.order_line:
58 val1 += line.price_subtotal
59 for c in self.pool.get('account.tax').compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, order.partner_address_id.id, line.product_id.id, order.partner_id)['taxes']:
60 val += c.get('amount', 0.0)
61 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
62 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
63 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
66 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
67 if not value: return False
68 if type(ids)!=type([]):
70 for po in self.browse(cr, uid, ids, context=context):
71 cr.execute("""update purchase_order_line set
75 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
78 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
80 purchase_obj=self.browse(cr, uid, ids, context=context)
81 for purchase in purchase_obj:
82 res[purchase.id] = time.strftime('%Y-%m-%d %H:%M:%S')
83 if purchase.order_line:
84 min_date=purchase.order_line[0].date_planned
85 for line in purchase.order_line:
86 if line.date_planned < min_date:
87 min_date=line.date_planned
88 res[purchase.id]=min_date
92 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
94 for purchase in self.browse(cursor, user, ids, context=context):
96 for invoice in purchase.invoice_ids:
97 if invoice.state not in ('draft','cancel'):
98 tot += invoice.amount_untaxed
99 if purchase.amount_untaxed:
100 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
102 res[purchase.id] = 0.0
105 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
106 if not ids: return {}
111 p.purchase_id,sum(m.product_qty), m.state
115 stock_picking p on (p.id=m.picking_id)
117 p.purchase_id IN %s GROUP BY m.state, p.purchase_id''',(tuple(ids),))
118 for oid,nbr,state in cr.fetchall():
122 res[oid][0] += nbr or 0.0
123 res[oid][1] += nbr or 0.0
125 res[oid][1] += nbr or 0.0
130 res[r] = 100.0 * res[r][0] / res[r][1]
133 def _get_order(self, cr, uid, ids, context=None):
135 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
136 result[line.order_id.id] = True
139 def _invoiced(self, cursor, user, ids, name, arg, context=None):
141 for purchase in self.browse(cursor, user, ids, context=context):
143 if purchase.invoiced_rate == 100.00:
145 res[purchase.id] = invoiced
149 ('draft', 'Request for Quotation'),
151 ('confirmed', 'Waiting Supplier Ack'),
152 ('approved', 'Approved'),
153 ('except_picking', 'Shipping Exception'),
154 ('except_invoice', 'Invoice Exception'),
156 ('cancel', 'Cancelled')
160 'name': fields.char('Order Reference', size=64, required=True, select=True, help="unique number of the purchase order,computed automatically when the purchase order is created"),
161 'origin': fields.char('Source Document', size=64,
162 help="Reference of the document that generated this purchase order request."
164 'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, size=64),
165 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, help="Date on which this document has been created."),
166 'date_approve':fields.date('Date Approved', readonly=1, help="Date on which purchase order has been approved"),
167 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, change_default=True),
168 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True,
169 states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},domain="[('partner_id', '=', partner_id)]"),
170 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address',
171 states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
172 help="Put an address if you want to deliver directly from the supplier to the customer." \
173 "In this case, it will remove the warehouse link and set the customer location."
175 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}),
176 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')]),
177 '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."),
178 'state': fields.selection(STATE_SELECTION, '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),
179 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
180 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
181 'notes': fields.text('Notes'),
182 'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id', 'invoice_id', 'Taxes', help="Invoices generated for a purchase order"),
183 '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"),
184 'shipped':fields.boolean('Received', readonly=True, select=True, help="It indicates that a picking has been done"),
185 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
186 'invoiced': fields.function(_invoiced, method=True, string='Invoiced & Paid', type='boolean', help="It indicates that an invoice has been paid"),
187 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
188 'invoice_method': fields.selection([('manual','Manual'),('order','From Order'),('picking','From Picking')], 'Invoicing Control', required=True,
189 help="From Order: a draft invoice will be pre-generated based on the purchase order. The accountant " \
190 "will just have to validate this invoice for control.\n" \
191 "From Picking: a draft invoice will be pre-generated based on validated receptions.\n" \
192 "Manual: allows you to generate suppliers invoices by chosing in the uninvoiced lines of all manual purchase orders."
194 'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, method=True,store=True, string='Expected Date', type='date', help="This is computed as the minimum scheduled date of all purchase order lines' products."),
195 'amount_untaxed': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Untaxed Amount',
197 'purchase.order.line': (_get_order, None, 10),
198 }, multi="sums", help="The amount without tax"),
199 'amount_tax': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Taxes',
201 'purchase.order.line': (_get_order, None, 10),
202 }, multi="sums", help="The tax amount"),
203 'amount_total': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Total',
205 'purchase.order.line': (_get_order, None, 10),
206 }, multi="sums",help="The total amount"),
207 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
208 'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
209 'create_uid': fields.many2one('res.users', 'Responsible'),
210 'company_id': fields.many2one('res.company','Company',required=True,select=1),
213 'date_order': time.strftime('%Y-%m-%d'),
215 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
217 'invoice_method': 'order',
219 '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'],
220 '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,
221 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
223 _name = "purchase.order"
224 _description = "Purchase Order"
227 def unlink(self, cr, uid, ids, context=None):
228 purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
230 for s in purchase_orders:
231 if s['state'] in ['draft','cancel']:
232 unlink_ids.append(s['id'])
234 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!') % _(dict(purchase_order.STATE_SELECTION).get(s['state'])))
236 # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
237 # automatically sending subflow.delete upon deletion
238 wf_service = netsvc.LocalService("workflow")
239 for id in unlink_ids:
240 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
242 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
244 def button_dummy(self, cr, uid, ids, context=None):
247 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
250 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
251 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
252 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
254 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
257 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
258 return {'value':{'location_id': res, 'dest_address_id': False}}
260 def onchange_partner_id(self, cr, uid, ids, part):
263 return {'value':{'partner_address_id': False, 'fiscal_position': False}}
264 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
265 part = self.pool.get('res.partner').browse(cr, uid, part)
266 pricelist = part.property_product_pricelist_purchase.id
267 fiscal_position = part.property_account_position and part.property_account_position.id or False
268 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
270 def wkf_approve_order(self, cr, uid, ids, context=None):
271 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
274 #TODO: implement messages system
275 def wkf_confirm_order(self, cr, uid, ids, context=None):
277 for po in self.browse(cr, uid, ids, context=context):
278 if not po.order_line:
279 raise osv.except_osv(_('Error !'),_('You can not confirm purchase order without Purchase Order Lines.'))
280 for line in po.order_line:
281 if line.state=='draft':
283 message = _("Purchase order '%s' is confirmed.") % (po.name,)
284 self.log(cr, uid, po.id, message)
285 # current_name = self.name_get(cr, uid, ids)[0][1]
286 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
288 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
291 def wkf_warn_buyer(self, cr, uid, ids):
292 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
293 request = pooler.get_pool(cr.dbname).get('res.request')
294 for po in self.browse(cr, uid, ids):
296 for oline in po.order_line:
297 manager = oline.product_id.product_manager
298 if manager and not (manager.id in managers):
299 managers.append(manager.id)
300 for manager_id in managers:
301 request.create(cr, uid,{
302 'name' : _("Purchase amount over the limit"),
304 'act_to' : manager_id,
305 'body': _('Somebody has just confirmed a purchase with an amount over the defined limit'),
306 'ref_partner_id': po.partner_id.id,
307 'ref_doc1': 'purchase.order,%d' % (po.id,),
309 def inv_line_create(self, cr, uid, a, ol):
313 'price_unit': ol.price_unit or 0.0,
314 'quantity': ol.product_qty,
315 'product_id': ol.product_id.id or False,
316 'uos_id': ol.product_uom.id or False,
317 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
318 'account_analytic_id': ol.account_analytic_id.id or False,
321 def action_cancel_draft(self, cr, uid, ids, *args):
324 self.write(cr, uid, ids, {'state':'draft','shipped':0})
325 wf_service = netsvc.LocalService("workflow")
327 # Deleting the existing instance of workflow for PO
328 wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
329 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
330 for (id,name) in self.name_get(cr, uid, ids):
331 message = _("Purchase order '%s' has been set in draft state.") % name
332 self.log(cr, uid, id, message)
335 def action_invoice_create(self, cr, uid, ids, *args):
338 journal_obj = self.pool.get('account.journal')
339 for o in self.browse(cr, uid, ids):
342 for ol in o.order_line:
345 a = ol.product_id.product_tmpl_id.property_account_expense.id
347 a = ol.product_id.categ_id.property_account_expense_categ.id
349 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,))
351 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category').id
352 fpos = o.fiscal_position or False
353 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
354 il.append(self.inv_line_create(cr, uid, a, ol))
356 a = o.partner_id.property_account_payable.id
357 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', o.company_id.id)], limit=1)
359 raise osv.except_osv(_('Error !'),
360 _('There is no purchase journal defined for this company: "%s" (id:%d)') % (o.company_id.name, o.company_id.id))
362 'name': o.partner_ref or o.name,
363 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
365 'type': 'in_invoice',
366 'partner_id': o.partner_id.id,
367 'currency_id': o.pricelist_id.currency_id.id,
368 'address_invoice_id': o.partner_address_id.id,
369 'address_contact_id': o.partner_address_id.id,
370 'journal_id': len(journal_ids) and journal_ids[0] or False,
373 'fiscal_position': o.fiscal_position.id or o.partner_id.property_account_position.id,
374 'payment_term': o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
375 'company_id': o.company_id.id,
377 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
378 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
379 self.pool.get('purchase.order.line').write(cr, uid, todo, {'invoiced':True})
380 self.write(cr, uid, [o.id], {'invoice_ids': [(4, inv_id)]})
384 def has_stockable_product(self,cr, uid, ids, *args):
385 for order in self.browse(cr, uid, ids):
386 for order_line in order.order_line:
387 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
391 def action_cancel(self, cr, uid, ids, context=None):
392 for purchase in self.browse(cr, uid, ids, context=context):
393 for pick in purchase.picking_ids:
394 if pick.state not in ('draft','cancel'):
395 raise osv.except_osv(
396 _('Could not cancel purchase order !'),
397 _('You must first cancel all picking attached to this purchase order.'))
398 for pick in purchase.picking_ids:
399 wf_service = netsvc.LocalService("workflow")
400 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
401 for inv in purchase.invoice_ids:
402 if inv and inv.state not in ('cancel','draft'):
403 raise osv.except_osv(
404 _('Could not cancel this purchase order !'),
405 _('You must first cancel all invoices attached to this purchase order.'))
407 wf_service = netsvc.LocalService("workflow")
408 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
409 self.write(cr,uid,ids,{'state':'cancel'})
410 for (id,name) in self.name_get(cr, uid, ids):
411 message = _("Purchase order '%s' is cancelled.") % name
412 self.log(cr, uid, id, message)
415 def action_picking_create(self,cr, uid, ids, *args):
417 for order in self.browse(cr, uid, ids):
418 loc_id = order.partner_id.property_stock_supplier.id
420 if order.invoice_method=='picking':
421 istate = '2binvoiced'
422 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in')
423 picking_id = self.pool.get('stock.picking').create(cr, uid, {
425 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
427 'address_id': order.dest_address_id.id or order.partner_address_id.id,
428 'invoice_state': istate,
429 'purchase_id': order.id,
430 'company_id': order.company_id.id,
434 for order_line in order.order_line:
435 if not order_line.product_id:
437 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
438 dest = order.location_id.id
439 move = self.pool.get('stock.move').create(cr, uid, {
440 'name': 'PO:'+order_line.name,
441 'product_id': order_line.product_id.id,
442 'product_qty': order_line.product_qty,
443 'product_uos_qty': order_line.product_qty,
444 'product_uom': order_line.product_uom.id,
445 'product_uos': order_line.product_uom.id,
446 'date': order_line.date_planned,
447 'date_expected': order_line.date_planned,
448 'location_id': loc_id,
449 'location_dest_id': dest,
450 'picking_id': picking_id,
451 'move_dest_id': order_line.move_dest_id.id,
453 'purchase_line_id': order_line.id,
454 'company_id': order.company_id.id,
456 if order_line.move_dest_id:
457 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
458 todo_moves.append(move)
459 self.pool.get('stock.move').action_confirm(cr, uid, todo_moves)
460 self.pool.get('stock.move').force_assign(cr, uid, todo_moves)
461 wf_service = netsvc.LocalService("workflow")
462 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
465 def copy(self, cr, uid, id, default=None, context=None):
474 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
476 return super(purchase_order, self).copy(cr, uid, id, default, context)
479 def do_merge(self, cr, uid, ids, context=None):
481 To merge similar type of purchase orders.
482 Orders will only be merged if:
483 * Purchase Orders are in draft
484 * Purchase Orders belong to the same partner
485 * Purchase Orders are have same stock location, same pricelist
486 Lines will only be merged if:
487 * Order lines are exactly the same except for the quantity and unit
489 @param self: The object pointer.
490 @param cr: A database cursor
491 @param uid: ID of the user currently logged in
492 @param ids: the ID or list of IDs
493 @param context: A standard dictionary
495 @return: new purchase order id
498 wf_service = netsvc.LocalService("workflow")
499 def make_key(br, fields):
502 field_val = getattr(br, field)
503 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
506 if isinstance(field_val, browse_record):
507 field_val = field_val.id
508 elif isinstance(field_val, browse_null):
510 elif isinstance(field_val, list):
511 field_val = ((6, 0, tuple([v.id for v in field_val])),)
512 list_key.append((field, field_val))
514 return tuple(list_key)
516 # compute what the new orders should contain
520 for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
521 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
522 new_order = new_orders.setdefault(order_key, ({}, []))
523 new_order[1].append(porder.id)
524 order_infos = new_order[0]
527 'origin': porder.origin,
528 'date_order': time.strftime('%Y-%m-%d'),
529 'partner_id': porder.partner_id.id,
530 'partner_address_id': porder.partner_address_id.id,
531 'dest_address_id': porder.dest_address_id.id,
532 'warehouse_id': porder.warehouse_id.id,
533 'location_id': porder.location_id.id,
534 'pricelist_id': porder.pricelist_id.id,
537 'notes': '%s' % (porder.notes or '',),
538 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
542 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
544 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
546 for order_line in porder.order_line:
547 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
548 o_line = order_infos['order_line'].setdefault(line_key, {})
550 # merge the line with an existing line
551 o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
553 # append a new "standalone" line
554 for field in ('product_qty', 'product_uom'):
555 field_val = getattr(order_line, field)
556 if isinstance(field_val, browse_record):
557 field_val = field_val.id
558 o_line[field] = field_val
559 o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
565 for order_key, (order_data, old_ids) in new_orders.iteritems():
566 # skip merges with only one order
568 allorders += (old_ids or [])
571 # cleanup order line data
572 for key, value in order_data['order_line'].iteritems():
573 del value['uom_factor']
574 value.update(dict(key))
575 order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
577 # create the new order
578 neworder_id = self.create(cr, uid, order_data)
579 orders_info.update({neworder_id: old_ids})
580 allorders.append(neworder_id)
582 # make triggers pointing to the old orders point to the new order
583 for old_id in old_ids:
584 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
585 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
590 class purchase_order_line(osv.osv):
591 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
593 cur_obj=self.pool.get('res.currency')
594 tax_obj = self.pool.get('account.tax')
595 for line in self.browse(cr, uid, ids, context=context):
596 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
597 cur = line.order_id.pricelist_id.currency_id
598 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
602 'name': fields.char('Description', size=256, required=True),
603 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
604 'date_planned': fields.date('Scheduled Date', required=True),
605 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
606 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
607 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
608 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
609 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
610 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
611 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
612 'notes': fields.text('Notes'),
613 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
614 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
615 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company'),
616 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
617 help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
618 \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
619 \n* The \'Done\' state is set automatically when purchase order is set as done. \
620 \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
621 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
622 'invoiced': fields.boolean('Invoiced', readonly=True),
623 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
624 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
628 'product_qty': lambda *a: 1.0,
629 'state': lambda *args: 'draft',
630 'invoiced': lambda *a: 0,
632 _table = 'purchase_order_line'
633 _name = 'purchase.order.line'
634 _description = 'Purchase Order Line'
636 def copy_data(self, cr, uid, id, default=None, context=None):
639 default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
640 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
642 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
643 partner_id, date_order=False, fiscal_position=False, date_planned=False,
644 name=False, price_unit=False, notes=False):
646 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.'))
648 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
650 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
651 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
652 prod= self.pool.get('product.product').browse(cr, uid, product)
654 product_uom_pool = self.pool.get('product.uom')
657 lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
658 context={'lang':lang}
659 context['partner_id'] = partner_id
661 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
662 prod_uom_po = prod.uom_po_id.id
666 date_order = time.strftime('%Y-%m-%d')
670 prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id], context=context)[0][1]
672 for s in prod.seller_ids:
673 if s.name.id == partner_id:
674 seller_delay = s.delay
675 temp_qty = s.min_qty # supplier _qty assigned to temp
677 qty = product_uom_pool._compute_qty(cr, uid, s.product_uom.id, s.min_qty, to_uom_id=prod_uom_po)
679 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
684 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
685 product, 1.0, partner_id, {
689 dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
692 res = {'value': {'price_unit': price, 'name': name or prod_name,
693 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
694 'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
699 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
700 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
701 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
703 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
704 res3 = prod.uom_id.category_id.id
705 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
706 if res2[0]['category_id'][0] != res3:
707 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'))
709 res['domain'] = domain
712 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
713 partner_id, date_order=False,fiscal_position=False):
714 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
715 partner_id, date_order=date_order,fiscal_position=fiscal_position)
716 if 'product_uom' in res['value']:
717 del res['value']['product_uom']
719 res['value']['price_unit'] = 0.0
722 def action_confirm(self, cr, uid, ids, context=None):
723 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
726 purchase_order_line()
728 class procurement_order(osv.osv):
729 _inherit = 'procurement.order'
731 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
734 def action_po_assign(self, cr, uid, ids, context=None):
735 """ This is action which call from workflow to assign purchase order to procurements
738 res = self.make_po(cr, uid, ids, context=context)
740 return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
742 def make_po(self, cr, uid, ids, context=None):
743 """ Make purchase order from procurement
744 @return: New created Purchase Orders procurement wise
749 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
750 partner_obj = self.pool.get('res.partner')
751 uom_obj = self.pool.get('product.uom')
752 pricelist_obj = self.pool.get('product.pricelist')
753 prod_obj = self.pool.get('product.product')
754 acc_pos_obj = self.pool.get('account.fiscal.position')
755 po_obj = self.pool.get('purchase.order')
756 for procurement in self.browse(cr, uid, ids, context=context):
757 res_id = procurement.move_id.id
758 partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
759 seller_qty = procurement.product_id.seller_qty
760 seller_delay = int(procurement.product_id.seller_delay)
761 partner_id = partner.id
762 address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
763 pricelist_id = partner.property_product_pricelist_purchase.id
765 uom_id = procurement.product_id.uom_po_id.id
767 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
769 qty = max(qty,seller_qty)
771 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
773 newdate = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
774 newdate = (newdate - relativedelta(days=company.po_lead)) - relativedelta(days=seller_delay)
776 #Passing partner_id to context for purchase order line integrity of Line name
777 context.update({'lang': partner.lang, 'partner_id': partner_id})
779 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
782 'name': product.partner_ref,
784 'product_id': procurement.product_id.id,
785 'product_uom': uom_id,
787 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
788 'move_dest_id': res_id,
789 'notes': product.description_purchase,
792 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
793 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
795 'taxes_id': [(6,0,taxes)]
797 purchase_id = po_obj.create(cr, uid, {
798 'origin': procurement.origin,
799 'partner_id': partner_id,
800 'partner_address_id': address_id,
801 'location_id': procurement.location_id.id,
802 'pricelist_id': pricelist_id,
803 'order_line': [(0,0,line)],
804 'company_id': procurement.company_id.id,
805 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
807 res[procurement.id] = purchase_id
808 self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': purchase_id})
813 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: