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 _amount_all(self, cr, uid, ids, field_name, arg, context=None):
40 cur_obj=self.pool.get('res.currency')
41 for order in self.browse(cr, uid, ids, context=context):
43 'amount_untaxed': 0.0,
48 cur = order.pricelist_id.currency_id
49 for line in order.order_line:
50 val1 += line.price_subtotal
51 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']:
52 val += c.get('amount', 0.0)
53 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
54 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
55 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
58 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
59 if not value: return False
60 if type(ids)!=type([]):
62 for po in self.browse(cr, uid, ids, context=context):
64 cr.execute("""update purchase_order_line set
68 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
69 cr.execute("""update purchase_order set
70 minimum_planned_date=%s where id=%s""", (value, po.id))
73 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
75 purchase_obj=self.browse(cr, uid, ids, context=context)
76 for purchase in purchase_obj:
77 res[purchase.id] = False
78 if purchase.order_line:
79 min_date=purchase.order_line[0].date_planned
80 for line in purchase.order_line:
81 if line.date_planned < min_date:
82 min_date=line.date_planned
83 res[purchase.id]=min_date
87 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
89 for purchase in self.browse(cursor, user, ids, context=context):
91 for invoice in purchase.invoice_ids:
92 if invoice.state not in ('draft','cancel'):
93 tot += invoice.amount_untaxed
94 if purchase.amount_untaxed:
95 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
97 res[purchase.id] = 0.0
100 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
101 if not ids: return {}
106 p.purchase_id,sum(m.product_qty), m.state
110 stock_picking p on (p.id=m.picking_id)
112 p.purchase_id IN %s GROUP BY m.state, p.purchase_id''',(tuple(ids),))
113 for oid,nbr,state in cr.fetchall():
117 res[oid][0] += nbr or 0.0
118 res[oid][1] += nbr or 0.0
120 res[oid][1] += nbr or 0.0
125 res[r] = 100.0 * res[r][0] / res[r][1]
128 def _get_order(self, cr, uid, ids, context=None):
130 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
131 result[line.order_id.id] = True
134 def _invoiced(self, cursor, user, ids, name, arg, context=None):
136 for purchase in self.browse(cursor, user, ids, context=context):
138 if purchase.invoiced_rate == 100.00:
140 res[purchase.id] = invoiced
144 ('draft', 'Request for Quotation'),
146 ('confirmed', 'Waiting Approval'),
147 ('approved', 'Approved'),
148 ('except_picking', 'Shipping Exception'),
149 ('except_invoice', 'Invoice Exception'),
151 ('cancel', 'Cancelled')
155 '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"),
156 'origin': fields.char('Source Document', size=64,
157 help="Reference of the document that generated this purchase order request."
159 'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, size=64),
160 'date_order':fields.date('Order Date', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, select=True, help="Date on which this document has been created."),
161 'date_approve':fields.date('Date Approved', readonly=1, select=True, help="Date on which purchase order has been approved"),
162 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, change_default=True),
163 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True,
164 states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},domain="[('partner_id', '=', partner_id)]"),
165 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', domain="[('partner_id', '!=', False)]",
166 states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
167 help="Put an address if you want to deliver directly from the supplier to the customer." \
168 "In this case, it will remove the warehouse link and set the customer location."
170 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}),
171 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')]),
172 '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."),
173 '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),
174 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
175 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
176 'notes': fields.text('Notes'),
177 'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id', 'invoice_id', 'Invoices', help="Invoices generated for a purchase order"),
178 '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"),
179 'shipped':fields.boolean('Received', readonly=True, select=True, help="It indicates that a picking has been done"),
180 'shipped_rate': fields.function(_shipped_rate, string='Received', type='float'),
181 'invoiced': fields.function(_invoiced, string='Invoiced & Paid', type='boolean', help="It indicates that an invoice has been paid"),
182 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
183 'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on receptions')], 'Invoicing Control', required=True,
184 help="Based on Purchase Order lines: place individual lines in 'Invoice Control > Based on P.O. lines' from where you can selectively create an invoice.\n" \
185 "Based on generated invoice: create a draft invoice you can validate later.\n" \
186 "Based on receptions: let you create an invoice when receptions are validated."
188 'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, string='Expected Date', type='date', select=True, help="This is computed as the minimum scheduled date of all purchase order lines' products.",
190 'purchase.order.line': (_get_order, ['date_planned'], 10),
193 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Untaxed Amount',
195 'purchase.order.line': (_get_order, None, 10),
196 }, multi="sums", help="The amount without tax"),
197 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Taxes',
199 'purchase.order.line': (_get_order, None, 10),
200 }, multi="sums", help="The tax amount"),
201 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Total',
203 'purchase.order.line': (_get_order, None, 10),
204 }, multi="sums",help="The total amount"),
205 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
206 'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
207 'create_uid': fields.many2one('res.users', 'Responsible'),
208 'company_id': fields.many2one('res.company','Company',required=True,select=1),
211 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
213 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
215 'invoice_method': 'order',
217 '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'],
218 '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,
219 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
222 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
224 _name = "purchase.order"
225 _description = "Purchase Order"
228 def unlink(self, cr, uid, ids, context=None):
229 purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
231 for s in purchase_orders:
232 if s['state'] in ['draft','cancel']:
233 unlink_ids.append(s['id'])
235 raise osv.except_osv(_('Invalid action !'), _('In order to delete a purchase order, it must be cancelled first!'))
237 # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
238 # automatically sending subflow.delete upon deletion
239 wf_service = netsvc.LocalService("workflow")
240 for id in unlink_ids:
241 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
243 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
245 def button_dummy(self, cr, uid, ids, context=None):
248 def onchange_dest_address_id(self, cr, uid, ids, address_id):
251 address = self.pool.get('res.partner.address')
252 values = {'warehouse_id': False}
253 supplier = address.browse(cr, uid, address_id).partner_id
255 location_id = supplier.property_stock_customer.id
256 values.update({'location_id': location_id})
257 return {'value':values}
259 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
262 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id)
263 return {'value':{'location_id': warehouse.lot_input_id.id, 'dest_address_id': False}}
265 def onchange_partner_id(self, cr, uid, ids, partner_id):
266 partner = self.pool.get('res.partner')
268 return {'value':{'partner_address_id': False, 'fiscal_position': False}}
269 supplier_address = partner.address_get(cr, uid, [partner_id], ['default'])
270 supplier = partner.browse(cr, uid, partner_id)
271 pricelist = supplier.property_product_pricelist_purchase.id
272 fiscal_position = supplier.property_account_position and supplier.property_account_position.id or False
273 return {'value':{'partner_address_id': supplier_address['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
275 def wkf_approve_order(self, cr, uid, ids, context=None):
276 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
279 #TODO: implement messages system
280 def wkf_confirm_order(self, cr, uid, ids, context=None):
282 for po in self.browse(cr, uid, ids, context=context):
283 if not po.order_line:
284 raise osv.except_osv(_('Error !'),_('You cannot confirm a purchase order without any lines.'))
285 for line in po.order_line:
286 if line.state=='draft':
288 message = _("Purchase order '%s' is confirmed.") % (po.name,)
289 self.log(cr, uid, po.id, message)
290 # current_name = self.name_get(cr, uid, ids)[0][1]
291 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
293 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
296 def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
297 """Collects require data from purchase order line that is used to create invoice line
298 for that purchase order line
299 :param account_id: Expense account of the product of PO line if any.
300 :param browse_record order_line: Purchase order line browse record
301 :return: Value for fields of invoice lines.
305 'name': order_line.name,
306 'account_id': account_id,
307 'price_unit': order_line.price_unit or 0.0,
308 'quantity': order_line.product_qty,
309 'product_id': order_line.product_id.id or False,
310 'uos_id': order_line.product_uom.id or False,
311 'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
312 'account_analytic_id': order_line.account_analytic_id.id or False,
315 def action_cancel_draft(self, cr, uid, ids, *args):
318 self.write(cr, uid, ids, {'state':'draft','shipped':0})
319 wf_service = netsvc.LocalService("workflow")
321 # Deleting the existing instance of workflow for PO
322 wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
323 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
324 for (id,name) in self.name_get(cr, uid, ids):
325 message = _("Purchase order '%s' has been set in draft state.") % name
326 self.log(cr, uid, id, message)
329 def action_invoice_create(self, cr, uid, ids, context=None):
330 """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
331 :param ids: list of ids of purchase orders.
332 :return: ID of created invoice.
337 journal_obj = self.pool.get('account.journal')
338 inv_obj = self.pool.get('account.invoice')
339 inv_line_obj = self.pool.get('account.invoice.line')
340 fiscal_obj = self.pool.get('account.fiscal.position')
341 property_obj = self.pool.get('ir.property')
343 for order in self.browse(cr, uid, ids, context=context):
344 pay_acc_id = order.partner_id.property_account_payable.id
345 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', order.company_id.id)], limit=1)
347 raise osv.except_osv(_('Error !'),
348 _('There is no purchase journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
350 # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
352 for po_line in order.order_line:
353 if po_line.product_id:
354 acc_id = po_line.product_id.product_tmpl_id.property_account_expense.id
356 acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
358 raise osv.except_osv(_('Error !'), _('There is no expense account defined for this product: "%s" (id:%d)') % (po_line.product_id.name, po_line.product_id.id,))
360 acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category').id
361 fpos = order.fiscal_position or False
362 acc_id = fiscal_obj.map_account(cr, uid, fpos, acc_id)
364 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
365 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
366 inv_lines.append(inv_line_id)
368 po_line.write({'invoiced':True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
370 # get invoice data and create invoice
372 'name': order.partner_ref or order.name,
373 'reference': order.partner_ref or order.name,
374 'account_id': pay_acc_id,
375 'type': 'in_invoice',
376 'partner_id': order.partner_id.id,
377 'currency_id': order.pricelist_id.currency_id.id,
378 'address_invoice_id': order.partner_address_id.id,
379 'address_contact_id': order.partner_address_id.id,
380 'journal_id': len(journal_ids) and journal_ids[0] or False,
381 'invoice_line': [(6, 0, inv_lines)],
382 'origin': order.name,
383 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
384 'payment_term': order.partner_id.property_payment_term and order.partner_id.property_payment_term.id or False,
385 'company_id': order.company_id.id,
387 inv_id = inv_obj.create(cr, uid, inv_data, context=context)
389 # compute the invoice
390 inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
392 # Link this new invoice to related purchase order
393 order.write({'invoice_ids': [(4, inv_id)]}, context=context)
397 def has_stockable_product(self,cr, uid, ids, *args):
398 for order in self.browse(cr, uid, ids):
399 for order_line in order.order_line:
400 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
404 def action_cancel(self, cr, uid, ids, context=None):
405 wf_service = netsvc.LocalService("workflow")
406 for purchase in self.browse(cr, uid, ids, context=context):
407 for pick in purchase.picking_ids:
408 if pick.state not in ('draft','cancel'):
409 raise osv.except_osv(
410 _('Unable to cancel this purchase order!'),
411 _('You must first cancel all receptions related to this purchase order.'))
412 for pick in purchase.picking_ids:
413 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
414 for inv in purchase.invoice_ids:
415 if inv and inv.state not in ('cancel','draft'):
416 raise osv.except_osv(
417 _('Unable to cancel this purchase order!'),
418 _('You must first cancel all invoices related to this purchase order.'))
420 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
421 self.write(cr,uid,ids,{'state':'cancel'})
423 for (id, name) in self.name_get(cr, uid, ids):
424 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
425 message = _("Purchase order '%s' is cancelled.") % name
426 self.log(cr, uid, id, message)
429 def _prepare_order_picking(self, cr, uid, order, context=None):
431 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
432 'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
433 'date': order.date_order,
435 'address_id': order.dest_address_id.id or order.partner_address_id.id,
436 'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
437 'purchase_id': order.id,
438 'company_id': order.company_id.id,
442 def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, context=None):
444 'name': order.name + ': ' + (order_line.name or ''),
445 'product_id': order_line.product_id.id,
446 'product_qty': order_line.product_qty,
447 'product_uos_qty': order_line.product_qty,
448 'product_uom': order_line.product_uom.id,
449 'product_uos': order_line.product_uom.id,
450 'date': order_line.date_planned,
451 'date_expected': order_line.date_planned,
452 'location_id': order.partner_id.property_stock_supplier.id,
453 'location_dest_id': order.location_id.id,
454 'picking_id': picking_id,
455 'address_id': order.dest_address_id.id or order.partner_address_id.id,
456 'move_dest_id': order_line.move_dest_id.id,
458 'purchase_line_id': order_line.id,
459 'company_id': order.company_id.id,
460 'price_unit': order_line.price_unit
463 def _create_pickings(self, cr, uid, order, order_lines, picking_id=False, context=None):
464 """Creates pickings and appropriate stock moves for given order lines, then
465 confirms the moves, makes them available, and confirms the picking.
467 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
468 a standard outgoing picking will be created to wrap the stock moves, as returned
469 by :meth:`~._prepare_order_picking`.
471 Modules that wish to customize the procurements or partition the stock moves over
472 multiple stock pickings may override this method and call ``super()`` with
473 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
475 :param browse_record order: purchase order to which the order lines belong
476 :param list(browse_record) order_lines: purchase order line records for which picking
477 and moves should be created.
478 :param int picking_id: optional ID of a stock picking to which the created stock moves
479 will be added. A new picking will be created if omitted.
480 :return: list of IDs of pickings used/created for the given order lines (usually just one)
483 picking_id = self.pool.get('stock.picking').create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
485 stock_move = self.pool.get('stock.move')
486 wf_service = netsvc.LocalService("workflow")
487 for order_line in order_lines:
488 if not order_line.product_id:
490 if order_line.product_id.type in ('product', 'consu'):
491 move = stock_move.create(cr, uid, self._prepare_order_line_move(cr, uid, order, order_line, picking_id, context=context))
492 if order_line.move_dest_id:
493 order_line.move_dest_id.write({'location_id': order.location_id.id})
494 todo_moves.append(move)
495 stock_move.action_confirm(cr, uid, todo_moves)
496 stock_move.force_assign(cr, uid, todo_moves)
497 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
500 def action_picking_create(self,cr, uid, ids, context=None):
502 for order in self.browse(cr, uid, ids):
503 picking_ids.extend(self._create_pickings(cr, uid, order, order.order_line, None, context=context))
505 # Must return one unique picking ID: the one to connect in the subflow of the purchase order.
506 # In case of multiple (split) pickings, we should return the ID of the critical one, i.e. the
507 # one that should trigger the advancement of the purchase workflow.
508 # By default we will consider the first one as most important, but this behavior can be overridden.
509 return picking_ids[0] if picking_ids else False
511 def copy(self, cr, uid, id, default=None, context=None):
520 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
522 return super(purchase_order, self).copy(cr, uid, id, default, context)
524 def do_merge(self, cr, uid, ids, context=None):
526 To merge similar type of purchase orders.
527 Orders will only be merged if:
528 * Purchase Orders are in draft
529 * Purchase Orders belong to the same partner
530 * Purchase Orders are have same stock location, same pricelist
531 Lines will only be merged if:
532 * Order lines are exactly the same except for the quantity and unit
534 @param self: The object pointer.
535 @param cr: A database cursor
536 @param uid: ID of the user currently logged in
537 @param ids: the ID or list of IDs
538 @param context: A standard dictionary
540 @return: new purchase order id
543 #TOFIX: merged order line should be unlink
544 wf_service = netsvc.LocalService("workflow")
545 def make_key(br, fields):
548 field_val = getattr(br, field)
549 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
552 if isinstance(field_val, browse_record):
553 field_val = field_val.id
554 elif isinstance(field_val, browse_null):
556 elif isinstance(field_val, list):
557 field_val = ((6, 0, tuple([v.id for v in field_val])),)
558 list_key.append((field, field_val))
560 return tuple(list_key)
562 # compute what the new orders should contain
566 for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
567 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
568 new_order = new_orders.setdefault(order_key, ({}, []))
569 new_order[1].append(porder.id)
570 order_infos = new_order[0]
573 'origin': porder.origin,
574 'date_order': porder.date_order,
575 'partner_id': porder.partner_id.id,
576 'partner_address_id': porder.partner_address_id.id,
577 'dest_address_id': porder.dest_address_id.id,
578 'warehouse_id': porder.warehouse_id.id,
579 'location_id': porder.location_id.id,
580 'pricelist_id': porder.pricelist_id.id,
583 'notes': '%s' % (porder.notes or '',),
584 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
587 if porder.date_order < order_infos['date_order']:
588 order_infos['date_order'] = porder.date_order
590 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
592 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
594 for order_line in porder.order_line:
595 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
596 o_line = order_infos['order_line'].setdefault(line_key, {})
598 # merge the line with an existing line
599 o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
601 # append a new "standalone" line
602 for field in ('product_qty', 'product_uom'):
603 field_val = getattr(order_line, field)
604 if isinstance(field_val, browse_record):
605 field_val = field_val.id
606 o_line[field] = field_val
607 o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
613 for order_key, (order_data, old_ids) in new_orders.iteritems():
614 # skip merges with only one order
616 allorders += (old_ids or [])
619 # cleanup order line data
620 for key, value in order_data['order_line'].iteritems():
621 del value['uom_factor']
622 value.update(dict(key))
623 order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
625 # create the new order
626 neworder_id = self.create(cr, uid, order_data)
627 orders_info.update({neworder_id: old_ids})
628 allorders.append(neworder_id)
630 # make triggers pointing to the old orders point to the new order
631 for old_id in old_ids:
632 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
633 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
638 class purchase_order_line(osv.osv):
639 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
641 cur_obj=self.pool.get('res.currency')
642 tax_obj = self.pool.get('account.tax')
643 for line in self.browse(cr, uid, ids, context=context):
644 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
645 cur = line.order_id.pricelist_id.currency_id
646 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
649 def _get_uom_id(self, cr, uid, context=None):
651 proxy = self.pool.get('ir.model.data')
652 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
654 except Exception, ex:
658 'name': fields.char('Description', size=256, required=True),
659 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product UoM'), required=True),
660 'date_planned': fields.date('Scheduled Date', required=True, select=True),
661 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
662 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
663 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
664 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
665 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
666 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
667 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
668 'notes': fields.text('Notes'),
669 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
670 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
671 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
672 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
673 help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
674 \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
675 \n* The \'Done\' state is set automatically when purchase order is set as done. \
676 \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
677 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
678 'invoiced': fields.boolean('Invoiced', readonly=True),
679 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
680 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
684 'product_uom' : _get_uom_id,
685 'product_qty': lambda *a: 1.0,
686 'state': lambda *args: 'draft',
687 'invoiced': lambda *a: 0,
689 _table = 'purchase_order_line'
690 _name = 'purchase.order.line'
691 _description = 'Purchase Order Line'
693 def copy_data(self, cr, uid, id, default=None, context=None):
696 default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
697 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
700 # - name of method should "onchange_product_id"
702 # - merge 'product_uom_change' method
703 # - split into small internal methods for clearity
704 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
705 partner_id, date_order=False, fiscal_position=False, date_planned=False,
706 name=False, price_unit=False, notes=False, context={}):
708 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.'))
710 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
712 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
713 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
715 prod= self.pool.get('product.product').browse(cr, uid, product)
716 product_uom_pool = self.pool.get('product.uom')
719 lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
720 context={'lang':lang}
721 context['partner_id'] = partner_id
723 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
724 prod_uom_po = prod.uom_po_id.id
728 date_order = time.strftime('%Y-%m-%d')
732 uom1_cat = prod.uom_id.category_id.id
733 uom2_cat = product_uom_pool.browse(cr, uid, uom).category_id.id
734 if uom1_cat != uom2_cat:
737 prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id], context=context)[0][1]
739 for s in prod.seller_ids:
740 if s.name.id == partner_id:
741 seller_delay = s.delay
743 temp_qty = product_uom_pool._compute_qty(cr, uid, s.product_uom.id, s.min_qty, to_uom_id=prod.uom_id.id)
744 uom = s.product_uom.id #prod_uom_po
745 temp_qty = s.min_qty # supplier _qty assigned to temp
746 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
748 res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier has a minimal quantity set to %s, you should not purchase less.') % qty}})
749 qty_in_product_uom = product_uom_pool._compute_qty(cr, uid, uom, qty, to_uom_id=prod.uom_id.id)
750 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
751 product, qty_in_product_uom or 1.0, partner_id, {
755 dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
758 res.update({'value': {'price_unit': price, 'name': prod_name,
759 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
760 'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
762 'product_uom': prod.uom_id.id}})
765 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
766 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
767 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
768 res2 = self.pool.get('product.uom').read(cr, uid, [prod.uom_id.id], ['category_id'])
769 res3 = prod.uom_id.category_id.id
770 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
771 if res2[0]['category_id'][0] != res3:
772 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'))
774 res['domain'] = domain
778 # - merge into 'product_id_change' method
779 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
780 partner_id, date_order=False, fiscal_position=False, date_planned=False,
781 name=False, price_unit=False, notes=False, context={}):
782 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
783 partner_id, date_order=date_order, fiscal_position=fiscal_position, date_planned=date_planned,
784 name=name, price_unit=price_unit, notes=notes, context=context)
785 if 'product_uom' in res['value']:
786 if uom and (uom != res['value']['product_uom']) and res['value']['product_uom']:
787 seller_uom_name = self.pool.get('product.uom').read(cr, uid, [res['value']['product_uom']], ['name'])[0]['name']
788 res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier only sells this product by %s') % seller_uom_name }})
789 del res['value']['product_uom']
791 res['value']['price_unit'] = 0.0
794 def action_confirm(self, cr, uid, ids, context=None):
795 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
798 purchase_order_line()
800 class procurement_order(osv.osv):
801 _inherit = 'procurement.order'
803 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
806 def action_po_assign(self, cr, uid, ids, context=None):
807 """ This is action which call from workflow to assign purchase order to procurements
810 res = self.make_po(cr, uid, ids, context=context)
812 return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
814 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
815 """Create the purchase order from the procurement, using
816 the provided field values, after adding the given purchase
817 order line in the purchase order.
819 :params procurement: the procurement object generating the purchase order
820 :params dict po_vals: field values for the new purchase order (the
821 ``order_line`` field will be overwritten with one
822 single line, as passed in ``line_vals``).
823 :params dict line_vals: field values of the single purchase order line that
824 the purchase order will contain.
825 :return: id of the newly created purchase order
828 po_vals.update({'order_line': [(0,0,line_vals)]})
829 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
831 def _get_schedule_date(self, cr, uid, procurement, company, context=None):
832 procurement_date_planned = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
833 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
836 def _get_order_dates(self, cr, uid, schedule_date, seller_delay, context=None):
837 order_dates = schedule_date - relativedelta(days=seller_delay)
840 def make_po(self, cr, uid, ids, context=None):
841 """ Make purchase order from procurement
842 @return: New created Purchase Orders procurement wise
847 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
848 partner_obj = self.pool.get('res.partner')
849 uom_obj = self.pool.get('product.uom')
850 pricelist_obj = self.pool.get('product.pricelist')
851 prod_obj = self.pool.get('product.product')
852 acc_pos_obj = self.pool.get('account.fiscal.position')
853 seq_obj = self.pool.get('ir.sequence')
854 warehouse_obj = self.pool.get('stock.warehouse')
855 for procurement in self.browse(cr, uid, ids, context=context):
856 res_id = procurement.move_id.id
857 partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
858 seller_qty = procurement.product_id.seller_qty
859 seller_delay = int(procurement.product_id.seller_delay)
860 partner_id = partner.id
861 address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
862 pricelist_id = partner.property_product_pricelist_purchase.id
863 warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
864 uom_id = procurement.product_id.uom_po_id.id
866 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
868 qty = max(qty,seller_qty)
870 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
872 schedule_date = self._get_schedule_date(cr, uid, procurement, company, context=context)
873 order_dates = self._get_order_dates(cr, uid, schedule_date, seller_delay, context=context)
875 #Passing partner_id to context for purchase order line integrity of Line name
876 context.update({'lang': partner.lang, 'partner_id': partner_id})
878 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
879 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
880 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
883 'name': product.partner_ref,
885 'product_id': procurement.product_id.id,
886 'product_uom': uom_id,
887 'price_unit': price or 0.0,
888 'date_planned': schedule_date.strftime('%Y-%m-%d %H:%M:%S'),
889 'move_dest_id': res_id,
890 'notes': product.description_purchase,
891 'taxes_id': [(6,0,taxes)],
893 name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
896 'origin': procurement.origin,
897 'partner_id': partner_id,
898 'partner_address_id': address_id,
899 'location_id': procurement.location_id.id,
900 'warehouse_id': warehouse_id and warehouse_id[0] or False,
901 'pricelist_id': pricelist_id,
902 'date_order': order_dates.strftime('%Y-%m-%d %H:%M:%S'),
903 'company_id': procurement.company_id.id,
904 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
906 res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=context)
907 self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]})
911 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: