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, you must cancel it 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 purchase order line.'))
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 _('Define purchase journal 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!'), _('Define expense account for this company: "%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 _('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 receptions 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 needaction_domain_get(self, cr, uid, ids, context=None):
736 return [('state','=','draft')]
738 def create_send_note(self, cr, uid, ids, context=None):
739 return self.message_append_note(cr, uid, ids, body=_("Request for quotation <b>created</b>."), context=context)
741 def confirm_send_note(self, cr, uid, ids, context=None):
742 for obj in self.browse(cr, uid, ids, context=context):
743 self.message_subscribe(cr, uid, [obj.id], [obj.validator.id], context=context)
744 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)
746 def shipment_send_note(self, cr, uid, ids, picking_id, context=None):
747 for order in self.browse(cr, uid, ids, context=context):
748 for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
749 # convert datetime field to a datetime, using server format, then
750 # convert it to the user TZ and re-render it with %Z to add the timezone
751 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
752 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
753 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)
755 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
756 for order in self.browse(cr, uid, ids, context=context):
757 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
758 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)
760 def shipment_done_send_note(self, cr, uid, ids, context=None):
761 self.message_append_note(cr, uid, ids, body=_("""Shipment <b>received</b>."""), context=context)
763 def invoice_done_send_note(self, cr, uid, ids, context=None):
764 self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
766 def draft_send_note(self, cr, uid, ids, context=None):
767 return self.message_append_note(cr, uid, ids, body=_("Purchase Order has been set to <b>draft</b>."), context=context)
769 def cancel_send_note(self, cr, uid, ids, context=None):
770 for obj in self.browse(cr, uid, ids, context=context):
771 self.message_append_note(cr, uid, [obj.id], body=_("Purchase Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
775 class purchase_order_line(osv.osv):
776 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
778 cur_obj=self.pool.get('res.currency')
779 tax_obj = self.pool.get('account.tax')
780 for line in self.browse(cr, uid, ids, context=context):
781 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
782 cur = line.order_id.pricelist_id.currency_id
783 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
786 def _get_uom_id(self, cr, uid, context=None):
788 proxy = self.pool.get('ir.model.data')
789 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
791 except Exception, ex:
795 'name': fields.text('Description', required=True),
796 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
797 'date_planned': fields.date('Scheduled Date', required=True, select=True),
798 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
799 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
800 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
801 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
802 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
803 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
804 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
805 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
806 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
807 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
808 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
809 help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
810 \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
811 \n* The \'Done\' state is set automatically when purchase order is set as done. \
812 \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
813 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
814 'invoiced': fields.boolean('Invoiced', readonly=True),
815 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
816 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
820 'product_uom' : _get_uom_id,
821 'product_qty': lambda *a: 1.0,
822 'state': lambda *args: 'draft',
823 'invoiced': lambda *a: 0,
825 _table = 'purchase_order_line'
826 _name = 'purchase.order.line'
827 _description = 'Purchase Order Line'
829 def copy_data(self, cr, uid, id, default=None, context=None):
832 default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
833 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
835 def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
836 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
837 name=False, price_unit=False, context=None):
839 onchange handler of product_uom.
842 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
843 return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
844 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
845 name=name, price_unit=price_unit, context=context)
847 def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
848 """Return the datetime value to use as Schedule Date (``date_planned``) for
849 PO Lines that correspond to the given product.supplierinfo,
850 when ordered at `date_order_str`.
852 :param browse_record | False supplier_info: product.supplierinfo, used to
853 determine delivery delay (if False, default delay = 0)
854 :param str date_order_str: date of order, as a string in
855 DEFAULT_SERVER_DATE_FORMAT
857 :return: desired Schedule Date for the PO line
859 supplier_delay = int(supplier_info.delay) if supplier_info else 0
860 return datetime.strptime(date_order_str, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=supplier_delay)
862 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
863 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
864 name=False, price_unit=False, context=None):
866 onchange handler of product_id.
871 res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
875 product_product = self.pool.get('product.product')
876 product_uom = self.pool.get('product.uom')
877 res_partner = self.pool.get('res.partner')
878 product_supplierinfo = self.pool.get('product.supplierinfo')
879 product_pricelist = self.pool.get('product.pricelist')
880 account_fiscal_position = self.pool.get('account.fiscal.position')
881 account_tax = self.pool.get('account.tax')
883 # - check for the presence of partner_id and pricelist_id
885 raise osv.except_osv(_('No Pricelist !'), _('Select a price list for a supplier in the purchase form to choose a product.'))
887 raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
889 # - determine name and notes based on product in partner lang.
890 lang = res_partner.browse(cr, uid, partner_id).lang
891 context_partner = {'lang': lang, 'partner_id': partner_id}
892 product = product_product.browse(cr, uid, product_id, context=context_partner)
894 if product.description_purchase:
895 name += '\n' + product.description_purchase
896 res['value'].update({'name': name})
898 # - set a domain on product_uom
899 res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
901 # - check that uom and product uom belong to the same category
902 product_uom_po_id = product.uom_po_id.id
904 uom_id = product_uom_po_id
906 if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
907 res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
908 uom_id = product_uom_po_id
910 res['value'].update({'product_uom': uom_id})
912 # - determine product_qty and date_planned based on seller info
914 date_order = fields.date.context_today(self,cr,uid,context=context)
918 for supplier in product.seller_ids:
919 if supplier.name.id == partner_id:
920 supplierinfo = supplier
921 if supplierinfo.product_uom.id != uom_id:
922 res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
923 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
924 if qty < min_qty: # If the supplier quantity is greater than entered from user, set minimal.
925 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)}
928 dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
930 res['value'].update({'date_planned': date_planned or dt, 'product_qty': qty})
932 # - determine price_unit and taxes_id
933 price = product_pricelist.price_get(cr, uid, [pricelist_id],
934 product.id, qty or 1.0, partner_id, {'uom': uom_id, 'date': date_order})[pricelist_id]
936 taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
937 fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
938 taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
939 res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
943 product_id_change = onchange_product_id
944 product_uom_change = onchange_product_uom
946 def action_confirm(self, cr, uid, ids, context=None):
947 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
950 purchase_order_line()
952 class procurement_order(osv.osv):
953 _inherit = 'procurement.order'
955 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
958 def action_po_assign(self, cr, uid, ids, context=None):
959 """ This is action which call from workflow to assign purchase order to procurements
962 res = self.make_po(cr, uid, ids, context=context)
964 return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
966 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
967 """Create the purchase order from the procurement, using
968 the provided field values, after adding the given purchase
969 order line in the purchase order.
971 :params procurement: the procurement object generating the purchase order
972 :params dict po_vals: field values for the new purchase order (the
973 ``order_line`` field will be overwritten with one
974 single line, as passed in ``line_vals``).
975 :params dict line_vals: field values of the single purchase order line that
976 the purchase order will contain.
977 :return: id of the newly created purchase order
980 po_vals.update({'order_line': [(0,0,line_vals)]})
981 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
983 def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
984 """Return the datetime value to use as Schedule Date (``date_planned``) for the
985 Purchase Order Lines created to satisfy the given procurement.
987 :param browse_record procurement: the procurement for which a PO will be created.
988 :param browse_report company: the company to which the new PO will belong to.
990 :return: the desired Schedule Date for the PO lines
992 procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
993 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
996 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
997 """Return the datetime value to use as Order Date (``date_order``) for the
998 Purchase Order created to satisfy the given procurement.
1000 :param browse_record procurement: the procurement for which a PO will be created.
1001 :param browse_report company: the company to which the new PO will belong to.
1002 :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1004 :return: the desired Order Date for the PO
1006 seller_delay = int(procurement.product_id.seller_delay)
1007 return schedule_date - relativedelta(days=seller_delay)
1009 def make_po(self, cr, uid, ids, context=None):
1010 """ Make purchase order from procurement
1011 @return: New created Purchase Orders procurement wise
1016 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1017 partner_obj = self.pool.get('res.partner')
1018 uom_obj = self.pool.get('product.uom')
1019 pricelist_obj = self.pool.get('product.pricelist')
1020 prod_obj = self.pool.get('product.product')
1021 acc_pos_obj = self.pool.get('account.fiscal.position')
1022 seq_obj = self.pool.get('ir.sequence')
1023 warehouse_obj = self.pool.get('stock.warehouse')
1024 for procurement in self.browse(cr, uid, ids, context=context):
1025 res_id = procurement.move_id.id
1026 partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
1027 seller_qty = procurement.product_id.seller_qty
1028 partner_id = partner.id
1029 address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1030 pricelist_id = partner.property_product_pricelist_purchase.id
1031 warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
1032 uom_id = procurement.product_id.uom_po_id.id
1034 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1036 qty = max(qty,seller_qty)
1038 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
1040 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1041 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1043 #Passing partner_id to context for purchase order line integrity of Line name
1044 context.update({'lang': partner.lang, 'partner_id': partner_id})
1046 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
1047 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1048 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1050 name = product.partner_ref
1051 if product.description_purchase:
1052 name += '\n'+ product.description_purchase
1056 'product_id': procurement.product_id.id,
1057 'product_uom': uom_id,
1058 'price_unit': price or 0.0,
1059 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1060 'move_dest_id': res_id,
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: