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 "Otherwise, keep empty to deliver to your own company."
170 'warehouse_id': fields.many2one('stock.warehouse', 'Destination 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 incoming 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', '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.text('Description', 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 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
810 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
811 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
812 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
813 help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
814 \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
815 \n* The \'Done\' state is set automatically when purchase order is set as done. \
816 \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
817 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
818 'invoiced': fields.boolean('Invoiced', readonly=True),
819 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
820 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
824 'product_uom' : _get_uom_id,
825 'product_qty': lambda *a: 1.0,
826 'state': lambda *args: 'draft',
827 'invoiced': lambda *a: 0,
829 _table = 'purchase_order_line'
830 _name = 'purchase.order.line'
831 _description = 'Purchase Order Line'
833 def copy_data(self, cr, uid, id, default=None, context=None):
836 default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
837 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
839 def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
840 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
841 name=False, price_unit=False, context=None):
843 onchange handler of product_uom.
846 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
847 return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
848 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
849 name=name, price_unit=price_unit, context=context)
851 def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
852 """Return the datetime value to use as Schedule Date (``date_planned``) for
853 PO Lines that correspond to the given product.supplierinfo,
854 when ordered at `date_order_str`.
856 :param browse_record | False supplier_info: product.supplierinfo, used to
857 determine delivery delay (if False, default delay = 0)
858 :param str date_order_str: date of order, as a string in
859 DEFAULT_SERVER_DATE_FORMAT
861 :return: desired Schedule Date for the PO line
863 supplier_delay = int(supplier_info.delay) if supplier_info else 0
864 return datetime.strptime(date_order_str, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=supplier_delay)
866 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
867 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
868 name=False, price_unit=False, context=None):
870 onchange handler of product_id.
875 res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
879 product_product = self.pool.get('product.product')
880 product_uom = self.pool.get('product.uom')
881 res_partner = self.pool.get('res.partner')
882 product_supplierinfo = self.pool.get('product.supplierinfo')
883 product_pricelist = self.pool.get('product.pricelist')
884 account_fiscal_position = self.pool.get('account.fiscal.position')
885 account_tax = self.pool.get('account.tax')
887 # - check for the presence of partner_id and pricelist_id
889 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.'))
891 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
893 # - determine name and notes based on product in partner lang.
894 lang = res_partner.browse(cr, uid, partner_id).lang
895 context_partner = {'lang': lang, 'partner_id': partner_id}
896 product = product_product.browse(cr, uid, product_id, context=context_partner)
898 if product.description_purchase:
899 name += '\n' + product.description_purchase
900 res['value'].update({'name': name})
902 # - set a domain on product_uom
903 res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
905 # - check that uom and product uom belong to the same category
906 product_uom_po_id = product.uom_po_id.id
908 uom_id = product_uom_po_id
910 if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
911 res['warning'] = {'title': _('Warning'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure')}
912 uom_id = product_uom_po_id
914 res['value'].update({'product_uom': uom_id})
916 # - determine product_qty and date_planned based on seller info
918 date_order = fields.date.context_today(self,cr,uid,context=context)
922 for supplier in product.seller_ids:
923 if supplier.name.id == partner_id:
924 supplierinfo = supplier
925 if supplierinfo.product_uom.id != uom_id:
926 res['warning'] = {'title': _('Warning'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
927 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
928 if qty < min_qty: # If the supplier quantity is greater than entered from user, set minimal.
929 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)}
932 dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
934 res['value'].update({'date_planned': date_planned or dt, 'product_qty': qty})
936 # - determine price_unit and taxes_id
937 price = product_pricelist.price_get(cr, uid, [pricelist_id],
938 product.id, qty or 1.0, partner_id, {'uom': uom_id, 'date': date_order})[pricelist_id]
940 taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
941 fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
942 taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
943 res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
947 product_id_change = onchange_product_id
948 product_uom_change = onchange_product_uom
950 def action_confirm(self, cr, uid, ids, context=None):
951 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
954 purchase_order_line()
956 class procurement_order(osv.osv):
957 _inherit = 'procurement.order'
959 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
962 def action_po_assign(self, cr, uid, ids, context=None):
963 """ This is action which call from workflow to assign purchase order to procurements
966 res = self.make_po(cr, uid, ids, context=context)
968 return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
970 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
971 """Create the purchase order from the procurement, using
972 the provided field values, after adding the given purchase
973 order line in the purchase order.
975 :params procurement: the procurement object generating the purchase order
976 :params dict po_vals: field values for the new purchase order (the
977 ``order_line`` field will be overwritten with one
978 single line, as passed in ``line_vals``).
979 :params dict line_vals: field values of the single purchase order line that
980 the purchase order will contain.
981 :return: id of the newly created purchase order
984 po_vals.update({'order_line': [(0,0,line_vals)]})
985 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
987 def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
988 """Return the datetime value to use as Schedule Date (``date_planned``) for the
989 Purchase Order Lines created to satisfy the given procurement.
991 :param browse_record procurement: the procurement for which a PO will be created.
992 :param browse_report company: the company to which the new PO will belong to.
994 :return: the desired Schedule Date for the PO lines
996 procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
997 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
1000 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
1001 """Return the datetime value to use as Order Date (``date_order``) for the
1002 Purchase Order created to satisfy the given procurement.
1004 :param browse_record procurement: the procurement for which a PO will be created.
1005 :param browse_report company: the company to which the new PO will belong to.
1006 :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1008 :return: the desired Order Date for the PO
1010 seller_delay = int(procurement.product_id.seller_delay)
1011 return schedule_date - relativedelta(days=seller_delay)
1013 def make_po(self, cr, uid, ids, context=None):
1014 """ Make purchase order from procurement
1015 @return: New created Purchase Orders procurement wise
1020 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1021 partner_obj = self.pool.get('res.partner')
1022 uom_obj = self.pool.get('product.uom')
1023 pricelist_obj = self.pool.get('product.pricelist')
1024 prod_obj = self.pool.get('product.product')
1025 acc_pos_obj = self.pool.get('account.fiscal.position')
1026 seq_obj = self.pool.get('ir.sequence')
1027 warehouse_obj = self.pool.get('stock.warehouse')
1028 for procurement in self.browse(cr, uid, ids, context=context):
1029 res_id = procurement.move_id.id
1030 partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
1031 seller_qty = procurement.product_id.seller_qty
1032 partner_id = partner.id
1033 address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1034 pricelist_id = partner.property_product_pricelist_purchase.id
1035 warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
1036 uom_id = procurement.product_id.uom_po_id.id
1038 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1040 qty = max(qty,seller_qty)
1042 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
1044 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1045 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1047 #Passing partner_id to context for purchase order line integrity of Line name
1048 context.update({'lang': partner.lang, 'partner_id': partner_id})
1050 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
1051 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1052 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1054 name = product.partner_ref
1055 if product.description_purchase:
1056 name += '\n'+ product.description_purchase
1060 'product_id': procurement.product_id.id,
1061 'product_uom': uom_id,
1062 'price_unit': price or 0.0,
1063 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1064 'move_dest_id': res_id,
1065 'taxes_id': [(6,0,taxes)],
1067 name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1070 'origin': procurement.origin,
1071 'partner_id': partner_id,
1072 'location_id': procurement.location_id.id,
1073 'warehouse_id': warehouse_id and warehouse_id[0] or False,
1074 'pricelist_id': pricelist_id,
1075 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1076 'company_id': procurement.company_id.id,
1077 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1079 res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=context)
1080 self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]})
1081 self.running_send_note(cr, uid, [procurement.id], context=context)
1086 class mail_message(osv.osv):
1087 _name = 'mail.message'
1088 _inherit = 'mail.message'
1090 def _postprocess_sent_message(self, cr, uid, message, context=None):
1091 if message.model == 'purchase.order':
1092 wf_service = netsvc.LocalService("workflow")
1093 wf_service.trg_validate(uid, 'purchase.order', message.res_id, 'send_rfq', cr)
1094 return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1097 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: