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 openerp import SUPERUSER_ID, workflow
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
26 from operator import attrgetter
27 from openerp.tools.safe_eval import safe_eval as eval
28 from openerp.osv import fields, osv
29 from openerp.tools.translate import _
30 import openerp.addons.decimal_precision as dp
31 from openerp.osv.orm import browse_record_list, browse_record, browse_null
32 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
33 from openerp.tools.float_utils import float_compare
35 class purchase_order(osv.osv):
37 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
39 cur_obj=self.pool.get('res.currency')
40 for order in self.browse(cr, uid, ids, context=context):
42 'amount_untaxed': 0.0,
47 cur = order.pricelist_id.currency_id
48 for line in order.order_line:
49 val1 += line.price_subtotal
50 for c in self.pool.get('account.tax').compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, order.partner_id)['taxes']:
51 val += c.get('amount', 0.0)
52 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
53 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
54 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
57 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
58 if not value: return False
59 if type(ids)!=type([]):
61 for po in self.browse(cr, uid, ids, context=context):
63 cr.execute("""update purchase_order_line set
67 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
68 cr.execute("""update purchase_order set
69 minimum_planned_date=%s where id=%s""", (value, po.id))
70 self.invalidate_cache(cr, uid, context=context)
73 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
75 purchase_obj=self.browse(cr, uid, ids, context=context)
76 for purchase in purchase_obj:
77 res[purchase.id] = False
78 if purchase.order_line:
79 min_date=purchase.order_line[0].date_planned
80 for line in purchase.order_line:
81 if line.date_planned < min_date:
82 min_date=line.date_planned
83 res[purchase.id]=min_date
87 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
89 for purchase in self.browse(cursor, user, ids, context=context):
91 for invoice in purchase.invoice_ids:
92 if invoice.state not in ('draft','cancel'):
93 tot += invoice.amount_untaxed
94 if purchase.amount_untaxed:
95 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
97 res[purchase.id] = 0.0
100 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
101 if not ids: return {}
106 p.order_id, sum(m.product_qty), m.state
110 purchase_order_line p on (p.id=m.purchase_line_id)
112 p.order_id IN %s GROUP BY m.state, p.order_id''',(tuple(ids),))
113 for oid,nbr,state in cr.fetchall():
117 res[oid][0] += nbr or 0.0
118 res[oid][1] += nbr or 0.0
120 res[oid][1] += nbr or 0.0
125 res[r] = 100.0 * res[r][0] / res[r][1]
128 def _get_order(self, cr, uid, ids, context=None):
130 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
131 result[line.order_id.id] = True
134 def _invoiced(self, cursor, user, ids, name, arg, context=None):
136 for purchase in self.browse(cursor, user, ids, context=context):
137 res[purchase.id] = all(line.invoiced for line in purchase.order_line)
140 def _get_journal(self, cr, uid, context=None):
143 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
144 company_id = context.get('company_id', user.company_id.id)
145 journal_obj = self.pool.get('account.journal')
146 res = journal_obj.search(cr, uid, [('type', '=', 'purchase'),
147 ('company_id', '=', company_id)],
149 return res and res[0] or False
151 def _get_picking_in(self, cr, uid, context=None):
152 obj_data = self.pool.get('ir.model.data')
153 type_obj = self.pool.get('stock.picking.type')
154 user_obj = self.pool.get('res.users')
155 company_id = user_obj.browse(cr, uid, uid, context=context).company_id.id
156 types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)], context=context)
158 types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id', '=', False)], context=context)
160 raise osv.except_osv(_('Error!'), _("Make sure you have at least an incoming picking type defined"))
163 def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
168 SELECT picking_id, po.id FROM stock_picking p, stock_move m, purchase_order_line pol, purchase_order po
169 WHERE po.id in %s and po.id = pol.order_id and pol.id = m.purchase_line_id and m.picking_id = p.id
170 GROUP BY picking_id, po.id
173 cr.execute(query, (tuple(ids), ))
174 picks = cr.fetchall()
175 for pick_id, po_id in picks:
176 res[po_id].append(pick_id)
179 def _count_all(self, cr, uid, ids, field_name, arg, context=None):
182 'shipment_count': len(purchase.picking_ids),
183 'invoice_count': len(purchase.invoice_ids),
185 for purchase in self.browse(cr, uid, ids, context=context)
189 ('draft', 'Draft PO'),
191 ('bid', 'Bid Received'),
192 ('confirmed', 'Waiting Approval'),
193 ('approved', 'Purchase Confirmed'),
194 ('except_picking', 'Shipping Exception'),
195 ('except_invoice', 'Invoice Exception'),
197 ('cancel', 'Cancelled')
201 'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirmed',
202 'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'approved',
203 'purchase.mt_rfq_done': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
207 'name': fields.char('Order Reference', required=True, select=True, copy=False,
208 help="Unique number of the purchase order, "
209 "computed automatically when the purchase order is created."),
210 'origin': fields.char('Source Document', copy=False,
211 help="Reference of the document that generated this purchase order "
212 "request; a sales order or an internal procurement request."),
213 'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)],
214 'approved':[('readonly',True)],
215 'done':[('readonly',True)]},
217 help="Reference of the sales order or bid sent by your supplier. "
218 "It's mainly used to do the matching when you receive the "
219 "products as this reference is usually written on the "
220 "delivery order sent by your supplier."),
221 'date_order':fields.datetime('Order Date', required=True, states={'confirmed':[('readonly',True)],
222 'approved':[('readonly',True)]},
223 select=True, help="Depicts the date where the Quotation should be validated and converted into a Purchase Order, by default it's the creation date.",
225 'date_approve':fields.date('Date Approved', readonly=1, select=True, copy=False,
226 help="Date on which purchase order has been approved"),
227 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
228 change_default=True, track_visibility='always'),
229 'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
230 states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
231 help="Put an address if you want to deliver directly from the supplier to the customer. " \
232 "Otherwise, keep empty to deliver to your own company."
234 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]} ),
235 '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."),
236 'currency_id': fields.many2one('res.currency','Currency', readonly=True, required=True,states={'draft': [('readonly', False)],'sent': [('readonly', False)]}),
237 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True,
238 help="The status of the purchase order or the quotation request. "
239 "A request for quotation is a purchase order in a 'Draft' status. "
240 "Then the order has to be confirmed by the user, the status switch "
241 "to 'Confirmed'. Then the supplier must confirm the order to change "
242 "the status to 'Approved'. When the purchase order is paid and "
243 "received, the status becomes 'Done'. If a cancel action occurs in "
244 "the invoice or in the receipt of goods, the status becomes "
246 select=True, copy=False),
247 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines',
248 states={'approved':[('readonly',True)],
249 'done':[('readonly',True)]},
251 'validator' : fields.many2one('res.users', 'Validated by', readonly=True, copy=False),
252 'notes': fields.text('Terms and Conditions'),
253 'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id',
254 'invoice_id', 'Invoices', copy=False,
255 help="Invoices generated for a purchase order"),
256 'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking List', help="This is the list of receipts that have been generated for this purchase order."),
257 'shipped':fields.boolean('Received', readonly=True, select=True, copy=False,
258 help="It indicates that a picking has been done"),
259 'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'),
260 'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', copy=False,
261 help="It indicates that an invoice has been validated"),
262 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
263 'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on incoming shipments')], 'Invoicing Control', required=True,
264 readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)],'bid':[('readonly',False)]},
265 help="Based on Purchase Order lines: place individual lines in 'Invoice Control / On Purchase Order lines' from where you can selectively create an invoice.\n" \
266 "Based on generated invoice: create a draft invoice you can validate later.\n" \
267 "Based on incoming shipments: let you create an invoice when receipts are validated."
269 '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.",
271 'purchase.order.line': (_get_order, ['date_planned'], 10),
274 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
276 'purchase.order.line': (_get_order, None, 10),
277 }, multi="sums", help="The amount without tax", track_visibility='always'),
278 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
280 'purchase.order.line': (_get_order, None, 10),
281 }, multi="sums", help="The tax amount"),
282 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
284 'purchase.order.line': (_get_order, None, 10),
285 }, multi="sums", help="The total amount"),
286 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
287 'payment_term_id': fields.many2one('account.payment.term', 'Payment Term'),
288 'incoterm_id': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
289 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
290 'create_uid': fields.many2one('res.users', 'Responsible'),
291 'company_id': fields.many2one('res.company', 'Company', required=True, select=1, states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)]}),
292 'journal_id': fields.many2one('account.journal', 'Journal'),
293 'bid_date': fields.date('Bid Received On', readonly=True, help="Date on which the bid was received"),
294 'bid_validity': fields.date('Bid Valid Until', help="Date on which the bid expired"),
295 'picking_type_id': fields.many2one('stock.picking.type', 'Deliver To', help="This will determine picking type of incoming shipment", required=True,
296 states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}),
297 'related_location_id': fields.related('picking_type_id', 'default_location_dest_id', type='many2one', relation='stock.location', string="Related location", store=True),
298 'shipment_count': fields.function(_count_all, type='integer', string='Incoming Shipments', multi=True),
299 'invoice_count': fields.function(_count_all, type='integer', string='Invoices', multi=True)
302 'date_order': fields.datetime.now,
304 'name': lambda obj, cr, uid, context: '/',
306 'invoice_method': 'order',
308 '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,
309 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
310 'journal_id': _get_journal,
311 'currency_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id,
312 'picking_type_id': _get_picking_in,
315 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
317 _name = "purchase.order"
318 _inherit = ['mail.thread', 'ir.needaction_mixin']
319 _description = "Purchase Order"
320 _order = 'date_order desc, id desc'
322 def create(self, cr, uid, vals, context=None):
323 if vals.get('name','/')=='/':
324 vals['name'] = self.pool.get('ir.sequence').next_by_code(cr, uid, 'purchase.order') or '/'
325 context = dict(context or {}, mail_create_nolog=True)
326 order = super(purchase_order, self).create(cr, uid, vals, context=context)
327 self.message_post(cr, uid, [order], body=_("RFQ created"), context=context)
330 def unlink(self, cr, uid, ids, context=None):
331 purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
333 for s in purchase_orders:
334 if s['state'] in ['draft','cancel']:
335 unlink_ids.append(s['id'])
337 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.'))
339 # automatically sending subflow.delete upon deletion
340 self.signal_workflow(cr, uid, unlink_ids, 'purchase_cancel')
342 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
344 def set_order_line_status(self, cr, uid, ids, status, context=None):
345 line = self.pool.get('purchase.order.line')
347 proc_obj = self.pool.get('procurement.order')
348 for order in self.browse(cr, uid, ids, context=context):
349 order_line_ids += [po_line.id for po_line in order.order_line]
351 line.write(cr, uid, order_line_ids, {'state': status}, context=context)
352 if order_line_ids and status == 'cancel':
353 procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', order_line_ids)], context=context)
355 proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
358 def button_dummy(self, cr, uid, ids, context=None):
361 def onchange_pricelist(self, cr, uid, ids, pricelist_id, context=None):
364 return {'value': {'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id}}
366 #Destination address is used when dropshipping
367 def onchange_dest_address_id(self, cr, uid, ids, address_id, context=None):
370 address = self.pool.get('res.partner')
372 supplier = address.browse(cr, uid, address_id, context=context)
374 location_id = supplier.property_stock_customer.id
375 values.update({'location_id': location_id})
376 return {'value':values}
378 def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None):
381 picktype = self.pool.get("stock.picking.type").browse(cr, uid, picking_type_id, context=context)
382 if picktype.default_location_dest_id:
383 value.update({'location_id': picktype.default_location_dest_id.id})
384 value.update({'related_location_id': picktype.default_location_dest_id and picktype.default_location_dest_id.id or False})
385 return {'value': value}
387 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
388 partner = self.pool.get('res.partner')
391 'fiscal_position': False,
392 'payment_term_id': False,
394 supplier_address = partner.address_get(cr, uid, [partner_id], ['default'], context=context)
395 supplier = partner.browse(cr, uid, partner_id, context=context)
397 'pricelist_id': supplier.property_product_pricelist_purchase.id,
398 'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
399 'payment_term_id': supplier.property_supplier_payment_term.id or False,
402 def invoice_open(self, cr, uid, ids, context=None):
403 mod_obj = self.pool.get('ir.model.data')
404 act_obj = self.pool.get('ir.actions.act_window')
406 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree2')
407 id = result and result[1] or False
408 result = act_obj.read(cr, uid, [id], context=context)[0]
410 for po in self.browse(cr, uid, ids, context=context):
411 inv_ids+= [invoice.id for invoice in po.invoice_ids]
413 raise osv.except_osv(_('Error!'), _('Please create Invoices.'))
414 #choose the view_mode accordingly
416 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
418 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
419 result['views'] = [(res and res[1] or False, 'form')]
420 result['res_id'] = inv_ids and inv_ids[0] or False
423 def view_invoice(self, cr, uid, ids, context=None):
425 This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
427 context = dict(context or {})
428 mod_obj = self.pool.get('ir.model.data')
429 wizard_obj = self.pool.get('purchase.order.line_invoice')
430 #compute the number of invoices to display
432 for po in self.browse(cr, uid, ids, context=context):
433 if po.invoice_method == 'manual':
434 if not po.invoice_ids:
435 context.update({'active_ids' : [line.id for line in po.order_line]})
436 wizard_obj.makeInvoices(cr, uid, [], context=context)
438 for po in self.browse(cr, uid, ids, context=context):
439 inv_ids+= [invoice.id for invoice in po.invoice_ids]
440 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
441 res_id = res and res[1] or False
444 'name': _('Supplier Invoices'),
448 'res_model': 'account.invoice',
449 'context': "{'type':'in_invoice', 'journal_type': 'purchase'}",
450 'type': 'ir.actions.act_window',
453 'res_id': inv_ids and inv_ids[0] or False,
456 def view_picking(self, cr, uid, ids, context=None):
458 This function returns an action that display existing picking orders of given purchase order ids.
462 mod_obj = self.pool.get('ir.model.data')
463 dummy, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree'))
464 action = self.pool.get('ir.actions.act_window').read(cr, uid, action_id, context=context)
467 for po in self.browse(cr, uid, ids, context=context):
468 pick_ids += [picking.id for picking in po.picking_ids]
470 #override the context to get rid of the default filtering on picking type
471 action['context'] = {}
472 #choose the view_mode accordingly
473 if len(pick_ids) > 1:
474 action['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
476 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
477 action['views'] = [(res and res[1] or False, 'form')]
478 action['res_id'] = pick_ids and pick_ids[0] or False
481 def wkf_approve_order(self, cr, uid, ids, context=None):
482 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)})
485 def wkf_bid_received(self, cr, uid, ids, context=None):
486 return self.write(cr, uid, ids, {'state':'bid', 'bid_date': fields.date.context_today(self,cr,uid,context=context)})
488 def wkf_send_rfq(self, cr, uid, ids, context=None):
490 This function opens a window to compose an email, with the edi purchase template message loaded by default
494 ir_model_data = self.pool.get('ir.model.data')
496 if context.get('send_rfq', False):
497 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
499 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase_done')[1]
503 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
505 compose_form_id = False
508 'default_model': 'purchase.order',
509 'default_res_id': ids[0],
510 'default_use_template': bool(template_id),
511 'default_template_id': template_id,
512 'default_composition_mode': 'comment',
515 'name': _('Compose Email'),
516 'type': 'ir.actions.act_window',
519 'res_model': 'mail.compose.message',
520 'views': [(compose_form_id, 'form')],
521 'view_id': compose_form_id,
526 def print_quotation(self, cr, uid, ids, context=None):
528 This function prints the request for quotation and mark it as sent, so that we can see more easily the next step of the workflow
530 assert len(ids) == 1, 'This option should only be used for a single id at a time'
531 self.signal_workflow(cr, uid, ids, 'send_rfq')
532 return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
534 def wkf_confirm_order(self, cr, uid, ids, context=None):
536 for po in self.browse(cr, uid, ids, context=context):
537 if not po.order_line:
538 raise osv.except_osv(_('Error!'),_('You cannot confirm a purchase order without any purchase order line.'))
539 for line in po.order_line:
540 if line.state=='draft':
542 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
544 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
547 def _choose_account_from_po_line(self, cr, uid, po_line, context=None):
548 fiscal_obj = self.pool.get('account.fiscal.position')
549 property_obj = self.pool.get('ir.property')
550 if po_line.product_id:
551 acc_id = po_line.product_id.property_account_expense.id
553 acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
555 raise osv.except_osv(_('Error!'), _('Define an expense account for this product: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
557 acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context=context).id
558 fpos = po_line.order_id.fiscal_position or False
559 return fiscal_obj.map_account(cr, uid, fpos, acc_id)
561 def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
562 """Collects require data from purchase order line that is used to create invoice line
563 for that purchase order line
564 :param account_id: Expense account of the product of PO line if any.
565 :param browse_record order_line: Purchase order line browse record
566 :return: Value for fields of invoice lines.
570 'name': order_line.name,
571 'account_id': account_id,
572 'price_unit': order_line.price_unit or 0.0,
573 'quantity': order_line.product_qty,
574 'product_id': order_line.product_id.id or False,
575 'uos_id': order_line.product_uom.id or False,
576 'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
577 'account_analytic_id': order_line.account_analytic_id.id or False,
578 'purchase_line_id': order_line.id,
581 def _prepare_invoice(self, cr, uid, order, line_ids, context=None):
582 """Prepare the dict of values to create the new invoice for a
583 purchase order. This method may be overridden to implement custom
584 invoice generation (making sure to call super() to establish
585 a clean extension chain).
587 :param browse_record order: purchase.order record to invoice
588 :param list(int) line_ids: list of invoice line IDs that must be
589 attached to the invoice
590 :return: dict of value to create() the invoice
592 journal_ids = self.pool['account.journal'].search(
593 cr, uid, [('type', '=', 'purchase'),
594 ('company_id', '=', order.company_id.id)],
597 raise osv.except_osv(
599 _('Define purchase journal for this company: "%s" (id:%d).') % \
600 (order.company_id.name, order.company_id.id))
602 'name': order.partner_ref or order.name,
603 'reference': order.partner_ref or order.name,
604 'account_id': order.partner_id.property_account_payable.id,
605 'type': 'in_invoice',
606 'partner_id': order.partner_id.id,
607 'currency_id': order.currency_id.id,
608 'journal_id': len(journal_ids) and journal_ids[0] or False,
609 'invoice_line': [(6, 0, line_ids)],
610 'origin': order.name,
611 'fiscal_position': order.fiscal_position.id or False,
612 'payment_term': order.payment_term_id.id or False,
613 'company_id': order.company_id.id,
616 def action_cancel_draft(self, cr, uid, ids, context=None):
619 self.write(cr, uid, ids, {'state':'draft','shipped':0})
620 self.set_order_line_status(cr, uid, ids, 'draft', context=context)
622 # Deleting the existing instance of workflow for PO
623 self.delete_workflow(cr, uid, [p_id]) # TODO is it necessary to interleave the calls?
624 self.create_workflow(cr, uid, [p_id])
627 def wkf_po_done(self, cr, uid, ids, context=None):
628 self.write(cr, uid, ids, {'state': 'done'}, context=context)
629 self.set_order_line_status(cr, uid, ids, 'done', context=context)
631 def action_invoice_create(self, cr, uid, ids, context=None):
632 """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
633 :param ids: list of ids of purchase orders.
634 :return: ID of created invoice.
637 context = dict(context or {})
639 inv_obj = self.pool.get('account.invoice')
640 inv_line_obj = self.pool.get('account.invoice.line')
643 uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
644 for order in self.browse(cr, uid, ids, context=context):
645 context.pop('force_company', None)
646 if order.company_id.id != uid_company_id:
647 #if the company of the document is different than the current user company, force the company in the context
648 #then re-do a browse to read the property fields for the good company.
649 context['force_company'] = order.company_id.id
650 order = self.browse(cr, uid, order.id, context=context)
652 # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
654 for po_line in order.order_line:
655 acc_id = self._choose_account_from_po_line(cr, uid, po_line, context=context)
656 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
657 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
658 inv_lines.append(inv_line_id)
659 po_line.write({'invoice_lines': [(4, inv_line_id)]})
661 # get invoice data and create invoice
662 inv_data = self._prepare_invoice(cr, uid, order, inv_lines, context=context)
663 inv_id = inv_obj.create(cr, uid, inv_data, context=context)
665 # compute the invoice
666 inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
668 # Link this new invoice to related purchase order
669 order.write({'invoice_ids': [(4, inv_id)]})
673 def invoice_done(self, cr, uid, ids, context=None):
674 self.write(cr, uid, ids, {'state': 'approved'}, context=context)
677 def has_stockable_product(self, cr, uid, ids, *args):
678 for order in self.browse(cr, uid, ids):
679 for order_line in order.order_line:
680 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
684 def wkf_action_cancel(self, cr, uid, ids, context=None):
685 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
686 self.set_order_line_status(cr, uid, ids, 'cancel', context=context)
688 def action_cancel(self, cr, uid, ids, context=None):
689 for purchase in self.browse(cr, uid, ids, context=context):
690 for pick in purchase.picking_ids:
691 for move in pick.move_lines:
692 if pick.state == 'done':
693 raise osv.except_osv(
694 _('Unable to cancel the purchase order %s.') % (purchase.name),
695 _('You have already received some goods for it. '))
696 self.pool.get('stock.picking').action_cancel(cr, uid, [x.id for x in purchase.picking_ids if x.state != 'cancel'], context=context)
697 for inv in purchase.invoice_ids:
698 if inv and inv.state not in ('cancel', 'draft'):
699 raise osv.except_osv(
700 _('Unable to cancel this purchase order.'),
701 _('You must first cancel all invoices related to this purchase order.'))
702 self.pool.get('account.invoice') \
703 .signal_workflow(cr, uid, map(attrgetter('id'), purchase.invoice_ids), 'invoice_cancel')
704 self.signal_workflow(cr, uid, ids, 'purchase_cancel')
707 def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
708 ''' prepare the stock move data from the PO line. This function returns a list of dictionary ready to be used in stock.move's create()'''
709 product_uom = self.pool.get('product.uom')
710 price_unit = order_line.price_unit
711 if order_line.product_uom.id != order_line.product_id.uom_id.id:
712 price_unit *= order_line.product_uom.factor / order_line.product_id.uom_id.factor
713 if order.currency_id.id != order.company_id.currency_id.id:
714 #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency
715 price_unit = self.pool.get('res.currency').compute(cr, uid, order.currency_id.id, order.company_id.currency_id.id, price_unit, round=False, context=context)
718 'name': order_line.name or '',
719 'product_id': order_line.product_id.id,
720 'product_uom': order_line.product_uom.id,
721 'product_uos': order_line.product_uom.id,
722 'date': order.date_order,
723 'date_expected': fields.date.date_to_datetime(self, cr, uid, order_line.date_planned, context),
724 'location_id': order.partner_id.property_stock_supplier.id,
725 'location_dest_id': order.location_id.id,
726 'picking_id': picking_id,
727 'partner_id': order.dest_address_id.id or order.partner_id.id,
728 'move_dest_id': False,
730 'purchase_line_id': order_line.id,
731 'company_id': order.company_id.id,
732 'price_unit': price_unit,
733 'picking_type_id': order.picking_type_id.id,
734 'group_id': group_id,
735 'procurement_id': False,
736 'origin': order.name,
737 'route_ids': order.picking_type_id.warehouse_id and [(6, 0, [x.id for x in order.picking_type_id.warehouse_id.route_ids])] or [],
738 'warehouse_id':order.picking_type_id.warehouse_id.id,
739 'invoice_state': order.invoice_method == 'picking' and '2binvoiced' or 'none',
742 diff_quantity = order_line.product_qty
743 for procurement in order_line.procurement_ids:
744 procurement_qty = product_uom._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, to_uom_id=order_line.product_uom.id)
745 tmp = move_template.copy()
747 'product_uom_qty': min(procurement_qty, diff_quantity),
748 'product_uos_qty': min(procurement_qty, diff_quantity),
749 'move_dest_id': procurement.move_dest_id.id, #move destination is same as procurement destination
750 'group_id': procurement.group_id.id or group_id, #move group is same as group of procurements if it exists, otherwise take another group
751 'procurement_id': procurement.id,
752 'invoice_state': procurement.rule_id.invoice_state or (procurement.location_id and procurement.location_id.usage == 'customer' and procurement.invoice_state=='picking' and '2binvoiced') or (order.invoice_method == 'picking' and '2binvoiced') or 'none', #dropship case takes from sale
753 'propagate': procurement.rule_id.propagate,
755 diff_quantity -= min(procurement_qty, diff_quantity)
757 #if the order line has a bigger quantity than the procurement it was for (manually changed or minimal quantity), then
758 #split the future stock move in two because the route followed may be different.
759 if diff_quantity > 0:
760 move_template['product_uom_qty'] = diff_quantity
761 move_template['product_uos_qty'] = diff_quantity
762 res.append(move_template)
765 def _create_stock_moves(self, cr, uid, order, order_lines, picking_id=False, context=None):
766 """Creates appropriate stock moves for given order lines, whose can optionally create a
767 picking if none is given or no suitable is found, then confirms the moves, makes them
768 available, and confirms the pickings.
770 If ``picking_id`` is provided, the stock moves will be added to it, otherwise a standard
771 incoming picking will be created to wrap the stock moves (default behavior of the stock.move)
773 Modules that wish to customize the procurements or partition the stock moves over
774 multiple stock pickings may override this method and call ``super()`` with
775 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
777 :param browse_record order: purchase order to which the order lines belong
778 :param list(browse_record) order_lines: purchase order line records for which picking
779 and moves should be created.
780 :param int picking_id: optional ID of a stock picking to which the created stock moves
781 will be added. A new picking will be created if omitted.
784 stock_move = self.pool.get('stock.move')
786 new_group = self.pool.get("procurement.group").create(cr, uid, {'name': order.name, 'partner_id': order.partner_id.id}, context=context)
788 for order_line in order_lines:
789 if not order_line.product_id:
792 if order_line.product_id.type in ('product', 'consu'):
793 for vals in self._prepare_order_line_move(cr, uid, order, order_line, picking_id, new_group, context=context):
794 move = stock_move.create(cr, uid, vals, context=context)
795 todo_moves.append(move)
797 todo_moves = stock_move.action_confirm(cr, uid, todo_moves)
798 stock_move.force_assign(cr, uid, todo_moves)
800 def test_moves_done(self, cr, uid, ids, context=None):
801 '''PO is done at the delivery side if all the incoming shipments are done'''
802 for purchase in self.browse(cr, uid, ids, context=context):
803 for picking in purchase.picking_ids:
804 if picking.state != 'done':
808 def test_moves_except(self, cr, uid, ids, context=None):
809 ''' PO is in exception at the delivery side if one of the picking is canceled
810 and the other pickings are completed (done or canceled)
812 at_least_one_canceled = False
813 alldoneorcancel = True
814 for purchase in self.browse(cr, uid, ids, context=context):
815 for picking in purchase.picking_ids:
816 if picking.state == 'cancel':
817 at_least_one_canceled = True
818 if picking.state not in ['done', 'cancel']:
819 alldoneorcancel = False
820 return at_least_one_canceled and alldoneorcancel
822 def move_lines_get(self, cr, uid, ids, *args):
824 for order in self.browse(cr, uid, ids, context={}):
825 for line in order.order_line:
826 res += [x.id for x in line.move_ids]
829 def action_picking_create(self, cr, uid, ids, context=None):
830 for order in self.browse(cr, uid, ids):
832 'picking_type_id': order.picking_type_id.id,
833 'partner_id': order.dest_address_id.id or order.partner_id.id,
834 'date': max([l.date_planned for l in order.order_line]),
837 picking_id = self.pool.get('stock.picking').create(cr, uid, picking_vals, context=context)
838 self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
840 def picking_done(self, cr, uid, ids, context=None):
841 self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
842 # Do check on related procurements:
843 proc_obj = self.pool.get("procurement.order")
845 for po in self.browse(cr, uid, ids, context=context):
846 po_lines += [x.id for x in po.order_line]
848 procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', po_lines)], context=context)
850 proc_obj.check(cr, uid, procs, context=context)
851 self.message_post(cr, uid, ids, body=_("Products received"), context=context)
854 def do_merge(self, cr, uid, ids, context=None):
856 To merge similar type of purchase orders.
857 Orders will only be merged if:
858 * Purchase Orders are in draft
859 * Purchase Orders belong to the same partner
860 * Purchase Orders are have same stock location, same pricelist
861 Lines will only be merged if:
862 * Order lines are exactly the same except for the quantity and unit
864 @param self: The object pointer.
865 @param cr: A database cursor
866 @param uid: ID of the user currently logged in
867 @param ids: the ID or list of IDs
868 @param context: A standard dictionary
870 @return: new purchase order id
873 #TOFIX: merged order line should be unlink
874 def make_key(br, fields):
877 field_val = getattr(br, field)
878 if field in ('product_id', 'account_analytic_id'):
881 if isinstance(field_val, browse_record):
882 field_val = field_val.id
883 elif isinstance(field_val, browse_null):
885 elif isinstance(field_val, browse_record_list):
886 field_val = ((6, 0, tuple([v.id for v in field_val])),)
887 list_key.append((field, field_val))
889 return tuple(list_key)
891 context = dict(context or {})
893 # Compute what the new orders should contain
896 order_lines_to_move = []
897 for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
898 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
899 new_order = new_orders.setdefault(order_key, ({}, []))
900 new_order[1].append(porder.id)
901 order_infos = new_order[0]
905 'origin': porder.origin,
906 'date_order': porder.date_order,
907 'partner_id': porder.partner_id.id,
908 'dest_address_id': porder.dest_address_id.id,
909 'picking_type_id': porder.picking_type_id.id,
910 'location_id': porder.location_id.id,
911 'pricelist_id': porder.pricelist_id.id,
914 'notes': '%s' % (porder.notes or '',),
915 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
918 if porder.date_order < order_infos['date_order']:
919 order_infos['date_order'] = porder.date_order
921 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
923 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
925 for order_line in porder.order_line:
926 order_lines_to_move += [order_line.id]
930 for order_key, (order_data, old_ids) in new_orders.iteritems():
931 # skip merges with only one order
933 allorders += (old_ids or [])
936 # cleanup order line data
937 for key, value in order_data['order_line'].iteritems():
938 del value['uom_factor']
939 value.update(dict(key))
940 order_data['order_line'] = [(6, 0, order_lines_to_move)]
942 # create the new order
943 context.update({'mail_create_nolog': True})
944 neworder_id = self.create(cr, uid, order_data)
945 self.message_post(cr, uid, [neworder_id], body=_("RFQ created"), context=context)
946 orders_info.update({neworder_id: old_ids})
947 allorders.append(neworder_id)
949 # make triggers pointing to the old orders point to the new order
950 for old_id in old_ids:
951 self.redirect_workflow(cr, uid, [(old_id, neworder_id)])
952 self.signal_workflow(cr, uid, [old_id], 'purchase_cancel')
957 class purchase_order_line(osv.osv):
958 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
960 cur_obj=self.pool.get('res.currency')
961 tax_obj = self.pool.get('account.tax')
962 for line in self.browse(cr, uid, ids, context=context):
963 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, line.order_id.partner_id)
964 cur = line.order_id.pricelist_id.currency_id
965 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
968 def _get_uom_id(self, cr, uid, context=None):
970 proxy = self.pool.get('ir.model.data')
971 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
973 except Exception, ex:
977 'name': fields.text('Description', required=True),
978 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
979 'date_planned': fields.date('Scheduled Date', required=True, select=True),
980 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
981 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
982 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
983 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
984 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
985 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
986 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
987 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
988 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
989 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')],
990 'Status', required=True, readonly=True, copy=False,
991 help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
992 \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
993 \n* The \'Done\' status is set automatically when purchase order is set as done. \
994 \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
995 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel',
996 'order_line_id', 'invoice_id', 'Invoice Lines',
997 readonly=True, copy=False),
998 'invoiced': fields.boolean('Invoiced', readonly=True, copy=False),
999 'partner_id': fields.related('order_id', 'partner_id', string='Partner', readonly=True, type="many2one", relation="res.partner", store=True),
1000 'date_order': fields.related('order_id', 'date_order', string='Order Date', readonly=True, type="datetime"),
1001 'procurement_ids': fields.one2many('procurement.order', 'purchase_line_id', string='Associated procurements'),
1004 'product_uom' : _get_uom_id,
1005 'product_qty': lambda *a: 1.0,
1006 'state': lambda *args: 'draft',
1007 'invoiced': lambda *a: 0,
1009 _table = 'purchase_order_line'
1010 _name = 'purchase.order.line'
1011 _description = 'Purchase Order Line'
1013 def unlink(self, cr, uid, ids, context=None):
1014 for line in self.browse(cr, uid, ids, context=context):
1015 if line.state not in ['draft', 'cancel']:
1016 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
1017 procurement_obj = self.pool.get('procurement.order')
1018 procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context)
1019 if procurement_ids_to_cancel:
1020 self.pool['procurement.order'].cancel(cr, uid, procurement_ids_to_cancel)
1021 return super(purchase_order_line, self).unlink(cr, uid, ids, context=context)
1023 def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1024 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1025 name=False, price_unit=False, state='draft', context=None):
1027 onchange handler of product_uom.
1032 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1033 context = dict(context, purchase_uom_check=True)
1034 return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1035 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
1036 name=name, price_unit=price_unit, state=state, context=context)
1038 def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
1039 """Return the datetime value to use as Schedule Date (``date_planned``) for
1040 PO Lines that correspond to the given product.supplierinfo,
1041 when ordered at `date_order_str`.
1043 :param browse_record | False supplier_info: product.supplierinfo, used to
1044 determine delivery delay (if False, default delay = 0)
1045 :param str date_order_str: date of order field, as a string in
1046 DEFAULT_SERVER_DATETIME_FORMAT
1048 :return: desired Schedule Date for the PO line
1050 supplier_delay = int(supplier_info.delay) if supplier_info else 0
1051 return datetime.strptime(date_order_str, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=supplier_delay)
1053 def action_cancel(self, cr, uid, ids, context=None):
1054 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1055 for po_line in self.browse(cr, uid, ids, context=context):
1056 if all([l.state == 'cancel' for l in po_line.order_id.order_line]):
1057 self.pool.get('purchase.order').action_cancel(cr, uid, [po_line.order_id.id], context=context)
1059 def _check_product_uom_group(self, cr, uid, context=None):
1060 group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom')
1061 res = [user for user in group_uom.users if user.id == uid]
1062 return len(res) and True or False
1065 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1066 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1067 name=False, price_unit=False, state='draft', context=None):
1069 onchange handler of product_id.
1074 res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1078 product_product = self.pool.get('product.product')
1079 product_uom = self.pool.get('product.uom')
1080 res_partner = self.pool.get('res.partner')
1081 product_pricelist = self.pool.get('product.pricelist')
1082 account_fiscal_position = self.pool.get('account.fiscal.position')
1083 account_tax = self.pool.get('account.tax')
1085 # - check for the presence of partner_id and pricelist_id
1087 # raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
1088 #if not pricelist_id:
1089 # raise osv.except_osv(_('No Pricelist !'), _('Select a price list in the purchase order form before choosing a product.'))
1091 # - determine name and notes based on product in partner lang.
1092 context_partner = context.copy()
1094 lang = res_partner.browse(cr, uid, partner_id).lang
1095 context_partner.update( {'lang': lang, 'partner_id': partner_id} )
1096 product = product_product.browse(cr, uid, product_id, context=context_partner)
1097 #call name_get() with partner in the context to eventually match name and description in the seller_ids field
1098 dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0]
1099 if product.description_purchase:
1100 name += '\n' + product.description_purchase
1101 res['value'].update({'name': name})
1103 # - set a domain on product_uom
1104 res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
1106 # - check that uom and product uom belong to the same category
1107 product_uom_po_id = product.uom_po_id.id
1109 uom_id = product_uom_po_id
1111 if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
1112 if context.get('purchase_uom_check') and self._check_product_uom_group(cr, uid, context=context):
1113 res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
1114 uom_id = product_uom_po_id
1116 res['value'].update({'product_uom': uom_id})
1118 # - determine product_qty and date_planned based on seller info
1120 date_order = fields.datetime.now()
1123 supplierinfo = False
1124 precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Unit of Measure')
1125 for supplier in product.seller_ids:
1126 if partner_id and (supplier.name.id == partner_id):
1127 supplierinfo = supplier
1128 if supplierinfo.product_uom.id != uom_id:
1129 res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
1130 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
1131 if float_compare(min_qty , qty, precision_digits=precision) == 1: # If the supplier quantity is greater than entered from user, set minimal.
1133 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)}
1135 dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1137 res['value'].update({'date_planned': date_planned or dt})
1139 res['value'].update({'product_qty': qty})
1142 if price_unit is False or price_unit is None:
1143 # - determine price_unit and taxes_id
1145 date_order_str = datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT)
1146 price = product_pricelist.price_get(cr, uid, [pricelist_id],
1147 product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order_str})[pricelist_id]
1149 price = product.standard_price
1151 taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
1152 fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
1153 taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
1154 res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
1158 product_id_change = onchange_product_id
1159 product_uom_change = onchange_product_uom
1161 def action_confirm(self, cr, uid, ids, context=None):
1162 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
1166 class procurement_rule(osv.osv):
1167 _inherit = 'procurement.rule'
1169 def _get_action(self, cr, uid, context=None):
1170 return [('buy', _('Buy'))] + super(procurement_rule, self)._get_action(cr, uid, context=context)
1173 class procurement_order(osv.osv):
1174 _inherit = 'procurement.order'
1176 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
1177 'purchase_id': fields.related('purchase_line_id', 'order_id', type='many2one', relation='purchase.order', string='Purchase Order'),
1180 def propagate_cancel(self, cr, uid, procurement, context=None):
1181 if procurement.rule_id.action == 'buy' and procurement.purchase_line_id:
1182 purchase_line_obj = self.pool.get('purchase.order.line')
1183 if procurement.purchase_line_id.product_qty > procurement.product_qty and procurement.purchase_line_id.order_id.state == 'draft':
1184 purchase_line_obj.write(cr, uid, [procurement.purchase_line_id.id], {'product_qty': procurement.purchase_line_id.product_qty - procurement.product_qty}, context=context)
1186 purchase_line_obj.action_cancel(cr, uid, [procurement.purchase_line_id.id], context=context)
1187 return super(procurement_order, self).propagate_cancel(cr, uid, procurement, context=context)
1189 def _run(self, cr, uid, procurement, context=None):
1190 if procurement.rule_id and procurement.rule_id.action == 'buy':
1191 #make a purchase order for the procurement
1192 return self.make_po(cr, uid, [procurement.id], context=context)[procurement.id]
1193 return super(procurement_order, self)._run(cr, uid, procurement, context=context)
1195 def _check(self, cr, uid, procurement, context=None):
1196 if procurement.purchase_line_id and procurement.purchase_line_id.order_id.shipped: # TOCHECK: does it work for several deliveries?
1198 return super(procurement_order, self)._check(cr, uid, procurement, context=context)
1200 def _check_supplier_info(self, cr, uid, ids, context=None):
1201 ''' Check the supplier info field of a product and write an error message on the procurement if needed.
1202 Returns True if all needed information is there, False if some configuration mistake is detected.
1204 partner_obj = self.pool.get('res.partner')
1205 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1206 for procurement in self.browse(cr, uid, ids, context=context):
1208 partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
1210 if not procurement.product_id.seller_ids:
1211 message = _('No supplier defined for this product !')
1213 message = _('No default supplier defined for this product')
1214 elif not partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']:
1215 message = _('No address defined for the supplier')
1218 if procurement.message != message:
1219 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
1222 if user.company_id and user.company_id.partner_id:
1223 if partner.id == user.company_id.partner_id.id:
1224 raise osv.except_osv(_('Configuration Error!'), _('The product "%s" has been defined with your company as reseller which seems to be a configuration error!' % procurement.product_id.name))
1228 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
1229 """Create the purchase order from the procurement, using
1230 the provided field values, after adding the given purchase
1231 order line in the purchase order.
1233 :params procurement: the procurement object generating the purchase order
1234 :params dict po_vals: field values for the new purchase order (the
1235 ``order_line`` field will be overwritten with one
1236 single line, as passed in ``line_vals``).
1237 :params dict line_vals: field values of the single purchase order line that
1238 the purchase order will contain.
1239 :return: id of the newly created purchase order
1242 po_vals.update({'order_line': [(0,0,line_vals)]})
1243 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
1245 def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
1246 """Return the datetime value to use as Schedule Date (``date_planned``) for the
1247 Purchase Order Lines created to satisfy the given procurement.
1249 :param browse_record procurement: the procurement for which a PO will be created.
1250 :param browse_report company: the company to which the new PO will belong to.
1252 :return: the desired Schedule Date for the PO lines
1254 procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
1255 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
1256 return schedule_date
1258 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
1259 """Return the datetime value to use as Order Date (``date_order``) for the
1260 Purchase Order created to satisfy the given procurement.
1262 :param browse_record procurement: the procurement for which a PO will be created.
1263 :param browse_report company: the company to which the new PO will belong to.
1264 :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1266 :return: the desired Order Date for the PO
1268 seller_delay = int(procurement.product_id.seller_delay)
1269 return schedule_date - relativedelta(days=seller_delay)
1271 def _get_product_supplier(self, cr, uid, procurement, context=None):
1272 ''' returns the main supplier of the procurement's product given as argument'''
1273 return procurement.product_id.seller_id
1275 def _get_po_line_values_from_proc(self, cr, uid, procurement, partner, company, schedule_date, context=None):
1278 uom_obj = self.pool.get('product.uom')
1279 pricelist_obj = self.pool.get('product.pricelist')
1280 prod_obj = self.pool.get('product.product')
1281 acc_pos_obj = self.pool.get('account.fiscal.position')
1283 seller_qty = procurement.product_id.seller_qty
1284 pricelist_id = partner.property_product_pricelist_purchase.id
1285 uom_id = procurement.product_id.uom_po_id.id
1286 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1288 qty = max(qty, seller_qty)
1289 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner.id, {'uom': uom_id})[pricelist_id]
1291 #Passing partner_id to context for purchase order line integrity of Line name
1292 new_context = context.copy()
1293 new_context.update({'lang': partner.lang, 'partner_id': partner.id})
1294 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
1295 taxes_ids = procurement.product_id.supplier_taxes_id
1296 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1297 name = product.display_name
1298 if product.description_purchase:
1299 name += '\n' + product.description_purchase
1304 'product_id': procurement.product_id.id,
1305 'product_uom': uom_id,
1306 'price_unit': price or 0.0,
1307 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1308 'taxes_id': [(6, 0, taxes)],
1311 def make_po(self, cr, uid, ids, context=None):
1312 """ Resolve the purchase from procurement, which may result in a new PO creation, a new PO line creation or a quantity change on existing PO line.
1313 Note that some operations (as the PO creation) are made as SUPERUSER because the current user may not have rights to do it (mto product launched by a sale for example)
1315 @return: dictionary giving for each procurement its related resolving PO line.
1318 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1319 po_obj = self.pool.get('purchase.order')
1320 po_line_obj = self.pool.get('purchase.order.line')
1321 seq_obj = self.pool.get('ir.sequence')
1324 sum_po_line_ids = []
1325 for procurement in self.browse(cr, uid, ids, context=context):
1326 partner = self._get_product_supplier(cr, uid, procurement, context=context)
1328 self.message_post(cr, uid, [procurement.id], _('There is no supplier associated to product %s') % (procurement.product_id.name))
1329 res[procurement.id] = False
1331 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1332 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1333 line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context)
1334 #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one
1335 available_draft_po_ids = po_obj.search(cr, uid, [
1336 ('partner_id', '=', partner.id), ('state', '=', 'draft'), ('picking_type_id', '=', procurement.rule_id.picking_type_id.id),
1337 ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context)
1338 if available_draft_po_ids:
1339 po_id = available_draft_po_ids[0]
1340 po_rec = po_obj.browse(cr, uid, po_id, context=context)
1341 #if the product has to be ordered earlier those in the existing PO, we replace the purchase date on the order to avoid ordering it too late
1342 if datetime.strptime(po_rec.date_order, DEFAULT_SERVER_DATETIME_FORMAT) > purchase_date:
1343 po_obj.write(cr, uid, [po_id], {'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
1344 #look for any other PO line in the selected PO with same product and UoM to sum quantities instead of creating a new po line
1345 available_po_line_ids = po_line_obj.search(cr, uid, [('order_id', '=', po_id), ('product_id', '=', line_vals['product_id']), ('product_uom', '=', line_vals['product_uom'])], context=context)
1346 if available_po_line_ids:
1347 po_line = po_line_obj.browse(cr, uid, available_po_line_ids[0], context=context)
1348 po_line_obj.write(cr, SUPERUSER_ID, po_line.id, {'product_qty': po_line.product_qty + line_vals['product_qty']}, context=context)
1349 po_line_id = po_line.id
1350 sum_po_line_ids.append(procurement.id)
1352 line_vals.update(order_id=po_id)
1353 po_line_id = po_line_obj.create(cr, SUPERUSER_ID, line_vals, context=context)
1354 linked_po_ids.append(procurement.id)
1356 name = seq_obj.next_by_code(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1359 'origin': procurement.origin,
1360 'partner_id': partner.id,
1361 'location_id': procurement.location_id.id,
1362 'picking_type_id': procurement.rule_id.picking_type_id.id,
1363 'pricelist_id': partner.property_product_pricelist_purchase.id,
1364 'currency_id': partner.property_product_pricelist_purchase and partner.property_product_pricelist_purchase.currency_id.id or procurement.company_id.currency_id.id,
1365 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1366 'company_id': procurement.company_id.id,
1367 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False,
1368 'payment_term_id': partner.property_supplier_payment_term.id or False,
1369 'dest_address_id': procurement.partner_dest_id.id,
1371 po_id = self.create_procurement_purchase_order(cr, SUPERUSER_ID, procurement, po_vals, line_vals, context=context)
1372 po_line_id = po_obj.browse(cr, uid, po_id, context=context).order_line[0].id
1373 pass_ids.append(procurement.id)
1374 res[procurement.id] = po_line_id
1375 self.write(cr, uid, [procurement.id], {'purchase_line_id': po_line_id}, context=context)
1377 self.message_post(cr, uid, pass_ids, body=_("Draft Purchase Order created"), context=context)
1379 self.message_post(cr, uid, linked_po_ids, body=_("Purchase line created and linked to an existing Purchase Order"), context=context)
1381 self.message_post(cr, uid, sum_po_line_ids, body=_("Quantity added in existing Purchase Order Line"), context=context)
1385 class mail_mail(osv.Model):
1387 _inherit = 'mail.mail'
1389 def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
1390 if mail_sent and mail.model == 'purchase.order':
1391 obj = self.pool.get('purchase.order').browse(cr, uid, mail.res_id, context=context)
1392 if obj.state == 'draft':
1393 self.pool.get('purchase.order').signal_workflow(cr, uid, [mail.res_id], 'send_rfq')
1394 return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
1397 class product_template(osv.Model):
1398 _name = 'product.template'
1399 _inherit = 'product.template'
1401 def _get_buy_route(self, cr, uid, context=None):
1403 buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy')
1408 def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
1409 res = dict.fromkeys(ids, 0)
1410 for template in self.browse(cr, uid, ids, context=context):
1411 res[template.id] = sum([p.purchase_count for p in template.product_variant_ids])
1415 'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
1416 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
1421 'route_ids': _get_buy_route,
1424 def action_view_purchases(self, cr, uid, ids, context=None):
1425 products = self._get_products(cr, uid, ids, context=context)
1426 result = self._get_act_window_dict(cr, uid, 'purchase.action_purchase_line_product_tree', context=context)
1427 result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
1430 class product_product(osv.Model):
1431 _name = 'product.product'
1432 _inherit = 'product.product'
1434 def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
1435 Purchase = self.pool['purchase.order']
1437 product_id: Purchase.search_count(cr,uid, [('order_line.product_id', '=', product_id)], context=context)
1438 for product_id in ids
1442 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
1447 class mail_compose_message(osv.Model):
1448 _inherit = 'mail.compose.message'
1450 def send_mail(self, cr, uid, ids, context=None):
1451 context = context or {}
1452 if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
1453 context = dict(context, mail_post_autofollow=True)
1454 self.pool.get('purchase.order').signal_workflow(cr, uid, [context['default_res_id']], 'send_rfq')
1455 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1458 class account_invoice(osv.Model):
1459 """ Override account_invoice to add Chatter messages on the related purchase
1460 orders, logging the invoice receipt or payment. """
1461 _inherit = 'account.invoice'
1463 def invoice_validate(self, cr, uid, ids, context=None):
1464 res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context)
1465 purchase_order_obj = self.pool.get('purchase.order')
1466 # read access on purchase.order object is not required
1467 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1468 user_id = SUPERUSER_ID
1471 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1472 for order in purchase_order_obj.browse(cr, uid, po_ids, context=context):
1473 purchase_order_obj.message_post(cr, user_id, order.id, body=_("Invoice received"), context=context)
1475 for po_line in order.order_line:
1476 if any(line.invoice_id.state not in ['draft', 'cancel'] for line in po_line.invoice_lines):
1477 invoiced.append(po_line.id)
1479 self.pool['purchase.order.line'].write(cr, uid, invoiced, {'invoiced': True})
1480 workflow.trg_write(uid, 'purchase.order', order.id, cr)
1483 def confirm_paid(self, cr, uid, ids, context=None):
1484 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1485 purchase_order_obj = self.pool.get('purchase.order')
1486 # read access on purchase.order object is not required
1487 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1488 user_id = SUPERUSER_ID
1491 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1492 for po_id in po_ids:
1493 purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice paid"), context=context)
1496 class account_invoice_line(osv.Model):
1497 """ Override account_invoice_line to add the link to the purchase order line it is related to"""
1498 _inherit = 'account.invoice.line'
1500 'purchase_line_id': fields.many2one('purchase.order.line',
1501 'Purchase Order Line', ondelete='set null', select=True,
1506 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: