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
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
27 from osv import osv, fields
31 from tools import config
32 from tools.translate import _
33 import decimal_precision as dp
34 from osv.orm import browse_record, browse_null
39 class purchase_order(osv.osv):
40 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
42 for order in self.browse(cr, uid, ids):
44 for oline in order.order_line:
45 res[order.id] += oline.price_unit * oline.product_qty
48 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
50 cur_obj=self.pool.get('res.currency')
51 for order in self.browse(cr, uid, ids):
53 'amount_untaxed': 0.0,
58 cur = order.pricelist_id.currency_id
59 for line in order.order_line:
60 val1 += line.price_subtotal
61 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']:
63 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
64 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
65 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
68 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
69 if not value: return False
70 if type(ids)!=type([]):
72 for po in self.browse(cr, uid, ids, context):
73 cr.execute("""update purchase_order_line set
77 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
80 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
82 purchase_obj=self.browse(cr, uid, ids, context=context)
83 for purchase in purchase_obj:
84 res[purchase.id] = time.strftime('%Y-%m-%d %H:%M:%S')
85 if purchase.order_line:
86 min_date=purchase.order_line[0].date_planned
87 for line in purchase.order_line:
88 if line.date_planned < min_date:
89 min_date=line.date_planned
90 res[purchase.id]=min_date
93 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
95 for purchase in self.browse(cursor, user, ids, context=context):
97 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
98 tot += purchase.invoice_id.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):
142 if purchase.invoice_id.reconciled:
143 res[purchase.id] = purchase.invoice_id.reconciled
145 res[purchase.id] = False
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_id': fields.many2one('account.invoice', 'Invoice', readonly=True, help="An invoice 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: no invoice will be pre-generated. The accountant will have to encode manually."
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'])
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={}):
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={}):
271 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
272 for (id,name) in self.name_get(cr, uid, ids):
273 message = _('Purchase order ') + " '" + name + "' "+_("is approved by the supplier")
274 self.log(cr, uid, id, message)
277 #TODO: implement messages system
278 def wkf_confirm_order(self, cr, uid, ids, context={}):
281 for po in self.browse(cr, uid, ids):
282 if not po.order_line:
283 raise osv.except_osv(_('Error !'),_('You can not confirm purchase order without Purchase Order Lines.'))
284 for line in po.order_line:
285 if line.state=='draft':
287 current_name = self.name_get(cr, uid, ids)[0][1]
288 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
290 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
291 for line in po.order_line:
292 product.append(line.product_id.default_code or '')
293 params = ', '.join(map(lambda x : str(x), product))
294 message = _('Purchase order ') + " '" + po.name + "' "+_('placed on')+ " '" + po.date_order + "' "+_('for')+" '" + params + "' "+ _("is confirmed")
295 self.log(cr, uid, id, message)
298 def wkf_warn_buyer(self, cr, uid, ids):
299 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
300 request = pooler.get_pool(cr.dbname).get('res.request')
301 for po in self.browse(cr, uid, ids):
303 for oline in po.order_line:
304 manager = oline.product_id.product_manager
305 if manager and not (manager.id in managers):
306 managers.append(manager.id)
307 for manager_id in managers:
308 request.create(cr, uid,{
309 'name' : "Purchase amount over the limit",
311 'act_to' : manager_id,
312 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
313 'ref_partner_id': po.partner_id.id,
314 'ref_doc1': 'purchase.order,%d' % (po.id,),
316 def inv_line_create(self, cr, uid, a, ol):
320 'price_unit': ol.price_unit or 0.0,
321 'quantity': ol.product_qty,
322 'product_id': ol.product_id.id or False,
323 'uos_id': ol.product_uom.id or False,
324 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
325 'account_analytic_id': ol.account_analytic_id.id or False,
328 def action_cancel_draft(self, cr, uid, ids, *args):
331 self.write(cr, uid, ids, {'state':'draft','shipped':0})
332 wf_service = netsvc.LocalService("workflow")
334 # Deleting the existing instance of workflow for PO
335 wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
336 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
337 for (id,name) in self.name_get(cr, uid, ids):
338 message = _('Purchase order') + " '" + name + "' "+ _("is in the draft state")
339 self.log(cr, uid, id, message)
342 def action_invoice_create(self, cr, uid, ids, *args):
345 journal_obj = self.pool.get('account.journal')
346 for o in self.browse(cr, uid, ids):
349 for ol in o.order_line:
352 a = ol.product_id.product_tmpl_id.property_account_expense.id
354 a = ol.product_id.categ_id.property_account_expense_categ.id
356 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,))
358 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category').id
359 fpos = o.fiscal_position or False
360 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
361 il.append(self.inv_line_create(cr, uid, a, ol))
363 a = o.partner_id.property_account_payable.id
364 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', o.company_id.id)], limit=1)
366 raise osv.except_osv(_('Error !'),
367 _('There is no purchase journal defined for this company: "%s" (id:%d)') % (o.company_id.name, o.company_id.id))
369 'name': o.partner_ref or o.name,
370 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
372 'type': 'in_invoice',
373 'partner_id': o.partner_id.id,
374 'currency_id': o.pricelist_id.currency_id.id,
375 'address_invoice_id': o.partner_address_id.id,
376 'address_contact_id': o.partner_address_id.id,
377 'journal_id': len(journal_ids) and journal_ids[0] or False,
380 'fiscal_position': o.partner_id.property_account_position.id,
381 'payment_term': o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
382 'company_id': o.company_id.id,
384 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
385 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
386 self.pool.get('purchase.order.line').write(cr, uid, todo, {'invoiced':True})
387 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
391 def has_stockable_product(self,cr, uid, ids, *args):
392 for order in self.browse(cr, uid, ids):
393 for order_line in order.order_line:
394 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
398 def action_cancel(self, cr, uid, ids, context={}):
400 purchase_order_line_obj = self.pool.get('purchase.order.line')
401 for purchase in self.browse(cr, uid, ids):
402 for pick in purchase.picking_ids:
403 if pick.state not in ('draft','cancel'):
404 raise osv.except_osv(
405 _('Could not cancel purchase order !'),
406 _('You must first cancel all picking attached to this purchase order.'))
407 for pick in purchase.picking_ids:
408 wf_service = netsvc.LocalService("workflow")
409 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
410 inv = purchase.invoice_id
411 if inv and inv.state not in ('cancel','draft'):
412 raise osv.except_osv(
413 _('Could not cancel this purchase order !'),
414 _('You must first cancel all invoices attached to this purchase order.'))
416 wf_service = netsvc.LocalService("workflow")
417 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
418 self.write(cr,uid,ids,{'state':'cancel'})
419 message = _('Purchase order ') + " '" + purchase.name + "' "+ _("is cancelled")
420 self.log(cr, uid, id, message)
423 def action_picking_create(self,cr, uid, ids, *args):
425 for order in self.browse(cr, uid, ids):
426 loc_id = order.partner_id.property_stock_supplier.id
428 if order.invoice_method=='picking':
429 istate = '2binvoiced'
430 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in')
431 picking_id = self.pool.get('stock.picking').create(cr, uid, {
433 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
435 'address_id': order.dest_address_id.id or order.partner_address_id.id,
436 'invoice_state': istate,
437 'purchase_id': order.id,
438 'company_id': order.company_id.id,
442 for order_line in order.order_line:
443 if not order_line.product_id:
445 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
446 dest = order.location_id.id
447 move = self.pool.get('stock.move').create(cr, uid, {
448 'name': 'PO:'+order_line.name,
449 'product_id': order_line.product_id.id,
450 'product_qty': order_line.product_qty,
451 'product_uos_qty': order_line.product_qty,
452 'product_uom': order_line.product_uom.id,
453 'product_uos': order_line.product_uom.id,
454 'date': order_line.date_planned,
455 'date_expected': order_line.date_planned,
456 'location_id': loc_id,
457 'location_dest_id': dest,
458 'picking_id': picking_id,
459 'move_dest_id': order_line.move_dest_id.id,
461 'purchase_line_id': order_line.id,
462 'company_id': order.company_id.id,
464 if order_line.move_dest_id:
465 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
466 todo_moves.append(move)
467 self.pool.get('stock.move').action_confirm(cr, uid, todo_moves)
468 self.pool.get('stock.move').force_assign(cr, uid, todo_moves)
469 wf_service = netsvc.LocalService("workflow")
470 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
473 def copy(self, cr, uid, id, default=None,context={}):
482 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
484 return super(purchase_order, self).copy(cr, uid, id, default, context)
487 def do_merge(self, cr, uid, ids, context):
489 To merge similar type of purchase orders.
490 Orders will only be merged if:
491 * Purchase Orders are in draft
492 * Purchase Orders belong to the same partner
493 * Purchase Orders are have same stock location, same pricelist
494 Lines will only be merged if:
495 * Order lines are exactly the same except for the quantity and unit
497 @param self: The object pointer.
498 @param cr: A database cursor
499 @param uid: ID of the user currently logged in
500 @param ids: the ID or list of IDs
501 @param context: A standard dictionary
503 @return: new purchase order id
506 wf_service = netsvc.LocalService("workflow")
507 def make_key(br, fields):
510 field_val = getattr(br, field)
511 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
514 if isinstance(field_val, browse_record):
515 field_val = field_val.id
516 elif isinstance(field_val, browse_null):
518 elif isinstance(field_val, list):
519 field_val = ((6, 0, tuple([v.id for v in field_val])),)
520 list_key.append((field, field_val))
522 return tuple(list_key)
524 # compute what the new orders should contain
528 for porder in [order for order in self.browse(cr, uid, ids) if order.state == 'draft']:
529 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
530 new_order = new_orders.setdefault(order_key, ({}, []))
531 new_order[1].append(porder.id)
532 order_infos = new_order[0]
535 'origin': porder.origin,
536 'date_order': time.strftime('%Y-%m-%d'),
537 'partner_id': porder.partner_id.id,
538 'partner_address_id': porder.partner_address_id.id,
539 'dest_address_id': porder.dest_address_id.id,
540 'warehouse_id': porder.warehouse_id.id,
541 'location_id': porder.location_id.id,
542 'pricelist_id': porder.pricelist_id.id,
545 'notes': '%s' % (porder.notes or '',),
546 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
550 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
552 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
554 for order_line in porder.order_line:
555 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
556 o_line = order_infos['order_line'].setdefault(line_key, {})
558 # merge the line with an existing line
559 o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
561 # append a new "standalone" line
562 for field in ('product_qty', 'product_uom'):
563 field_val = getattr(order_line, field)
564 if isinstance(field_val, browse_record):
565 field_val = field_val.id
566 o_line[field] = field_val
567 o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
572 for order_key, (order_data, old_ids) in new_orders.iteritems():
573 # skip merges with only one order
575 allorders += (old_ids or [])
578 # cleanup order line data
579 for key, value in order_data['order_line'].iteritems():
580 del value['uom_factor']
581 value.update(dict(key))
582 order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
584 # create the new order
585 neworder_id = self.create(cr, uid, order_data)
586 allorders.append(neworder_id)
588 # make triggers pointing to the old orders point to the new order
589 for old_id in old_ids:
590 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
591 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
596 class purchase_order_line(osv.osv):
597 def _amount_line(self, cr, uid, ids, prop, arg,context):
599 cur_obj=self.pool.get('res.currency')
600 tax_obj = self.pool.get('account.tax')
601 for line in self.browse(cr, uid, ids, context=context):
602 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
603 cur = line.order_id.pricelist_id.currency_id
604 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
608 'name': fields.char('Description', size=256, required=True),
609 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
610 'date_planned': fields.date('Scheduled date', required=True),
611 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
612 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
613 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
614 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
615 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
616 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
617 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
618 'notes': fields.text('Notes'),
619 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
620 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
621 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company'),
622 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
623 help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
624 \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
625 \n* The \'Done\' state is set automatically when purchase order is set as done. \
626 \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
627 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
628 'invoiced': fields.boolean('Invoiced', readonly=True),
629 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner"),
630 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
634 'product_qty': lambda *a: 1.0,
635 'state': lambda *args: 'draft',
636 'invoiced': lambda *a: 0,
638 _table = 'purchase_order_line'
639 _name = 'purchase.order.line'
640 _description = 'Purchase Order Line'
642 def copy_data(self, cr, uid, id, default=None,context={}):
645 default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
646 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
648 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
649 partner_id, date_order=False, fiscal_position=False, date_planned=False,
650 name=False, price_unit=False, notes=False):
652 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.'))
654 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
656 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
657 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
658 prod= self.pool.get('product.product').browse(cr, uid, product)
661 lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
662 context={'lang':lang}
663 context['partner_id'] = partner_id
665 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
666 prod_uom_po = prod.uom_po_id.id
670 date_order = time.strftime('%Y-%m-%d')
673 for s in prod.seller_ids:
674 if s.name.id == partner_id:
675 seller_delay = s.delay
676 temp_qty = s.qty # supplier _qty assigned to temp
677 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
682 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
683 product, qty or 1.0, partner_id, {
687 dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
688 prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id])[0][1]
691 res = {'value': {'price_unit': price, 'name': name or prod_name,
692 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
693 'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
698 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
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={}):
723 self.write(cr, uid, ids, {'state': 'confirmed'}, context)
724 for (id,name) in self.name_get(cr, uid, ids):
725 message = _('Purchase order line') + " '" + name + "' "+ _("is confirmed")
726 self.log(cr, uid, id, message)
729 purchase_order_line()
731 class procurement_order(osv.osv):
732 _inherit = 'procurement.order'
734 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
737 def action_po_assign(self, cr, uid, ids, context={}):
738 """ This is action which call from workflow to assign purchase order to procurements
741 res = self.make_po(cr, uid, ids, context=context)
743 return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
745 def make_po(self, cr, uid, ids, context={}):
746 """ Make purchase order from procurement
747 @return: New created Purchase Orders procurement wise
750 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
751 partner_obj = self.pool.get('res.partner')
752 uom_obj = self.pool.get('product.uom')
753 pricelist_obj = self.pool.get('product.pricelist')
754 prod_obj = self.pool.get('product.product')
755 acc_pos_obj = self.pool.get('account.fiscal.position')
756 po_obj = self.pool.get('purchase.order')
757 for procurement in self.browse(cr, uid, ids):
758 res_id = procurement.move_id.id
759 partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
760 seller_qty = procurement.product_id.seller_qty
761 seller_delay = int(procurement.product_id.seller_delay)
762 partner_id = partner.id
763 address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
764 pricelist_id = partner.property_product_pricelist_purchase.id
766 uom_id = procurement.product_id.uom_po_id.id
768 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
770 qty = max(qty,seller_qty)
772 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
774 newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
775 newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
776 newdate = newdate - seller_delay
778 #Passing partner_id to context for purchase order line integrity of Line name
779 context.update({'lang': partner.lang, 'partner_id': partner_id})
781 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
784 'name': product.partner_ref,
786 'product_id': procurement.product_id.id,
787 'product_uom': uom_id,
789 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
790 'move_dest_id': res_id,
791 'notes': product.description_purchase,
794 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
795 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
797 'taxes_id': [(6,0,taxes)]
799 purchase_id = po_obj.create(cr, uid, {
800 'origin': procurement.origin,
801 'partner_id': partner_id,
802 'partner_address_id': address_id,
803 'location_id': procurement.location_id.id,
804 'pricelist_id': pricelist_id,
805 'order_line': [(0,0,line)],
806 'company_id': procurement.company_id.id,
807 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
809 res[procurement.id] = purchase_id
810 self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': purchase_id})
815 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: