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
32 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
37 class purchase_order(osv.osv):
39 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
41 cur_obj=self.pool.get('res.currency')
42 for order in self.browse(cr, uid, ids, context=context):
44 'amount_untaxed': 0.0,
49 cur = order.pricelist_id.currency_id
50 for line in order.order_line:
51 val1 += line.price_subtotal
52 for c in self.pool.get('account.tax').compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id.id, order.partner_id)['taxes']:
53 val += c.get('amount', 0.0)
54 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
55 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
56 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
59 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
60 if not value: return False
61 if type(ids)!=type([]):
63 for po in self.browse(cr, uid, ids, context=context):
65 cr.execute("""update purchase_order_line set
69 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
70 cr.execute("""update purchase_order set
71 minimum_planned_date=%s where id=%s""", (value, po.id))
74 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
76 purchase_obj=self.browse(cr, uid, ids, context=context)
77 for purchase in purchase_obj:
78 res[purchase.id] = False
79 if purchase.order_line:
80 min_date=purchase.order_line[0].date_planned
81 for line in purchase.order_line:
82 if line.date_planned < min_date:
83 min_date=line.date_planned
84 res[purchase.id]=min_date
88 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
90 for purchase in self.browse(cursor, user, ids, context=context):
92 for invoice in purchase.invoice_ids:
93 if invoice.state not in ('draft','cancel'):
94 tot += invoice.amount_untaxed
95 if purchase.amount_untaxed:
96 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
98 res[purchase.id] = 0.0
101 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
102 if not ids: return {}
107 p.purchase_id,sum(m.product_qty), m.state
111 stock_picking p on (p.id=m.picking_id)
113 p.purchase_id IN %s GROUP BY m.state, p.purchase_id''',(tuple(ids),))
114 for oid,nbr,state in cr.fetchall():
118 res[oid][0] += nbr or 0.0
119 res[oid][1] += nbr or 0.0
121 res[oid][1] += nbr or 0.0
126 res[r] = 100.0 * res[r][0] / res[r][1]
129 def _get_order(self, cr, uid, ids, context=None):
131 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
132 result[line.order_id.id] = True
135 def _invoiced(self, cursor, user, ids, name, arg, context=None):
137 for purchase in self.browse(cursor, user, ids, context=context):
139 if purchase.invoiced_rate == 100.00:
141 res[purchase.id] = invoiced
145 ('draft', 'Draft PO'),
147 ('sent', 'RFQ Sent'),
148 ('confirmed', 'Waiting Approval'),
149 ('approved', 'Purchase Order'),
150 ('except_picking', 'Shipping Exception'),
151 ('except_invoice', 'Invoice Exception'),
153 ('cancel', 'Cancelled')
157 '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"),
158 'origin': fields.char('Source Document', size=64,
159 help="Reference of the document that generated this purchase order request."
161 'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, size=64),
162 '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."),
163 'date_approve':fields.date('Date Approved', readonly=1, select=True, help="Date on which purchase order has been approved"),
164 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, change_default=True),
165 'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
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, 'Status', readonly=True, help="The state of the purchase order or the quotation request. A quotation is a purchase order in a 'Draft' state. Then the order has to be confirmed by the user, the state switch to 'Confirmed'. Then the supplier must confirm the order to change the state to 'Approved'. When the purchase order is paid and received, the state becomes 'Done'. If a cancel action occurs in the invoice or in the reception of goods, the state becomes in exception.", select=True),
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('Terms and Conditions'),
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.in', 'purchase_id', 'Picking List', readonly=True, help="This is the list of incomming shipments that have been generated for this purchase order."),
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='Invoice Received', 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','Bases on incoming shipments')], '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 "Bases on incoming shipments: 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': fields.date.context_today,
213 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
215 'invoice_method': 'order',
217 '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,
218 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
221 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
223 _name = "purchase.order"
224 _inherit = ['ir.needaction_mixin', 'mail.thread']
225 _description = "Purchase Order"
228 def create(self, cr, uid, vals, context=None):
229 order = super(purchase_order, self).create(cr, uid, vals, context=context)
231 self.create_send_note(cr, uid, [order], context=context)
234 def unlink(self, cr, uid, ids, context=None):
235 purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
237 for s in purchase_orders:
238 if s['state'] in ['draft','cancel']:
239 unlink_ids.append(s['id'])
241 raise osv.except_osv(_('Invalid action !'), _('In order to delete a purchase order, it must be cancelled first!'))
243 # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
244 # automatically sending subflow.delete upon deletion
245 wf_service = netsvc.LocalService("workflow")
246 for id in unlink_ids:
247 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
249 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
251 def button_dummy(self, cr, uid, ids, context=None):
254 def onchange_dest_address_id(self, cr, uid, ids, address_id):
257 address = self.pool.get('res.partner')
258 values = {'warehouse_id': False}
259 supplier = address.browse(cr, uid, address_id)
261 location_id = supplier.property_stock_customer.id
262 values.update({'location_id': location_id})
263 return {'value':values}
265 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
268 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id)
269 return {'value':{'location_id': warehouse.lot_input_id.id, 'dest_address_id': False}}
271 def onchange_partner_id(self, cr, uid, ids, partner_id):
272 partner = self.pool.get('res.partner')
274 return {'value':{'fiscal_position': False}}
275 supplier_address = partner.address_get(cr, uid, [partner_id], ['default'])
276 supplier = partner.browse(cr, uid, partner_id)
277 pricelist = supplier.property_product_pricelist_purchase.id
278 fiscal_position = supplier.property_account_position and supplier.property_account_position.id or False
279 return {'value':{'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
281 def view_invoice(self, cr, uid, ids, context=None):
283 This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
285 mod_obj = self.pool.get('ir.model.data')
286 wizard_obj = self.pool.get('purchase.order.line_invoice')
287 #compute the number of invoices to display
289 for po in self.browse(cr, uid, ids, context=context):
290 if po.invoice_method == 'manual':
291 if not po.invoice_ids:
292 context.update({'active_ids' : [line.id for line in po.order_line]})
293 wizard_obj.makeInvoices(cr, uid, [], context=context)
295 for po in self.browse(cr, uid, ids, context=context):
296 inv_ids+= [invoice.id for invoice in po.invoice_ids]
297 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
298 res_id = res and res[1] or False
301 'name': _('Supplier Invoices'),
305 'res_model': 'account.invoice',
306 'context': "{'type':'in_invoice', 'journal_type': 'purchase'}",
307 'type': 'ir.actions.act_window',
310 'res_id': inv_ids and inv_ids[0] or False,
313 def view_picking(self, cr, uid, ids, context=None):
315 This function returns an action that display existing pîcking orders of given purchase order ids.
317 mod_obj = self.pool.get('ir.model.data')
319 for po in self.browse(cr, uid, ids, context=context):
320 pick_ids += [picking.id for picking in po.picking_ids]
322 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_in_form')
323 res_id = res and res[1] or False
326 'name': _('Receptions'),
330 'res_model': 'stock.picking',
331 'context': "{'contact_display': 'partner'}",
332 'type': 'ir.actions.act_window',
335 'res_id': pick_ids and pick_ids[0] or False,
338 def wkf_approve_order(self, cr, uid, ids, context=None):
339 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)})
342 def wkf_send_rfq(self, cr, uid, ids, context=None):
344 This function opens a window to compose an email, with the edi purchase template message loaded by default
346 mod_obj = self.pool.get('ir.model.data')
347 template = mod_obj.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')
348 template_id = template and template[1] or False
349 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
350 res_id = res and res[1] or False
351 ctx = dict(context, active_model='purchase.order', active_id=ids[0])
352 ctx.update({'mail.compose.template_id': template_id})
356 'res_model': 'mail.compose.message',
357 'views': [(res_id,'form')],
359 'type': 'ir.actions.act_window',
365 #TODO: implement messages system
366 def wkf_confirm_order(self, cr, uid, ids, context=None):
368 for po in self.browse(cr, uid, ids, context=context):
369 if not po.order_line:
370 raise osv.except_osv(_('Error !'),_('You cannot confirm a purchase order without any lines.'))
371 for line in po.order_line:
372 if line.state=='draft':
374 # current_name = self.name_get(cr, uid, ids)[0][1]
375 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
377 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
378 self.confirm_send_note(cr, uid, ids, context)
381 def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
382 """Collects require data from purchase order line that is used to create invoice line
383 for that purchase order line
384 :param account_id: Expense account of the product of PO line if any.
385 :param browse_record order_line: Purchase order line browse record
386 :return: Value for fields of invoice lines.
390 'name': order_line.name,
391 'account_id': account_id,
392 'price_unit': order_line.price_unit or 0.0,
393 'quantity': order_line.product_qty,
394 'product_id': order_line.product_id.id or False,
395 'uos_id': order_line.product_uom.id or False,
396 'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
397 'account_analytic_id': order_line.account_analytic_id.id or False,
400 def action_cancel_draft(self, cr, uid, ids, context=None):
403 self.write(cr, uid, ids, {'state':'draft','shipped':0})
404 wf_service = netsvc.LocalService("workflow")
406 # Deleting the existing instance of workflow for PO
407 wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
408 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
409 self.draft_send_note(cr, uid, ids, context=context)
412 def action_invoice_create(self, cr, uid, ids, context=None):
413 """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
414 :param ids: list of ids of purchase orders.
415 :return: ID of created invoice.
420 journal_obj = self.pool.get('account.journal')
421 inv_obj = self.pool.get('account.invoice')
422 inv_line_obj = self.pool.get('account.invoice.line')
423 fiscal_obj = self.pool.get('account.fiscal.position')
424 property_obj = self.pool.get('ir.property')
426 for order in self.browse(cr, uid, ids, context=context):
427 pay_acc_id = order.partner_id.property_account_payable.id
428 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', order.company_id.id)], limit=1)
430 raise osv.except_osv(_('Error !'),
431 _('There is no purchase journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
433 # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
435 for po_line in order.order_line:
436 if po_line.product_id:
437 acc_id = po_line.product_id.product_tmpl_id.property_account_expense.id
439 acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
441 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,))
443 acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category').id
444 fpos = order.fiscal_position or False
445 acc_id = fiscal_obj.map_account(cr, uid, fpos, acc_id)
447 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
448 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
449 inv_lines.append(inv_line_id)
451 po_line.write({'invoiced':True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
453 # get invoice data and create invoice
455 'name': order.partner_ref or order.name,
456 'reference': order.partner_ref or order.name,
457 'account_id': pay_acc_id,
458 'type': 'in_invoice',
459 'partner_id': order.partner_id.id,
460 'currency_id': order.pricelist_id.currency_id.id,
461 'journal_id': len(journal_ids) and journal_ids[0] or False,
462 'invoice_line': [(6, 0, inv_lines)],
463 'origin': order.name,
464 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
465 'payment_term': order.partner_id.property_payment_term and order.partner_id.property_payment_term.id or False,
466 'company_id': order.company_id.id,
468 inv_id = inv_obj.create(cr, uid, inv_data, context=context)
470 # compute the invoice
471 inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
473 # Link this new invoice to related purchase order
474 order.write({'invoice_ids': [(4, inv_id)]}, context=context)
477 self.invoice_send_note(cr, uid, ids, res, context)
480 def invoice_done(self, cr, uid, ids, context=None):
481 self.write(cr, uid, ids, {'state':'approved'}, context=context)
482 self.invoice_done_send_note(cr, uid, ids, context=context)
485 def has_stockable_product(self,cr, uid, ids, *args):
486 for order in self.browse(cr, uid, ids):
487 for order_line in order.order_line:
488 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
492 def action_cancel(self, cr, uid, ids, context=None):
493 wf_service = netsvc.LocalService("workflow")
494 for purchase in self.browse(cr, uid, ids, context=context):
495 for pick in purchase.picking_ids:
496 if pick.state not in ('draft','cancel'):
497 raise osv.except_osv(
498 _('Unable to cancel this purchase order!'),
499 _('You must first cancel all receptions related to this purchase order.'))
500 for pick in purchase.picking_ids:
501 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
502 for inv in purchase.invoice_ids:
503 if inv and inv.state not in ('cancel','draft'):
504 raise osv.except_osv(
505 _('Unable to cancel this purchase order!'),
506 _('You must first cancel all invoices related to this purchase order.'))
508 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
509 self.write(cr,uid,ids,{'state':'cancel'})
511 for (id, name) in self.name_get(cr, uid, ids):
512 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
513 self.cancel_send_note(cr, uid, ids, context)
516 def _prepare_order_picking(self, cr, uid, order, context=None):
518 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
519 'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
520 'date': order.date_order,
521 'partner_id': order.dest_address_id.id or order.partner_id.id,
522 'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
524 'partner_id': order.dest_address_id.id or order.partner_id.id,
525 'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
526 'purchase_id': order.id,
527 'company_id': order.company_id.id,
531 def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, context=None):
533 'name': order.name + ': ' + (order_line.name or ''),
534 'product_id': order_line.product_id.id,
535 'product_qty': order_line.product_qty,
536 'product_uos_qty': order_line.product_qty,
537 'product_uom': order_line.product_uom.id,
538 'product_uos': order_line.product_uom.id,
539 'date': order_line.date_planned,
540 'date_expected': order_line.date_planned,
541 'location_id': order.partner_id.property_stock_supplier.id,
542 'location_dest_id': order.location_id.id,
543 'picking_id': picking_id,
544 'partner_id': order.dest_address_id.id or order.partner_id.id,
545 'move_dest_id': order_line.move_dest_id.id,
547 'purchase_line_id': order_line.id,
548 'company_id': order.company_id.id,
549 'price_unit': order_line.price_unit
552 def _create_pickings(self, cr, uid, order, order_lines, picking_id=False, context=None):
553 """Creates pickings and appropriate stock moves for given order lines, then
554 confirms the moves, makes them available, and confirms the picking.
556 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
557 a standard outgoing picking will be created to wrap the stock moves, as returned
558 by :meth:`~._prepare_order_picking`.
560 Modules that wish to customize the procurements or partition the stock moves over
561 multiple stock pickings may override this method and call ``super()`` with
562 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
564 :param browse_record order: purchase order to which the order lines belong
565 :param list(browse_record) order_lines: purchase order line records for which picking
566 and moves should be created.
567 :param int picking_id: optional ID of a stock picking to which the created stock moves
568 will be added. A new picking will be created if omitted.
569 :return: list of IDs of pickings used/created for the given order lines (usually just one)
572 picking_id = self.pool.get('stock.picking').create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
574 stock_move = self.pool.get('stock.move')
575 wf_service = netsvc.LocalService("workflow")
576 for order_line in order_lines:
577 if not order_line.product_id:
579 if order_line.product_id.type in ('product', 'consu'):
580 move = stock_move.create(cr, uid, self._prepare_order_line_move(cr, uid, order, order_line, picking_id, context=context))
581 if order_line.move_dest_id:
582 order_line.move_dest_id.write({'location_id': order.location_id.id})
583 todo_moves.append(move)
584 stock_move.action_confirm(cr, uid, todo_moves)
585 stock_move.force_assign(cr, uid, todo_moves)
586 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
589 def action_picking_create(self,cr, uid, ids, context=None):
591 for order in self.browse(cr, uid, ids):
592 picking_ids.extend(self._create_pickings(cr, uid, order, order.order_line, None, context=context))
594 # Must return one unique picking ID: the one to connect in the subflow of the purchase order.
595 # In case of multiple (split) pickings, we should return the ID of the critical one, i.e. the
596 # one that should trigger the advancement of the purchase workflow.
597 # By default we will consider the first one as most important, but this behavior can be overridden.
599 self.shipment_send_note(cr, uid, ids, picking_ids[0], context=context)
600 return picking_ids[0] if picking_ids else False
602 def picking_done(self, cr, uid, ids, context=None):
603 self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
604 self.shipment_done_send_note(cr, uid, ids, context=context)
607 def copy(self, cr, uid, id, default=None, context=None):
616 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
618 return super(purchase_order, self).copy(cr, uid, id, default, context)
620 def do_merge(self, cr, uid, ids, context=None):
622 To merge similar type of purchase orders.
623 Orders will only be merged if:
624 * Purchase Orders are in draft
625 * Purchase Orders belong to the same partner
626 * Purchase Orders are have same stock location, same pricelist
627 Lines will only be merged if:
628 * Order lines are exactly the same except for the quantity and unit
630 @param self: The object pointer.
631 @param cr: A database cursor
632 @param uid: ID of the user currently logged in
633 @param ids: the ID or list of IDs
634 @param context: A standard dictionary
636 @return: new purchase order id
639 #TOFIX: merged order line should be unlink
640 wf_service = netsvc.LocalService("workflow")
641 def make_key(br, fields):
644 field_val = getattr(br, field)
645 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
648 if isinstance(field_val, browse_record):
649 field_val = field_val.id
650 elif isinstance(field_val, browse_null):
652 elif isinstance(field_val, list):
653 field_val = ((6, 0, tuple([v.id for v in field_val])),)
654 list_key.append((field, field_val))
656 return tuple(list_key)
658 # compute what the new orders should contain
662 for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
663 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
664 new_order = new_orders.setdefault(order_key, ({}, []))
665 new_order[1].append(porder.id)
666 order_infos = new_order[0]
669 'origin': porder.origin,
670 'date_order': porder.date_order,
671 'partner_id': porder.partner_id.id,
672 'dest_address_id': porder.dest_address_id.id,
673 'warehouse_id': porder.warehouse_id.id,
674 'location_id': porder.location_id.id,
675 'pricelist_id': porder.pricelist_id.id,
678 'notes': '%s' % (porder.notes or '',),
679 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
682 if porder.date_order < order_infos['date_order']:
683 order_infos['date_order'] = porder.date_order
685 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
687 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
689 for order_line in porder.order_line:
690 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
691 o_line = order_infos['order_line'].setdefault(line_key, {})
693 # merge the line with an existing line
694 o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
696 # append a new "standalone" line
697 for field in ('product_qty', 'product_uom'):
698 field_val = getattr(order_line, field)
699 if isinstance(field_val, browse_record):
700 field_val = field_val.id
701 o_line[field] = field_val
702 o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
708 for order_key, (order_data, old_ids) in new_orders.iteritems():
709 # skip merges with only one order
711 allorders += (old_ids or [])
714 # cleanup order line data
715 for key, value in order_data['order_line'].iteritems():
716 del value['uom_factor']
717 value.update(dict(key))
718 order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
720 # create the new order
721 neworder_id = self.create(cr, uid, order_data)
722 orders_info.update({neworder_id: old_ids})
723 allorders.append(neworder_id)
725 # make triggers pointing to the old orders point to the new order
726 for old_id in old_ids:
727 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
728 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
731 # --------------------------------------
732 # OpenChatter methods and notifications
733 # --------------------------------------
735 def get_needaction_user_ids(self, cr, uid, ids, context=None):
736 result = super(purchase_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
737 for obj in self.browse(cr, uid, ids, context=context):
738 if obj.state == 'approved':
739 result[obj.id].append(obj.validator.id)
742 def create_send_note(self, cr, uid, ids, context=None):
743 return self.message_append_note(cr, uid, ids, body=_("Request for quotation <b>created</b>."), context=context)
745 def confirm_send_note(self, cr, uid, ids, context=None):
746 for obj in self.browse(cr, uid, ids, context=context):
747 self.message_subscribe(cr, uid, [obj.id], [obj.validator.id], context=context)
748 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to a Purchase Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
750 def shipment_send_note(self, cr, uid, ids, picking_id, context=None):
751 for order in self.browse(cr, uid, ids, context=context):
752 for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
753 # convert datetime field to a datetime, using server format, then
754 # convert it to the user TZ and re-render it with %Z to add the timezone
755 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
756 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
757 self.message_append_note(cr, uid, [order.id], body=_("Shipment <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
759 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
760 for order in self.browse(cr, uid, ids, context=context):
761 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
762 self.message_append_note(cr, uid, [order.id], body=_("Draft Invoice of %s %s is <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
764 def shipment_done_send_note(self, cr, uid, ids, context=None):
765 self.message_append_note(cr, uid, ids, body=_("""Shipment <b>received</b>."""), context=context)
767 def invoice_done_send_note(self, cr, uid, ids, context=None):
768 self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
770 def draft_send_note(self, cr, uid, ids, context=None):
771 return self.message_append_note(cr, uid, ids, body=_("Purchase Order has been set to <b>draft</b>."), context=context)
773 def cancel_send_note(self, cr, uid, ids, context=None):
774 for obj in self.browse(cr, uid, ids, context=context):
775 self.message_append_note(cr, uid, [obj.id], body=_("Purchase Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
779 class purchase_order_line(osv.osv):
780 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
782 cur_obj=self.pool.get('res.currency')
783 tax_obj = self.pool.get('account.tax')
784 for line in self.browse(cr, uid, ids, context=context):
785 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
786 cur = line.order_id.pricelist_id.currency_id
787 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
790 def _get_uom_id(self, cr, uid, context=None):
792 proxy = self.pool.get('ir.model.data')
793 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
795 except Exception, ex:
799 'name': fields.char('Description', size=256, required=True),
800 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
801 'date_planned': fields.date('Scheduled Date', required=True, select=True),
802 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
803 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
804 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
805 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
806 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
807 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
808 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
809 'notes': fields.text('Notes'),
810 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
811 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
812 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
813 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
814 help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
815 \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
816 \n* The \'Done\' state is set automatically when purchase order is set as done. \
817 \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
818 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
819 'invoiced': fields.boolean('Invoiced', readonly=True),
820 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
821 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
825 'product_uom' : _get_uom_id,
826 'product_qty': lambda *a: 1.0,
827 'state': lambda *args: 'draft',
828 'invoiced': lambda *a: 0,
830 _table = 'purchase_order_line'
831 _name = 'purchase.order.line'
832 _description = 'Purchase Order Line'
834 def copy_data(self, cr, uid, id, default=None, context=None):
837 default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
838 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
840 def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
841 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
842 name=False, price_unit=False, notes=False, context=None):
844 onchange handler of product_uom.
847 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'notes': notes or'', 'product_uom' : uom_id or False}}
848 return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
849 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
850 name=name, price_unit=price_unit, notes=notes, context=context)
852 def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
853 """Return the datetime value to use as Schedule Date (``date_planned``) for
854 PO Lines that correspond to the given product.supplierinfo,
855 when ordered at `date_order_str`.
857 :param browse_record | False supplier_info: product.supplierinfo, used to
858 determine delivery delay (if False, default delay = 0)
859 :param str date_order_str: date of order, as a string in
860 DEFAULT_SERVER_DATE_FORMAT
862 :return: desired Schedule Date for the PO line
864 supplier_delay = int(supplier_info.delay) if supplier_info else 0
865 return datetime.strptime(date_order_str, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=supplier_delay)
867 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
868 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
869 name=False, price_unit=False, notes=False, context=None):
871 onchange handler of product_id.
876 res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'notes': notes or '', 'product_uom' : uom_id or False}}
880 product_product = self.pool.get('product.product')
881 product_uom = self.pool.get('product.uom')
882 res_partner = self.pool.get('res.partner')
883 product_supplierinfo = self.pool.get('product.supplierinfo')
884 product_pricelist = self.pool.get('product.pricelist')
885 account_fiscal_position = self.pool.get('account.fiscal.position')
886 account_tax = self.pool.get('account.tax')
888 # - check for the presence of partner_id and pricelist_id
890 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.'))
892 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
894 # - determine name and notes based on product in partner lang.
895 lang = res_partner.browse(cr, uid, partner_id).lang
896 context_partner = {'lang': lang, 'partner_id': partner_id}
897 product = product_product.browse(cr, uid, product_id, context=context_partner)
898 res['value'].update({'name': product.partner_ref, 'notes': notes or product.description_purchase})
900 # - set a domain on product_uom
901 res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
903 # - check that uom and product uom belong to the same category
904 product_uom_po_id = product.uom_po_id.id
906 uom_id = product_uom_po_id
908 if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
909 res['warning'] = {'title': _('Warning'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure')}
910 uom_id = product_uom_po_id
912 res['value'].update({'product_uom': uom_id})
914 # - determine product_qty and date_planned based on seller info
916 date_order = fields.date.context_today(self,cr,uid,context=context)
920 for supplier in product.seller_ids:
921 if supplier.name.id == partner_id:
922 supplierinfo = supplier
923 if supplierinfo.product_uom.id != uom_id:
924 res['warning'] = {'title': _('Warning'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
925 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
926 if qty < min_qty: # If the supplier quantity is greater than entered from user, set minimal.
927 res['warning'] = {'title': _('Warning'), 'message': _('The selected supplier has a minimal quantity set to %s %s, you should not purchase less.') % (supplierinfo.min_qty, supplierinfo.product_uom.name)}
930 dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
932 res['value'].update({'date_planned': date_planned or dt, 'product_qty': qty})
934 # - determine price_unit and taxes_id
935 price = product_pricelist.price_get(cr, uid, [pricelist_id],
936 product.id, qty or 1.0, partner_id, {'uom': uom_id, 'date': date_order})[pricelist_id]
938 taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
939 fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
940 taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
941 res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
945 product_id_change = onchange_product_id
946 product_uom_change = onchange_product_uom
948 def action_confirm(self, cr, uid, ids, context=None):
949 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
952 purchase_order_line()
954 class procurement_order(osv.osv):
955 _inherit = 'procurement.order'
957 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
960 def action_po_assign(self, cr, uid, ids, context=None):
961 """ This is action which call from workflow to assign purchase order to procurements
964 res = self.make_po(cr, uid, ids, context=context)
966 return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
968 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
969 """Create the purchase order from the procurement, using
970 the provided field values, after adding the given purchase
971 order line in the purchase order.
973 :params procurement: the procurement object generating the purchase order
974 :params dict po_vals: field values for the new purchase order (the
975 ``order_line`` field will be overwritten with one
976 single line, as passed in ``line_vals``).
977 :params dict line_vals: field values of the single purchase order line that
978 the purchase order will contain.
979 :return: id of the newly created purchase order
982 po_vals.update({'order_line': [(0,0,line_vals)]})
983 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
985 def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
986 """Return the datetime value to use as Schedule Date (``date_planned``) for the
987 Purchase Order Lines created to satisfy the given procurement.
989 :param browse_record procurement: the procurement for which a PO will be created.
990 :param browse_report company: the company to which the new PO will belong to.
992 :return: the desired Schedule Date for the PO lines
994 procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
995 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
998 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
999 """Return the datetime value to use as Order Date (``date_order``) for the
1000 Purchase Order created to satisfy the given procurement.
1002 :param browse_record procurement: the procurement for which a PO will be created.
1003 :param browse_report company: the company to which the new PO will belong to.
1004 :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1006 :return: the desired Order Date for the PO
1008 seller_delay = int(procurement.product_id.seller_delay)
1009 return schedule_date - relativedelta(days=seller_delay)
1011 def make_po(self, cr, uid, ids, context=None):
1012 """ Make purchase order from procurement
1013 @return: New created Purchase Orders procurement wise
1018 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1019 partner_obj = self.pool.get('res.partner')
1020 uom_obj = self.pool.get('product.uom')
1021 pricelist_obj = self.pool.get('product.pricelist')
1022 prod_obj = self.pool.get('product.product')
1023 acc_pos_obj = self.pool.get('account.fiscal.position')
1024 seq_obj = self.pool.get('ir.sequence')
1025 warehouse_obj = self.pool.get('stock.warehouse')
1026 for procurement in self.browse(cr, uid, ids, context=context):
1027 res_id = procurement.move_id.id
1028 partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
1029 seller_qty = procurement.product_id.seller_qty
1030 partner_id = partner.id
1031 address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1032 pricelist_id = partner.property_product_pricelist_purchase.id
1033 warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
1034 uom_id = procurement.product_id.uom_po_id.id
1036 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1038 qty = max(qty,seller_qty)
1040 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
1042 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1043 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1045 #Passing partner_id to context for purchase order line integrity of Line name
1046 context.update({'lang': partner.lang, 'partner_id': partner_id})
1048 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
1049 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1050 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1053 'name': product.partner_ref,
1055 'product_id': procurement.product_id.id,
1056 'product_uom': uom_id,
1057 'price_unit': price or 0.0,
1058 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1059 'move_dest_id': res_id,
1060 'notes': product.description_purchase,
1061 'taxes_id': [(6,0,taxes)],
1063 name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1066 'origin': procurement.origin,
1067 'partner_id': partner_id,
1068 'location_id': procurement.location_id.id,
1069 'warehouse_id': warehouse_id and warehouse_id[0] or False,
1070 'pricelist_id': pricelist_id,
1071 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1072 'company_id': procurement.company_id.id,
1073 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1075 res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=context)
1076 self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]})
1077 self.running_send_note(cr, uid, [procurement.id], context=context)
1082 class mail_message(osv.osv):
1083 _name = 'mail.message'
1084 _inherit = 'mail.message'
1086 def _postprocess_sent_message(self, cr, uid, message, context=None):
1087 if message.model == 'purchase.order':
1088 wf_service = netsvc.LocalService("workflow")
1089 wf_service.trg_validate(uid, 'purchase.order', message.res_id, 'send_rfq', cr)
1090 return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1093 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: