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 pick_type = obj_data.get_object_reference(cr, uid, 'stock', 'picking_type_in') and obj_data.get_object_reference(cr, uid, 'stock', 'picking_type_in')[1] or False
158 type = type_obj.browse(cr, uid, pick_type, context=context)
159 if type and type.warehouse_id and type.warehouse_id.company_id.id == company_id:
161 types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)], context=context)
163 types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id', '=', False)], context=context)
165 raise osv.except_osv(_('Error!'), _("Make sure you have at least an incoming picking type defined"))
168 def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
173 SELECT picking_id, po.id FROM stock_picking p, stock_move m, purchase_order_line pol, purchase_order po
174 WHERE po.id in %s and po.id = pol.order_id and pol.id = m.purchase_line_id and m.picking_id = p.id
175 GROUP BY picking_id, po.id
178 cr.execute(query, (tuple(ids), ))
179 picks = cr.fetchall()
180 for pick_id, po_id in picks:
181 res[po_id].append(pick_id)
184 def _count_all(self, cr, uid, ids, field_name, arg, context=None):
187 'shipment_count': len(purchase.picking_ids),
188 'invoice_count': len(purchase.invoice_ids),
190 for purchase in self.browse(cr, uid, ids, context=context)
194 ('draft', 'Draft PO'),
196 ('bid', 'Bid Received'),
197 ('confirmed', 'Waiting Approval'),
198 ('approved', 'Purchase Confirmed'),
199 ('except_picking', 'Shipping Exception'),
200 ('except_invoice', 'Invoice Exception'),
202 ('cancel', 'Cancelled')
206 'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirmed',
207 'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'approved',
208 'purchase.mt_rfq_done': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
212 'name': fields.char('Order Reference', required=True, select=True, copy=False,
213 help="Unique number of the purchase order, "
214 "computed automatically when the purchase order is created."),
215 'origin': fields.char('Source Document', copy=False,
216 help="Reference of the document that generated this purchase order "
217 "request; a sales order or an internal procurement request."),
218 'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)],
219 'approved':[('readonly',True)],
220 'done':[('readonly',True)]},
222 help="Reference of the sales order or bid sent by your supplier. "
223 "It's mainly used to do the matching when you receive the "
224 "products as this reference is usually written on the "
225 "delivery order sent by your supplier."),
226 'date_order':fields.datetime('Order Date', required=True, states={'confirmed':[('readonly',True)],
227 'approved':[('readonly',True)]},
228 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.",
230 'date_approve':fields.date('Date Approved', readonly=1, select=True, copy=False,
231 help="Date on which purchase order has been approved"),
232 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
233 change_default=True, track_visibility='always'),
234 'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
235 states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
236 help="Put an address if you want to deliver directly from the supplier to the customer. " \
237 "Otherwise, keep empty to deliver to your own company."
239 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]} ),
240 '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."),
241 'currency_id': fields.many2one('res.currency','Currency', readonly=True, required=True,states={'draft': [('readonly', False)],'sent': [('readonly', False)]}),
242 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True,
243 help="The status of the purchase order or the quotation request. "
244 "A request for quotation is a purchase order in a 'Draft' status. "
245 "Then the order has to be confirmed by the user, the status switch "
246 "to 'Confirmed'. Then the supplier must confirm the order to change "
247 "the status to 'Approved'. When the purchase order is paid and "
248 "received, the status becomes 'Done'. If a cancel action occurs in "
249 "the invoice or in the receipt of goods, the status becomes "
251 select=True, copy=False),
252 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines',
253 states={'approved':[('readonly',True)],
254 'done':[('readonly',True)]},
256 'validator' : fields.many2one('res.users', 'Validated by', readonly=True, copy=False),
257 'notes': fields.text('Terms and Conditions'),
258 'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id',
259 'invoice_id', 'Invoices', copy=False,
260 help="Invoices generated for a purchase order"),
261 '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."),
262 'shipped':fields.boolean('Received', readonly=True, select=True, copy=False,
263 help="It indicates that a picking has been done"),
264 'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'),
265 'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', copy=False,
266 help="It indicates that an invoice has been validated"),
267 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
268 '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,
269 readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)]},
270 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" \
271 "Based on generated invoice: create a draft invoice you can validate later.\n" \
272 "Based on incoming shipments: let you create an invoice when receipts are validated."
274 '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.",
276 'purchase.order.line': (_get_order, ['date_planned'], 10),
279 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
281 'purchase.order.line': (_get_order, None, 10),
282 }, multi="sums", help="The amount without tax", track_visibility='always'),
283 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
285 'purchase.order.line': (_get_order, None, 10),
286 }, multi="sums", help="The tax amount"),
287 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
289 'purchase.order.line': (_get_order, None, 10),
290 }, multi="sums", help="The total amount"),
291 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
292 'payment_term_id': fields.many2one('account.payment.term', 'Payment Term'),
293 'incoterm_id': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
294 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
295 'create_uid': fields.many2one('res.users', 'Responsible'),
296 'company_id': fields.many2one('res.company', 'Company', required=True, select=1, states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)]}),
297 'journal_id': fields.many2one('account.journal', 'Journal'),
298 'bid_date': fields.date('Bid Received On', readonly=True, help="Date on which the bid was received"),
299 'bid_validity': fields.date('Bid Valid Until', help="Date on which the bid expired"),
300 'picking_type_id': fields.many2one('stock.picking.type', 'Deliver To', help="This will determine picking type of incoming shipment", required=True,
301 states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}),
302 'related_location_id': fields.related('picking_type_id', 'default_location_dest_id', type='many2one', relation='stock.location', string="Related location", store=True),
303 'shipment_count': fields.function(_count_all, type='integer', string='Incoming Shipments', multi=True),
304 'invoice_count': fields.function(_count_all, type='integer', string='Invoices', multi=True)
307 'date_order': fields.datetime.now,
309 'name': lambda obj, cr, uid, context: '/',
311 'invoice_method': 'order',
313 '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,
314 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
315 'journal_id': _get_journal,
316 'currency_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id,
317 'picking_type_id': _get_picking_in,
320 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
322 _name = "purchase.order"
323 _inherit = ['mail.thread', 'ir.needaction_mixin']
324 _description = "Purchase Order"
325 _order = 'date_order desc, id desc'
327 def create(self, cr, uid, vals, context=None):
328 if vals.get('name','/')=='/':
329 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'purchase.order') or '/'
330 context = dict(context or {}, mail_create_nolog=True)
331 order = super(purchase_order, self).create(cr, uid, vals, context=context)
332 self.message_post(cr, uid, [order], body=_("RFQ created"), context=context)
335 def unlink(self, cr, uid, ids, context=None):
336 purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
338 for s in purchase_orders:
339 if s['state'] in ['draft','cancel']:
340 unlink_ids.append(s['id'])
342 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.'))
344 # automatically sending subflow.delete upon deletion
345 self.signal_workflow(cr, uid, unlink_ids, 'purchase_cancel')
347 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
349 def set_order_line_status(self, cr, uid, ids, status, context=None):
350 line = self.pool.get('purchase.order.line')
352 proc_obj = self.pool.get('procurement.order')
353 for order in self.browse(cr, uid, ids, context=context):
354 order_line_ids += [po_line.id for po_line in order.order_line]
356 line.write(cr, uid, order_line_ids, {'state': status}, context=context)
357 if order_line_ids and status == 'cancel':
358 procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', order_line_ids)], context=context)
360 proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
363 def button_dummy(self, cr, uid, ids, context=None):
366 def onchange_pricelist(self, cr, uid, ids, pricelist_id, context=None):
369 return {'value': {'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id}}
371 #Destination address is used when dropshipping
372 def onchange_dest_address_id(self, cr, uid, ids, address_id, context=None):
375 address = self.pool.get('res.partner')
377 supplier = address.browse(cr, uid, address_id, context=context)
379 location_id = supplier.property_stock_customer.id
380 values.update({'location_id': location_id})
381 return {'value':values}
383 def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None):
386 picktype = self.pool.get("stock.picking.type").browse(cr, uid, picking_type_id, context=context)
387 if picktype.default_location_dest_id:
388 value.update({'location_id': picktype.default_location_dest_id.id})
389 value.update({'related_location_id': picktype.default_location_dest_id and picktype.default_location_dest_id.id or False})
390 return {'value': value}
392 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
393 partner = self.pool.get('res.partner')
396 'fiscal_position': False,
397 'payment_term_id': False,
399 supplier_address = partner.address_get(cr, uid, [partner_id], ['default'], context=context)
400 supplier = partner.browse(cr, uid, partner_id, context=context)
402 'pricelist_id': supplier.property_product_pricelist_purchase.id,
403 'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
404 'payment_term_id': supplier.property_supplier_payment_term.id or False,
407 def invoice_open(self, cr, uid, ids, context=None):
408 mod_obj = self.pool.get('ir.model.data')
409 act_obj = self.pool.get('ir.actions.act_window')
411 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree2')
412 id = result and result[1] or False
413 result = act_obj.read(cr, uid, [id], context=context)[0]
415 for po in self.browse(cr, uid, ids, context=context):
416 inv_ids+= [invoice.id for invoice in po.invoice_ids]
418 raise osv.except_osv(_('Error!'), _('Please create Invoices.'))
419 #choose the view_mode accordingly
421 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
423 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
424 result['views'] = [(res and res[1] or False, 'form')]
425 result['res_id'] = inv_ids and inv_ids[0] or False
428 def view_invoice(self, cr, uid, ids, context=None):
430 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.
432 context = dict(context or {})
433 mod_obj = self.pool.get('ir.model.data')
434 wizard_obj = self.pool.get('purchase.order.line_invoice')
435 #compute the number of invoices to display
437 for po in self.browse(cr, uid, ids, context=context):
438 if po.invoice_method == 'manual':
439 if not po.invoice_ids:
440 context.update({'active_ids' : [line.id for line in po.order_line]})
441 wizard_obj.makeInvoices(cr, uid, [], context=context)
443 for po in self.browse(cr, uid, ids, context=context):
444 inv_ids+= [invoice.id for invoice in po.invoice_ids]
445 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
446 res_id = res and res[1] or False
449 'name': _('Supplier Invoices'),
453 'res_model': 'account.invoice',
454 'context': "{'type':'in_invoice', 'journal_type': 'purchase'}",
455 'type': 'ir.actions.act_window',
458 'res_id': inv_ids and inv_ids[0] or False,
461 def view_picking(self, cr, uid, ids, context=None):
463 This function returns an action that display existing picking orders of given purchase order ids.
467 mod_obj = self.pool.get('ir.model.data')
468 dummy, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree'))
469 action = self.pool.get('ir.actions.act_window').read(cr, uid, action_id, context=context)
472 for po in self.browse(cr, uid, ids, context=context):
473 pick_ids += [picking.id for picking in po.picking_ids]
475 #override the context to get rid of the default filtering on picking type
476 action['context'] = {}
477 #choose the view_mode accordingly
478 if len(pick_ids) > 1:
479 action['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
481 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
482 action['views'] = [(res and res[1] or False, 'form')]
483 action['res_id'] = pick_ids and pick_ids[0] or False
486 def wkf_approve_order(self, cr, uid, ids, context=None):
487 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)})
490 def wkf_bid_received(self, cr, uid, ids, context=None):
491 return self.write(cr, uid, ids, {'state':'bid', 'bid_date': fields.date.context_today(self,cr,uid,context=context)})
493 def wkf_send_rfq(self, cr, uid, ids, context=None):
495 This function opens a window to compose an email, with the edi purchase template message loaded by default
499 ir_model_data = self.pool.get('ir.model.data')
501 if context.get('send_rfq', False):
502 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
504 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase_done')[1]
508 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
510 compose_form_id = False
513 'default_model': 'purchase.order',
514 'default_res_id': ids[0],
515 'default_use_template': bool(template_id),
516 'default_template_id': template_id,
517 'default_composition_mode': 'comment',
520 'name': _('Compose Email'),
521 'type': 'ir.actions.act_window',
524 'res_model': 'mail.compose.message',
525 'views': [(compose_form_id, 'form')],
526 'view_id': compose_form_id,
531 def print_quotation(self, cr, uid, ids, context=None):
533 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
535 assert len(ids) == 1, 'This option should only be used for a single id at a time'
536 self.signal_workflow(cr, uid, ids, 'send_rfq')
537 return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
539 def wkf_confirm_order(self, cr, uid, ids, context=None):
541 for po in self.browse(cr, uid, ids, context=context):
542 if not po.order_line:
543 raise osv.except_osv(_('Error!'),_('You cannot confirm a purchase order without any purchase order line.'))
544 for line in po.order_line:
545 if line.state=='draft':
547 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
549 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
552 def _choose_account_from_po_line(self, cr, uid, po_line, context=None):
553 fiscal_obj = self.pool.get('account.fiscal.position')
554 property_obj = self.pool.get('ir.property')
555 if po_line.product_id:
556 acc_id = po_line.product_id.property_account_expense.id
558 acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
560 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,))
562 acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context=context).id
563 fpos = po_line.order_id.fiscal_position or False
564 return fiscal_obj.map_account(cr, uid, fpos, acc_id)
566 def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
567 """Collects require data from purchase order line that is used to create invoice line
568 for that purchase order line
569 :param account_id: Expense account of the product of PO line if any.
570 :param browse_record order_line: Purchase order line browse record
571 :return: Value for fields of invoice lines.
575 'name': order_line.name,
576 'account_id': account_id,
577 'price_unit': order_line.price_unit or 0.0,
578 'quantity': order_line.product_qty,
579 'product_id': order_line.product_id.id or False,
580 'uos_id': order_line.product_uom.id or False,
581 'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
582 'account_analytic_id': order_line.account_analytic_id.id or False,
583 'purchase_line_id': order_line.id,
586 def _prepare_invoice(self, cr, uid, order, line_ids, context=None):
587 """Prepare the dict of values to create the new invoice for a
588 purchase order. This method may be overridden to implement custom
589 invoice generation (making sure to call super() to establish
590 a clean extension chain).
592 :param browse_record order: purchase.order record to invoice
593 :param list(int) line_ids: list of invoice line IDs that must be
594 attached to the invoice
595 :return: dict of value to create() the invoice
597 journal_ids = self.pool['account.journal'].search(
598 cr, uid, [('type', '=', 'purchase'),
599 ('company_id', '=', order.company_id.id)],
602 raise osv.except_osv(
604 _('Define purchase journal for this company: "%s" (id:%d).') % \
605 (order.company_id.name, order.company_id.id))
607 'name': order.partner_ref or order.name,
608 'reference': order.partner_ref or order.name,
609 'account_id': order.partner_id.property_account_payable.id,
610 'type': 'in_invoice',
611 'partner_id': order.partner_id.id,
612 'currency_id': order.currency_id.id,
613 'journal_id': len(journal_ids) and journal_ids[0] or False,
614 'invoice_line': [(6, 0, line_ids)],
615 'origin': order.name,
616 'fiscal_position': order.fiscal_position.id or False,
617 'payment_term': order.payment_term_id.id or False,
618 'company_id': order.company_id.id,
621 def action_cancel_draft(self, cr, uid, ids, context=None):
624 self.write(cr, uid, ids, {'state':'draft','shipped':0})
625 self.set_order_line_status(cr, uid, ids, 'draft', context=context)
627 # Deleting the existing instance of workflow for PO
628 self.delete_workflow(cr, uid, [p_id]) # TODO is it necessary to interleave the calls?
629 self.create_workflow(cr, uid, [p_id])
632 def wkf_po_done(self, cr, uid, ids, context=None):
633 self.write(cr, uid, ids, {'state': 'done'}, context=context)
634 self.set_order_line_status(cr, uid, ids, 'done', context=context)
636 def action_invoice_create(self, cr, uid, ids, context=None):
637 """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
638 :param ids: list of ids of purchase orders.
639 :return: ID of created invoice.
642 context = dict(context or {})
644 inv_obj = self.pool.get('account.invoice')
645 inv_line_obj = self.pool.get('account.invoice.line')
648 uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
649 for order in self.browse(cr, uid, ids, context=context):
650 context.pop('force_company', None)
651 if order.company_id.id != uid_company_id:
652 #if the company of the document is different than the current user company, force the company in the context
653 #then re-do a browse to read the property fields for the good company.
654 context['force_company'] = order.company_id.id
655 order = self.browse(cr, uid, order.id, context=context)
657 # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
659 for po_line in order.order_line:
660 acc_id = self._choose_account_from_po_line(cr, uid, po_line, context=context)
661 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
662 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
663 inv_lines.append(inv_line_id)
664 po_line.write({'invoice_lines': [(4, inv_line_id)]})
666 # get invoice data and create invoice
667 inv_data = self._prepare_invoice(cr, uid, order, inv_lines, context=context)
668 inv_id = inv_obj.create(cr, uid, inv_data, context=context)
670 # compute the invoice
671 inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
673 # Link this new invoice to related purchase order
674 order.write({'invoice_ids': [(4, inv_id)]})
678 def invoice_done(self, cr, uid, ids, context=None):
679 self.write(cr, uid, ids, {'state': 'approved'}, context=context)
682 def has_stockable_product(self, cr, uid, ids, *args):
683 for order in self.browse(cr, uid, ids):
684 for order_line in order.order_line:
685 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
689 def wkf_action_cancel(self, cr, uid, ids, context=None):
690 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
691 self.set_order_line_status(cr, uid, ids, 'cancel', context=context)
693 def action_cancel(self, cr, uid, ids, context=None):
694 for purchase in self.browse(cr, uid, ids, context=context):
695 for pick in purchase.picking_ids:
696 for move in pick.move_lines:
697 if pick.state == 'done':
698 raise osv.except_osv(
699 _('Unable to cancel the purchase order %s.') % (purchase.name),
700 _('You have already received some goods for it. '))
701 self.pool.get('stock.picking').action_cancel(cr, uid, [x.id for x in purchase.picking_ids if x.state != 'cancel'], context=context)
702 for inv in purchase.invoice_ids:
703 if inv and inv.state not in ('cancel', 'draft'):
704 raise osv.except_osv(
705 _('Unable to cancel this purchase order.'),
706 _('You must first cancel all invoices related to this purchase order.'))
707 self.pool.get('account.invoice') \
708 .signal_workflow(cr, uid, map(attrgetter('id'), purchase.invoice_ids), 'invoice_cancel')
709 self.signal_workflow(cr, uid, ids, 'purchase_cancel')
712 def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
713 ''' 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()'''
714 product_uom = self.pool.get('product.uom')
715 price_unit = order_line.price_unit
716 if order_line.product_uom.id != order_line.product_id.uom_id.id:
717 price_unit *= order_line.product_uom.factor / order_line.product_id.uom_id.factor
718 if order.currency_id.id != order.company_id.currency_id.id:
719 #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency
720 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)
723 'name': order_line.name or '',
724 'product_id': order_line.product_id.id,
725 'product_uom': order_line.product_uom.id,
726 'product_uos': order_line.product_uom.id,
727 'date': order.date_order,
728 'date_expected': fields.date.date_to_datetime(self, cr, uid, order_line.date_planned, context),
729 'location_id': order.partner_id.property_stock_supplier.id,
730 'location_dest_id': order.location_id.id,
731 'picking_id': picking_id,
732 'partner_id': order.dest_address_id.id or order.partner_id.id,
733 'move_dest_id': False,
735 'purchase_line_id': order_line.id,
736 'company_id': order.company_id.id,
737 'price_unit': price_unit,
738 'picking_type_id': order.picking_type_id.id,
739 'group_id': group_id,
740 'procurement_id': False,
741 'origin': order.name,
742 '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 [],
743 'warehouse_id':order.picking_type_id.warehouse_id.id,
744 'invoice_state': order.invoice_method == 'picking' and '2binvoiced' or 'none',
747 diff_quantity = order_line.product_qty
748 for procurement in order_line.procurement_ids:
749 procurement_qty = product_uom._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, to_uom_id=order_line.product_uom.id)
750 tmp = move_template.copy()
752 'product_uom_qty': min(procurement_qty, diff_quantity),
753 'product_uos_qty': min(procurement_qty, diff_quantity),
754 'move_dest_id': procurement.move_dest_id.id, #move destination is same as procurement destination
755 'group_id': procurement.group_id.id or group_id, #move group is same as group of procurements if it exists, otherwise take another group
756 'procurement_id': procurement.id,
757 '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
758 'propagate': procurement.rule_id.propagate,
760 diff_quantity -= min(procurement_qty, diff_quantity)
762 #if the order line has a bigger quantity than the procurement it was for (manually changed or minimal quantity), then
763 #split the future stock move in two because the route followed may be different.
764 if diff_quantity > 0:
765 move_template['product_uom_qty'] = diff_quantity
766 move_template['product_uos_qty'] = diff_quantity
767 res.append(move_template)
770 def _create_stock_moves(self, cr, uid, order, order_lines, picking_id=False, context=None):
771 """Creates appropriate stock moves for given order lines, whose can optionally create a
772 picking if none is given or no suitable is found, then confirms the moves, makes them
773 available, and confirms the pickings.
775 If ``picking_id`` is provided, the stock moves will be added to it, otherwise a standard
776 incoming picking will be created to wrap the stock moves (default behavior of the stock.move)
778 Modules that wish to customize the procurements or partition the stock moves over
779 multiple stock pickings may override this method and call ``super()`` with
780 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
782 :param browse_record order: purchase order to which the order lines belong
783 :param list(browse_record) order_lines: purchase order line records for which picking
784 and moves should be created.
785 :param int picking_id: optional ID of a stock picking to which the created stock moves
786 will be added. A new picking will be created if omitted.
789 stock_move = self.pool.get('stock.move')
791 new_group = self.pool.get("procurement.group").create(cr, uid, {'name': order.name, 'partner_id': order.partner_id.id}, context=context)
793 for order_line in order_lines:
794 if not order_line.product_id:
797 if order_line.product_id.type in ('product', 'consu'):
798 for vals in self._prepare_order_line_move(cr, uid, order, order_line, picking_id, new_group, context=context):
799 move = stock_move.create(cr, uid, vals, context=context)
800 todo_moves.append(move)
802 todo_moves = stock_move.action_confirm(cr, uid, todo_moves)
803 stock_move.force_assign(cr, uid, todo_moves)
805 def test_moves_done(self, cr, uid, ids, context=None):
806 '''PO is done at the delivery side if all the incoming shipments are done'''
807 for purchase in self.browse(cr, uid, ids, context=context):
808 for picking in purchase.picking_ids:
809 if picking.state != 'done':
813 def test_moves_except(self, cr, uid, ids, context=None):
814 ''' PO is in exception at the delivery side if one of the picking is canceled
815 and the other pickings are completed (done or canceled)
817 at_least_one_canceled = False
818 alldoneorcancel = True
819 for purchase in self.browse(cr, uid, ids, context=context):
820 for picking in purchase.picking_ids:
821 if picking.state == 'cancel':
822 at_least_one_canceled = True
823 if picking.state not in ['done', 'cancel']:
824 alldoneorcancel = False
825 return at_least_one_canceled and alldoneorcancel
827 def move_lines_get(self, cr, uid, ids, *args):
829 for order in self.browse(cr, uid, ids, context={}):
830 for line in order.order_line:
831 res += [x.id for x in line.move_ids]
834 def action_picking_create(self, cr, uid, ids, context=None):
835 for order in self.browse(cr, uid, ids):
837 'picking_type_id': order.picking_type_id.id,
838 'partner_id': order.dest_address_id.id or order.partner_id.id,
839 'date': max([l.date_planned for l in order.order_line]),
842 picking_id = self.pool.get('stock.picking').create(cr, uid, picking_vals, context=context)
843 self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
845 def picking_done(self, cr, uid, ids, context=None):
846 self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
847 # Do check on related procurements:
848 proc_obj = self.pool.get("procurement.order")
850 for po in self.browse(cr, uid, ids, context=context):
851 po_lines += [x.id for x in po.order_line]
853 procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', po_lines)], context=context)
855 proc_obj.check(cr, uid, procs, context=context)
856 self.message_post(cr, uid, ids, body=_("Products received"), context=context)
859 def do_merge(self, cr, uid, ids, context=None):
861 To merge similar type of purchase orders.
862 Orders will only be merged if:
863 * Purchase Orders are in draft
864 * Purchase Orders belong to the same partner
865 * Purchase Orders are have same stock location, same pricelist
866 Lines will only be merged if:
867 * Order lines are exactly the same except for the quantity and unit
869 @param self: The object pointer.
870 @param cr: A database cursor
871 @param uid: ID of the user currently logged in
872 @param ids: the ID or list of IDs
873 @param context: A standard dictionary
875 @return: new purchase order id
878 #TOFIX: merged order line should be unlink
879 def make_key(br, fields):
882 field_val = getattr(br, field)
883 if field in ('product_id', 'account_analytic_id'):
886 if isinstance(field_val, browse_record):
887 field_val = field_val.id
888 elif isinstance(field_val, browse_null):
890 elif isinstance(field_val, browse_record_list):
891 field_val = ((6, 0, tuple([v.id for v in field_val])),)
892 list_key.append((field, field_val))
894 return tuple(list_key)
896 context = dict(context or {})
898 # Compute what the new orders should contain
901 order_lines_to_move = []
902 for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
903 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
904 new_order = new_orders.setdefault(order_key, ({}, []))
905 new_order[1].append(porder.id)
906 order_infos = new_order[0]
910 'origin': porder.origin,
911 'date_order': porder.date_order,
912 'partner_id': porder.partner_id.id,
913 'dest_address_id': porder.dest_address_id.id,
914 'picking_type_id': porder.picking_type_id.id,
915 'location_id': porder.location_id.id,
916 'pricelist_id': porder.pricelist_id.id,
919 'notes': '%s' % (porder.notes or '',),
920 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
923 if porder.date_order < order_infos['date_order']:
924 order_infos['date_order'] = porder.date_order
926 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
928 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
930 for order_line in porder.order_line:
931 order_lines_to_move += [order_line.id]
935 for order_key, (order_data, old_ids) in new_orders.iteritems():
936 # skip merges with only one order
938 allorders += (old_ids or [])
941 # cleanup order line data
942 for key, value in order_data['order_line'].iteritems():
943 del value['uom_factor']
944 value.update(dict(key))
945 order_data['order_line'] = [(6, 0, order_lines_to_move)]
947 # create the new order
948 context.update({'mail_create_nolog': True})
949 neworder_id = self.create(cr, uid, order_data)
950 self.message_post(cr, uid, [neworder_id], body=_("RFQ created"), context=context)
951 orders_info.update({neworder_id: old_ids})
952 allorders.append(neworder_id)
954 # make triggers pointing to the old orders point to the new order
955 for old_id in old_ids:
956 self.redirect_workflow(cr, uid, [(old_id, neworder_id)])
957 self.signal_workflow(cr, uid, [old_id], 'purchase_cancel')
962 class purchase_order_line(osv.osv):
963 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
965 cur_obj=self.pool.get('res.currency')
966 tax_obj = self.pool.get('account.tax')
967 for line in self.browse(cr, uid, ids, context=context):
968 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, line.order_id.partner_id)
969 cur = line.order_id.pricelist_id.currency_id
970 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
973 def _get_uom_id(self, cr, uid, context=None):
975 proxy = self.pool.get('ir.model.data')
976 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
978 except Exception, ex:
982 'name': fields.text('Description', required=True),
983 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
984 'date_planned': fields.date('Scheduled Date', required=True, select=True),
985 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
986 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
987 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
988 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
989 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
990 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
991 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
992 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
993 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
994 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')],
995 'Status', required=True, readonly=True, copy=False,
996 help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
997 \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
998 \n* The \'Done\' status is set automatically when purchase order is set as done. \
999 \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
1000 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel',
1001 'order_line_id', 'invoice_id', 'Invoice Lines',
1002 readonly=True, copy=False),
1003 'invoiced': fields.boolean('Invoiced', readonly=True, copy=False),
1004 'partner_id': fields.related('order_id', 'partner_id', string='Partner', readonly=True, type="many2one", relation="res.partner", store=True),
1005 'date_order': fields.related('order_id', 'date_order', string='Order Date', readonly=True, type="datetime"),
1006 'procurement_ids': fields.one2many('procurement.order', 'purchase_line_id', string='Associated procurements'),
1009 'product_uom' : _get_uom_id,
1010 'product_qty': lambda *a: 1.0,
1011 'state': lambda *args: 'draft',
1012 'invoiced': lambda *a: 0,
1014 _table = 'purchase_order_line'
1015 _name = 'purchase.order.line'
1016 _description = 'Purchase Order Line'
1018 def unlink(self, cr, uid, ids, context=None):
1019 for line in self.browse(cr, uid, ids, context=context):
1020 if line.state not in ['draft', 'cancel']:
1021 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
1022 procurement_obj = self.pool.get('procurement.order')
1023 procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context)
1024 if procurement_ids_to_cancel:
1025 self.pool['procurement.order'].cancel(cr, uid, procurement_ids_to_cancel)
1026 return super(purchase_order_line, self).unlink(cr, uid, ids, context=context)
1028 def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1029 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1030 name=False, price_unit=False, state='draft', context=None):
1032 onchange handler of product_uom.
1037 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1038 context = dict(context, purchase_uom_check=True)
1039 return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1040 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
1041 name=name, price_unit=price_unit, state=state, context=context)
1043 def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
1044 """Return the datetime value to use as Schedule Date (``date_planned``) for
1045 PO Lines that correspond to the given product.supplierinfo,
1046 when ordered at `date_order_str`.
1048 :param browse_record | False supplier_info: product.supplierinfo, used to
1049 determine delivery delay (if False, default delay = 0)
1050 :param str date_order_str: date of order field, as a string in
1051 DEFAULT_SERVER_DATETIME_FORMAT
1053 :return: desired Schedule Date for the PO line
1055 supplier_delay = int(supplier_info.delay) if supplier_info else 0
1056 return datetime.strptime(date_order_str, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=supplier_delay)
1058 def action_cancel(self, cr, uid, ids, context=None):
1059 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1060 for po_line in self.browse(cr, uid, ids, context=context):
1061 if all([l.state == 'cancel' for l in po_line.order_id.order_line]):
1062 self.pool.get('purchase.order').action_cancel(cr, uid, [po_line.order_id.id], context=context)
1064 def _check_product_uom_group(self, cr, uid, context=None):
1065 group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom')
1066 res = [user for user in group_uom.users if user.id == uid]
1067 return len(res) and True or False
1070 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1071 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1072 name=False, price_unit=False, state='draft', context=None):
1074 onchange handler of product_id.
1079 res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1083 product_product = self.pool.get('product.product')
1084 product_uom = self.pool.get('product.uom')
1085 res_partner = self.pool.get('res.partner')
1086 product_pricelist = self.pool.get('product.pricelist')
1087 account_fiscal_position = self.pool.get('account.fiscal.position')
1088 account_tax = self.pool.get('account.tax')
1090 # - check for the presence of partner_id and pricelist_id
1092 # raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
1093 #if not pricelist_id:
1094 # raise osv.except_osv(_('No Pricelist !'), _('Select a price list in the purchase order form before choosing a product.'))
1096 # - determine name and notes based on product in partner lang.
1097 context_partner = context.copy()
1099 lang = res_partner.browse(cr, uid, partner_id).lang
1100 context_partner.update( {'lang': lang, 'partner_id': partner_id} )
1101 product = product_product.browse(cr, uid, product_id, context=context_partner)
1102 #call name_get() with partner in the context to eventually match name and description in the seller_ids field
1103 dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0]
1104 if product.description_purchase:
1105 name += '\n' + product.description_purchase
1106 res['value'].update({'name': name})
1108 # - set a domain on product_uom
1109 res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
1111 # - check that uom and product uom belong to the same category
1112 product_uom_po_id = product.uom_po_id.id
1114 uom_id = product_uom_po_id
1116 if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
1117 if context.get('purchase_uom_check') and self._check_product_uom_group(cr, uid, context=context):
1118 res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
1119 uom_id = product_uom_po_id
1121 res['value'].update({'product_uom': uom_id})
1123 # - determine product_qty and date_planned based on seller info
1125 date_order = fields.datetime.now()
1128 supplierinfo = False
1129 precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Unit of Measure')
1130 for supplier in product.seller_ids:
1131 if partner_id and (supplier.name.id == partner_id):
1132 supplierinfo = supplier
1133 if supplierinfo.product_uom.id != uom_id:
1134 res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
1135 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
1136 if float_compare(min_qty , qty, precision_digits=precision) == 1: # If the supplier quantity is greater than entered from user, set minimal.
1138 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)}
1140 dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1142 res['value'].update({'date_planned': date_planned or dt})
1144 res['value'].update({'product_qty': qty})
1147 if price_unit is False or price_unit is None:
1148 # - determine price_unit and taxes_id
1150 date_order_str = datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT)
1151 price = product_pricelist.price_get(cr, uid, [pricelist_id],
1152 product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order_str})[pricelist_id]
1154 price = product.standard_price
1156 taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
1157 fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
1158 taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
1159 res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
1163 product_id_change = onchange_product_id
1164 product_uom_change = onchange_product_uom
1166 def action_confirm(self, cr, uid, ids, context=None):
1167 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
1171 class procurement_rule(osv.osv):
1172 _inherit = 'procurement.rule'
1174 def _get_action(self, cr, uid, context=None):
1175 return [('buy', _('Buy'))] + super(procurement_rule, self)._get_action(cr, uid, context=context)
1178 class procurement_order(osv.osv):
1179 _inherit = 'procurement.order'
1181 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
1182 'purchase_id': fields.related('purchase_line_id', 'order_id', type='many2one', relation='purchase.order', string='Purchase Order'),
1185 def propagate_cancel(self, cr, uid, procurement, context=None):
1186 if procurement.rule_id.action == 'buy' and procurement.purchase_line_id:
1187 purchase_line_obj = self.pool.get('purchase.order.line')
1188 if procurement.purchase_line_id.product_qty > procurement.product_qty and procurement.purchase_line_id.order_id.state == 'draft':
1189 purchase_line_obj.write(cr, uid, [procurement.purchase_line_id.id], {'product_qty': procurement.purchase_line_id.product_qty - procurement.product_qty}, context=context)
1191 purchase_line_obj.action_cancel(cr, uid, [procurement.purchase_line_id.id], context=context)
1192 return super(procurement_order, self).propagate_cancel(cr, uid, procurement, context=context)
1194 def _run(self, cr, uid, procurement, context=None):
1195 if procurement.rule_id and procurement.rule_id.action == 'buy':
1196 #make a purchase order for the procurement
1197 return self.make_po(cr, uid, [procurement.id], context=context)[procurement.id]
1198 return super(procurement_order, self)._run(cr, uid, procurement, context=context)
1200 def _check(self, cr, uid, procurement, context=None):
1201 if procurement.purchase_line_id and procurement.purchase_line_id.order_id.shipped: # TOCHECK: does it work for several deliveries?
1203 return super(procurement_order, self)._check(cr, uid, procurement, context=context)
1205 def _check_supplier_info(self, cr, uid, ids, context=None):
1206 ''' Check the supplier info field of a product and write an error message on the procurement if needed.
1207 Returns True if all needed information is there, False if some configuration mistake is detected.
1209 partner_obj = self.pool.get('res.partner')
1210 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1211 for procurement in self.browse(cr, uid, ids, context=context):
1213 partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
1215 if not procurement.product_id.seller_ids:
1216 message = _('No supplier defined for this product !')
1218 message = _('No default supplier defined for this product')
1219 elif not partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']:
1220 message = _('No address defined for the supplier')
1223 if procurement.message != message:
1224 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
1227 if user.company_id and user.company_id.partner_id:
1228 if partner.id == user.company_id.partner_id.id:
1229 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))
1233 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
1234 """Create the purchase order from the procurement, using
1235 the provided field values, after adding the given purchase
1236 order line in the purchase order.
1238 :params procurement: the procurement object generating the purchase order
1239 :params dict po_vals: field values for the new purchase order (the
1240 ``order_line`` field will be overwritten with one
1241 single line, as passed in ``line_vals``).
1242 :params dict line_vals: field values of the single purchase order line that
1243 the purchase order will contain.
1244 :return: id of the newly created purchase order
1247 po_vals.update({'order_line': [(0,0,line_vals)]})
1248 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
1250 def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
1251 """Return the datetime value to use as Schedule Date (``date_planned``) for the
1252 Purchase Order Lines created to satisfy the given procurement.
1254 :param browse_record procurement: the procurement for which a PO will be created.
1255 :param browse_report company: the company to which the new PO will belong to.
1257 :return: the desired Schedule Date for the PO lines
1259 procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
1260 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
1261 return schedule_date
1263 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
1264 """Return the datetime value to use as Order Date (``date_order``) for the
1265 Purchase Order created to satisfy the given procurement.
1267 :param browse_record procurement: the procurement for which a PO will be created.
1268 :param browse_report company: the company to which the new PO will belong to.
1269 :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1271 :return: the desired Order Date for the PO
1273 seller_delay = int(procurement.product_id.seller_delay)
1274 return schedule_date - relativedelta(days=seller_delay)
1276 def _get_product_supplier(self, cr, uid, procurement, context=None):
1277 ''' returns the main supplier of the procurement's product given as argument'''
1278 return procurement.product_id.seller_id
1280 def _get_po_line_values_from_proc(self, cr, uid, procurement, partner, company, schedule_date, context=None):
1283 uom_obj = self.pool.get('product.uom')
1284 pricelist_obj = self.pool.get('product.pricelist')
1285 prod_obj = self.pool.get('product.product')
1286 acc_pos_obj = self.pool.get('account.fiscal.position')
1288 seller_qty = procurement.product_id.seller_qty
1289 pricelist_id = partner.property_product_pricelist_purchase.id
1290 uom_id = procurement.product_id.uom_po_id.id
1291 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1293 qty = max(qty, seller_qty)
1294 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner.id, {'uom': uom_id})[pricelist_id]
1296 #Passing partner_id to context for purchase order line integrity of Line name
1297 new_context = context.copy()
1298 new_context.update({'lang': partner.lang, 'partner_id': partner.id})
1299 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
1300 taxes_ids = procurement.product_id.supplier_taxes_id
1301 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1302 name = product.display_name
1303 if product.description_purchase:
1304 name += '\n' + product.description_purchase
1309 'product_id': procurement.product_id.id,
1310 'product_uom': uom_id,
1311 'price_unit': price or 0.0,
1312 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1313 'taxes_id': [(6, 0, taxes)],
1316 def make_po(self, cr, uid, ids, context=None):
1317 """ 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.
1318 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)
1320 @return: dictionary giving for each procurement its related resolving PO line.
1323 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1324 po_obj = self.pool.get('purchase.order')
1325 po_line_obj = self.pool.get('purchase.order.line')
1326 seq_obj = self.pool.get('ir.sequence')
1329 sum_po_line_ids = []
1330 for procurement in self.browse(cr, uid, ids, context=context):
1331 partner = self._get_product_supplier(cr, uid, procurement, context=context)
1333 self.message_post(cr, uid, [procurement.id], _('There is no supplier associated to product %s') % (procurement.product_id.name))
1334 res[procurement.id] = False
1336 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1337 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1338 line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context)
1339 #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one
1340 available_draft_po_ids = po_obj.search(cr, uid, [
1341 ('partner_id', '=', partner.id), ('state', '=', 'draft'), ('picking_type_id', '=', procurement.rule_id.picking_type_id.id),
1342 ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context)
1343 if available_draft_po_ids:
1344 po_id = available_draft_po_ids[0]
1345 po_rec = po_obj.browse(cr, uid, po_id, context=context)
1346 #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
1347 if datetime.strptime(po_rec.date_order, DEFAULT_SERVER_DATETIME_FORMAT) > purchase_date:
1348 po_obj.write(cr, uid, [po_id], {'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
1349 #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
1350 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)
1351 if available_po_line_ids:
1352 po_line = po_line_obj.browse(cr, uid, available_po_line_ids[0], context=context)
1353 po_line_obj.write(cr, SUPERUSER_ID, po_line.id, {'product_qty': po_line.product_qty + line_vals['product_qty']}, context=context)
1354 po_line_id = po_line.id
1355 sum_po_line_ids.append(procurement.id)
1357 line_vals.update(order_id=po_id)
1358 po_line_id = po_line_obj.create(cr, SUPERUSER_ID, line_vals, context=context)
1359 linked_po_ids.append(procurement.id)
1361 name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1364 'origin': procurement.origin,
1365 'partner_id': partner.id,
1366 'location_id': procurement.location_id.id,
1367 'picking_type_id': procurement.rule_id.picking_type_id.id,
1368 'pricelist_id': partner.property_product_pricelist_purchase.id,
1369 'currency_id': partner.property_product_pricelist_purchase and partner.property_product_pricelist_purchase.currency_id.id or procurement.company_id.currency_id.id,
1370 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1371 'company_id': procurement.company_id.id,
1372 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False,
1373 'payment_term_id': partner.property_supplier_payment_term.id or False,
1374 'dest_address_id': procurement.partner_dest_id.id,
1376 po_id = self.create_procurement_purchase_order(cr, SUPERUSER_ID, procurement, po_vals, line_vals, context=context)
1377 po_line_id = po_obj.browse(cr, uid, po_id, context=context).order_line[0].id
1378 pass_ids.append(procurement.id)
1379 res[procurement.id] = po_line_id
1380 self.write(cr, uid, [procurement.id], {'purchase_line_id': po_line_id}, context=context)
1382 self.message_post(cr, uid, pass_ids, body=_("Draft Purchase Order created"), context=context)
1384 self.message_post(cr, uid, linked_po_ids, body=_("Purchase line created and linked to an existing Purchase Order"), context=context)
1386 self.message_post(cr, uid, sum_po_line_ids, body=_("Quantity added in existing Purchase Order Line"), context=context)
1390 class mail_mail(osv.Model):
1392 _inherit = 'mail.mail'
1394 def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
1395 if mail_sent and mail.model == 'purchase.order':
1396 obj = self.pool.get('purchase.order').browse(cr, uid, mail.res_id, context=context)
1397 if obj.state == 'draft':
1398 self.pool.get('purchase.order').signal_workflow(cr, uid, [mail.res_id], 'send_rfq')
1399 return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
1402 class product_template(osv.Model):
1403 _name = 'product.template'
1404 _inherit = 'product.template'
1406 def _get_buy_route(self, cr, uid, context=None):
1408 buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy')
1413 def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
1414 res = dict.fromkeys(ids, 0)
1415 for template in self.browse(cr, uid, ids, context=context):
1416 res[template.id] = sum([p.purchase_count for p in template.product_variant_ids])
1420 'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
1421 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
1426 'route_ids': _get_buy_route,
1429 def action_view_purchases(self, cr, uid, ids, context=None):
1430 products = self._get_products(cr, uid, ids, context=context)
1431 result = self._get_act_window_dict(cr, uid, 'purchase.action_purchase_line_product_tree', context=context)
1432 result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
1435 class product_product(osv.Model):
1436 _name = 'product.product'
1437 _inherit = 'product.product'
1439 def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
1440 Purchase = self.pool['purchase.order']
1442 product_id: Purchase.search_count(cr,uid, [('order_line.product_id', '=', product_id)], context=context)
1443 for product_id in ids
1447 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
1452 class mail_compose_message(osv.Model):
1453 _inherit = 'mail.compose.message'
1455 def send_mail(self, cr, uid, ids, context=None):
1456 context = context or {}
1457 if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
1458 context = dict(context, mail_post_autofollow=True)
1459 self.pool.get('purchase.order').signal_workflow(cr, uid, [context['default_res_id']], 'send_rfq')
1460 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1463 class account_invoice(osv.Model):
1464 """ Override account_invoice to add Chatter messages on the related purchase
1465 orders, logging the invoice receipt or payment. """
1466 _inherit = 'account.invoice'
1468 def invoice_validate(self, cr, uid, ids, context=None):
1469 res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context)
1470 purchase_order_obj = self.pool.get('purchase.order')
1471 # read access on purchase.order object is not required
1472 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1473 user_id = SUPERUSER_ID
1476 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1477 for order in purchase_order_obj.browse(cr, uid, po_ids, context=context):
1478 purchase_order_obj.message_post(cr, user_id, order.id, body=_("Invoice received"), context=context)
1480 for po_line in order.order_line:
1481 if any(line.invoice_id.state not in ['draft', 'cancel'] for line in po_line.invoice_lines):
1482 invoiced.append(po_line.id)
1484 self.pool['purchase.order.line'].write(cr, uid, invoiced, {'invoiced': True})
1485 workflow.trg_write(uid, 'purchase.order', order.id, cr)
1488 def confirm_paid(self, cr, uid, ids, context=None):
1489 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1490 purchase_order_obj = self.pool.get('purchase.order')
1491 # read access on purchase.order object is not required
1492 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1493 user_id = SUPERUSER_ID
1496 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1497 for po_id in po_ids:
1498 purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice paid"), context=context)
1501 class account_invoice_line(osv.Model):
1502 """ Override account_invoice_line to add the link to the purchase order line it is related to"""
1503 _inherit = 'account.invoice.line'
1505 'purchase_line_id': fields.many2one('purchase.order.line',
1506 'Purchase Order Line', ondelete='set null', select=True,
1511 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: