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 return 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
155 def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
160 SELECT picking_id, po.id FROM stock_picking p, stock_move m, purchase_order_line pol, purchase_order po
161 WHERE po.id in %s and po.id = pol.order_id and pol.id = m.purchase_line_id and m.picking_id = p.id
162 GROUP BY picking_id, po.id
165 cr.execute(query, (tuple(ids), ))
166 picks = cr.fetchall()
167 for pick_id, po_id in picks:
168 res[po_id].append(pick_id)
171 def _count_all(self, cr, uid, ids, field_name, arg, context=None):
174 'shipment_count': len(purchase.picking_ids),
175 'invoice_count': len(purchase.invoice_ids),
177 for purchase in self.browse(cr, uid, ids, context=context)
181 ('draft', 'Draft PO'),
183 ('bid', 'Bid Received'),
184 ('confirmed', 'Waiting Approval'),
185 ('approved', 'Purchase Confirmed'),
186 ('except_picking', 'Shipping Exception'),
187 ('except_invoice', 'Invoice Exception'),
189 ('cancel', 'Cancelled')
193 'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirmed',
194 'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'approved',
195 'purchase.mt_rfq_done': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
199 'name': fields.char('Order Reference', required=True, select=True, copy=False,
200 help="Unique number of the purchase order, "
201 "computed automatically when the purchase order is created."),
202 'origin': fields.char('Source Document', copy=False,
203 help="Reference of the document that generated this purchase order "
204 "request; a sales order or an internal procurement request."),
205 'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)],
206 'approved':[('readonly',True)],
207 'done':[('readonly',True)]},
209 help="Reference of the sales order or bid sent by your supplier. "
210 "It's mainly used to do the matching when you receive the "
211 "products as this reference is usually written on the "
212 "delivery order sent by your supplier."),
213 'date_order':fields.datetime('Order Date', required=True, states={'confirmed':[('readonly',True)],
214 'approved':[('readonly',True)]},
215 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.",
217 'date_approve':fields.date('Date Approved', readonly=1, select=True, copy=False,
218 help="Date on which purchase order has been approved"),
219 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
220 change_default=True, track_visibility='always'),
221 'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
222 states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
223 help="Put an address if you want to deliver directly from the supplier to the customer. " \
224 "Otherwise, keep empty to deliver to your own company."
226 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]} ),
227 '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."),
228 'currency_id': fields.many2one('res.currency','Currency', readonly=True, required=True,states={'draft': [('readonly', False)],'sent': [('readonly', False)]}),
229 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True,
230 help="The status of the purchase order or the quotation request. "
231 "A request for quotation is a purchase order in a 'Draft' status. "
232 "Then the order has to be confirmed by the user, the status switch "
233 "to 'Confirmed'. Then the supplier must confirm the order to change "
234 "the status to 'Approved'. When the purchase order is paid and "
235 "received, the status becomes 'Done'. If a cancel action occurs in "
236 "the invoice or in the receipt of goods, the status becomes "
238 select=True, copy=False),
239 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines',
240 states={'approved':[('readonly',True)],
241 'done':[('readonly',True)]},
243 'validator' : fields.many2one('res.users', 'Validated by', readonly=True, copy=False),
244 'notes': fields.text('Terms and Conditions'),
245 'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id',
246 'invoice_id', 'Invoices', copy=False,
247 help="Invoices generated for a purchase order"),
248 '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."),
249 'shipped':fields.boolean('Received', readonly=True, select=True, copy=False,
250 help="It indicates that a picking has been done"),
251 'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'),
252 'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', copy=False,
253 help="It indicates that an invoice has been validated"),
254 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
255 '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,
256 readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)],'bid':[('readonly',False)]},
257 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" \
258 "Based on generated invoice: create a draft invoice you can validate later.\n" \
259 "Based on incoming shipments: let you create an invoice when receipts are validated."
261 '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.",
263 'purchase.order.line': (_get_order, ['date_planned'], 10),
266 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
268 'purchase.order.line': (_get_order, None, 10),
269 }, multi="sums", help="The amount without tax", track_visibility='always'),
270 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
272 'purchase.order.line': (_get_order, None, 10),
273 }, multi="sums", help="The tax amount"),
274 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
276 'purchase.order.line': (_get_order, None, 10),
277 }, multi="sums", help="The total amount"),
278 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
279 'payment_term_id': fields.many2one('account.payment.term', 'Payment Term'),
280 'incoterm_id': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
281 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
282 'create_uid': fields.many2one('res.users', 'Responsible'),
283 'company_id': fields.many2one('res.company', 'Company', required=True, select=1, states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)]}),
284 'journal_id': fields.many2one('account.journal', 'Journal'),
285 'bid_date': fields.date('Bid Received On', readonly=True, help="Date on which the bid was received"),
286 'bid_validity': fields.date('Bid Valid Until', help="Date on which the bid expired"),
287 'picking_type_id': fields.many2one('stock.picking.type', 'Deliver To', help="This will determine picking type of incoming shipment", required=True,
288 states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}),
289 'related_location_id': fields.related('picking_type_id', 'default_location_dest_id', type='many2one', relation='stock.location', string="Related location", store=True),
290 'shipment_count': fields.function(_count_all, type='integer', string='Incoming Shipments', multi=True),
291 'invoice_count': fields.function(_count_all, type='integer', string='Invoices', multi=True)
294 'date_order': fields.datetime.now,
296 'name': lambda obj, cr, uid, context: '/',
298 'invoice_method': 'order',
300 '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,
301 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
302 'journal_id': _get_journal,
303 'currency_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id,
304 'picking_type_id': _get_picking_in,
307 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
309 _name = "purchase.order"
310 _inherit = ['mail.thread', 'ir.needaction_mixin']
311 _description = "Purchase Order"
312 _order = 'date_order desc, id desc'
314 def create(self, cr, uid, vals, context=None):
315 if vals.get('name','/')=='/':
316 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'purchase.order') or '/'
317 context = dict(context or {}, mail_create_nolog=True)
318 order = super(purchase_order, self).create(cr, uid, vals, context=context)
319 self.message_post(cr, uid, [order], body=_("RFQ created"), context=context)
322 def unlink(self, cr, uid, ids, context=None):
323 purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
325 for s in purchase_orders:
326 if s['state'] in ['draft','cancel']:
327 unlink_ids.append(s['id'])
329 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.'))
331 # automatically sending subflow.delete upon deletion
332 self.signal_workflow(cr, uid, unlink_ids, 'purchase_cancel')
334 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
336 def set_order_line_status(self, cr, uid, ids, status, context=None):
337 line = self.pool.get('purchase.order.line')
339 proc_obj = self.pool.get('procurement.order')
340 for order in self.browse(cr, uid, ids, context=context):
341 order_line_ids += [po_line.id for po_line in order.order_line]
343 line.write(cr, uid, order_line_ids, {'state': status}, context=context)
344 if order_line_ids and status == 'cancel':
345 procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', order_line_ids)], context=context)
347 proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
350 def button_dummy(self, cr, uid, ids, context=None):
353 def onchange_pricelist(self, cr, uid, ids, pricelist_id, context=None):
356 return {'value': {'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id}}
358 #Destination address is used when dropshipping
359 def onchange_dest_address_id(self, cr, uid, ids, address_id, context=None):
362 address = self.pool.get('res.partner')
364 supplier = address.browse(cr, uid, address_id, context=context)
366 location_id = supplier.property_stock_customer.id
367 values.update({'location_id': location_id})
368 return {'value':values}
370 def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None):
373 picktype = self.pool.get("stock.picking.type").browse(cr, uid, picking_type_id, context=context)
374 if picktype.default_location_dest_id:
375 value.update({'location_id': picktype.default_location_dest_id.id})
376 value.update({'related_location_id': picktype.default_location_dest_id and picktype.default_location_dest_id.id or False})
377 return {'value': value}
379 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
380 partner = self.pool.get('res.partner')
383 'fiscal_position': False,
384 'payment_term_id': False,
386 supplier_address = partner.address_get(cr, uid, [partner_id], ['default'], context=context)
387 supplier = partner.browse(cr, uid, partner_id, context=context)
389 'pricelist_id': supplier.property_product_pricelist_purchase.id,
390 'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
391 'payment_term_id': supplier.property_supplier_payment_term.id or False,
394 def invoice_open(self, cr, uid, ids, context=None):
395 mod_obj = self.pool.get('ir.model.data')
396 act_obj = self.pool.get('ir.actions.act_window')
398 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree2')
399 id = result and result[1] or False
400 result = act_obj.read(cr, uid, [id], context=context)[0]
402 for po in self.browse(cr, uid, ids, context=context):
403 inv_ids+= [invoice.id for invoice in po.invoice_ids]
405 raise osv.except_osv(_('Error!'), _('Please create Invoices.'))
406 #choose the view_mode accordingly
408 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
410 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
411 result['views'] = [(res and res[1] or False, 'form')]
412 result['res_id'] = inv_ids and inv_ids[0] or False
415 def view_invoice(self, cr, uid, ids, context=None):
417 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.
419 context = dict(context or {})
420 mod_obj = self.pool.get('ir.model.data')
421 wizard_obj = self.pool.get('purchase.order.line_invoice')
422 #compute the number of invoices to display
424 for po in self.browse(cr, uid, ids, context=context):
425 if po.invoice_method == 'manual':
426 if not po.invoice_ids:
427 context.update({'active_ids' : [line.id for line in po.order_line]})
428 wizard_obj.makeInvoices(cr, uid, [], context=context)
430 for po in self.browse(cr, uid, ids, context=context):
431 inv_ids+= [invoice.id for invoice in po.invoice_ids]
432 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
433 res_id = res and res[1] or False
436 'name': _('Supplier Invoices'),
440 'res_model': 'account.invoice',
441 'context': "{'type':'in_invoice', 'journal_type': 'purchase'}",
442 'type': 'ir.actions.act_window',
445 'res_id': inv_ids and inv_ids[0] or False,
448 def view_picking(self, cr, uid, ids, context=None):
450 This function returns an action that display existing picking orders of given purchase order ids.
454 mod_obj = self.pool.get('ir.model.data')
455 dummy, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree'))
456 action = self.pool.get('ir.actions.act_window').read(cr, uid, action_id, context=context)
459 for po in self.browse(cr, uid, ids, context=context):
460 pick_ids += [picking.id for picking in po.picking_ids]
462 #override the context to get rid of the default filtering on picking type
463 action['context'] = {}
464 #choose the view_mode accordingly
465 if len(pick_ids) > 1:
466 action['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
468 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
469 action['views'] = [(res and res[1] or False, 'form')]
470 action['res_id'] = pick_ids and pick_ids[0] or False
473 def wkf_approve_order(self, cr, uid, ids, context=None):
474 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)})
477 def wkf_bid_received(self, cr, uid, ids, context=None):
478 return self.write(cr, uid, ids, {'state':'bid', 'bid_date': fields.date.context_today(self,cr,uid,context=context)})
480 def wkf_send_rfq(self, cr, uid, ids, context=None):
482 This function opens a window to compose an email, with the edi purchase template message loaded by default
486 ir_model_data = self.pool.get('ir.model.data')
488 if context.get('send_rfq', False):
489 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
491 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase_done')[1]
495 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
497 compose_form_id = False
500 'default_model': 'purchase.order',
501 'default_res_id': ids[0],
502 'default_use_template': bool(template_id),
503 'default_template_id': template_id,
504 'default_composition_mode': 'comment',
507 'name': _('Compose Email'),
508 'type': 'ir.actions.act_window',
511 'res_model': 'mail.compose.message',
512 'views': [(compose_form_id, 'form')],
513 'view_id': compose_form_id,
518 def print_quotation(self, cr, uid, ids, context=None):
520 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
522 assert len(ids) == 1, 'This option should only be used for a single id at a time'
523 self.signal_workflow(cr, uid, ids, 'send_rfq')
524 return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
526 def wkf_confirm_order(self, cr, uid, ids, context=None):
528 for po in self.browse(cr, uid, ids, context=context):
529 if not po.order_line:
530 raise osv.except_osv(_('Error!'),_('You cannot confirm a purchase order without any purchase order line.'))
531 for line in po.order_line:
532 if line.state=='draft':
534 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
536 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
539 def _choose_account_from_po_line(self, cr, uid, po_line, context=None):
540 fiscal_obj = self.pool.get('account.fiscal.position')
541 property_obj = self.pool.get('ir.property')
542 if po_line.product_id:
543 acc_id = po_line.product_id.property_account_expense.id
545 acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
547 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,))
549 acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context=context).id
550 fpos = po_line.order_id.fiscal_position or False
551 return fiscal_obj.map_account(cr, uid, fpos, acc_id)
553 def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
554 """Collects require data from purchase order line that is used to create invoice line
555 for that purchase order line
556 :param account_id: Expense account of the product of PO line if any.
557 :param browse_record order_line: Purchase order line browse record
558 :return: Value for fields of invoice lines.
562 'name': order_line.name,
563 'account_id': account_id,
564 'price_unit': order_line.price_unit or 0.0,
565 'quantity': order_line.product_qty,
566 'product_id': order_line.product_id.id or False,
567 'uos_id': order_line.product_uom.id or False,
568 'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
569 'account_analytic_id': order_line.account_analytic_id.id or False,
570 'purchase_line_id': order_line.id,
573 def _prepare_invoice(self, cr, uid, order, line_ids, context=None):
574 """Prepare the dict of values to create the new invoice for a
575 purchase order. This method may be overridden to implement custom
576 invoice generation (making sure to call super() to establish
577 a clean extension chain).
579 :param browse_record order: purchase.order record to invoice
580 :param list(int) line_ids: list of invoice line IDs that must be
581 attached to the invoice
582 :return: dict of value to create() the invoice
584 journal_ids = self.pool['account.journal'].search(
585 cr, uid, [('type', '=', 'purchase'),
586 ('company_id', '=', order.company_id.id)],
589 raise osv.except_osv(
591 _('Define purchase journal for this company: "%s" (id:%d).') % \
592 (order.company_id.name, order.company_id.id))
594 'name': order.partner_ref or order.name,
595 'reference': order.partner_ref or order.name,
596 'account_id': order.partner_id.property_account_payable.id,
597 'type': 'in_invoice',
598 'partner_id': order.partner_id.id,
599 'currency_id': order.currency_id.id,
600 'journal_id': len(journal_ids) and journal_ids[0] or False,
601 'invoice_line': [(6, 0, line_ids)],
602 'origin': order.name,
603 'fiscal_position': order.fiscal_position.id or False,
604 'payment_term': order.payment_term_id.id or False,
605 'company_id': order.company_id.id,
608 def action_cancel_draft(self, cr, uid, ids, context=None):
611 self.write(cr, uid, ids, {'state':'draft','shipped':0})
612 self.set_order_line_status(cr, uid, ids, 'draft', context=context)
614 # Deleting the existing instance of workflow for PO
615 self.delete_workflow(cr, uid, [p_id]) # TODO is it necessary to interleave the calls?
616 self.create_workflow(cr, uid, [p_id])
619 def wkf_po_done(self, cr, uid, ids, context=None):
620 self.write(cr, uid, ids, {'state': 'done'}, context=context)
621 self.set_order_line_status(cr, uid, ids, 'done', context=context)
623 def action_invoice_create(self, cr, uid, ids, context=None):
624 """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
625 :param ids: list of ids of purchase orders.
626 :return: ID of created invoice.
629 context = dict(context or {})
631 inv_obj = self.pool.get('account.invoice')
632 inv_line_obj = self.pool.get('account.invoice.line')
635 uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
636 for order in self.browse(cr, uid, ids, context=context):
637 context.pop('force_company', None)
638 if order.company_id.id != uid_company_id:
639 #if the company of the document is different than the current user company, force the company in the context
640 #then re-do a browse to read the property fields for the good company.
641 context['force_company'] = order.company_id.id
642 order = self.browse(cr, uid, order.id, context=context)
644 # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
646 for po_line in order.order_line:
647 acc_id = self._choose_account_from_po_line(cr, uid, po_line, context=context)
648 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
649 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
650 inv_lines.append(inv_line_id)
651 po_line.write({'invoice_lines': [(4, inv_line_id)]})
653 # get invoice data and create invoice
654 inv_data = self._prepare_invoice(cr, uid, order, inv_lines, context=context)
655 inv_id = inv_obj.create(cr, uid, inv_data, context=context)
657 # compute the invoice
658 inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
660 # Link this new invoice to related purchase order
661 order.write({'invoice_ids': [(4, inv_id)]})
665 def invoice_done(self, cr, uid, ids, context=None):
666 self.write(cr, uid, ids, {'state': 'approved'}, context=context)
669 def has_stockable_product(self, cr, uid, ids, *args):
670 for order in self.browse(cr, uid, ids):
671 for order_line in order.order_line:
672 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
676 def wkf_action_cancel(self, cr, uid, ids, context=None):
677 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
678 self.set_order_line_status(cr, uid, ids, 'cancel', context=context)
680 def action_cancel(self, cr, uid, ids, context=None):
681 for purchase in self.browse(cr, uid, ids, context=context):
682 for pick in purchase.picking_ids:
683 for move in pick.move_lines:
684 if pick.state == 'done':
685 raise osv.except_osv(
686 _('Unable to cancel the purchase order %s.') % (purchase.name),
687 _('You have already received some goods for it. '))
688 self.pool.get('stock.picking').action_cancel(cr, uid, [x.id for x in purchase.picking_ids if x.state != 'cancel'], context=context)
689 for inv in purchase.invoice_ids:
690 if inv and inv.state not in ('cancel', 'draft'):
691 raise osv.except_osv(
692 _('Unable to cancel this purchase order.'),
693 _('You must first cancel all invoices related to this purchase order.'))
694 self.pool.get('account.invoice') \
695 .signal_workflow(cr, uid, map(attrgetter('id'), purchase.invoice_ids), 'invoice_cancel')
696 self.signal_workflow(cr, uid, ids, 'purchase_cancel')
699 def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
700 ''' 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()'''
701 product_uom = self.pool.get('product.uom')
702 price_unit = order_line.price_unit
703 if order_line.product_uom.id != order_line.product_id.uom_id.id:
704 price_unit *= order_line.product_uom.factor
705 if order.currency_id.id != order.company_id.currency_id.id:
706 #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency
707 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)
710 'name': order_line.name or '',
711 'product_id': order_line.product_id.id,
712 'product_uom': order_line.product_uom.id,
713 'product_uos': order_line.product_uom.id,
714 'date': order.date_order,
715 'date_expected': fields.date.date_to_datetime(self, cr, uid, order_line.date_planned, context),
716 'location_id': order.partner_id.property_stock_supplier.id,
717 'location_dest_id': order.location_id.id,
718 'picking_id': picking_id,
719 'partner_id': order.dest_address_id.id or order.partner_id.id,
720 'move_dest_id': False,
722 'purchase_line_id': order_line.id,
723 'company_id': order.company_id.id,
724 'price_unit': price_unit,
725 'picking_type_id': order.picking_type_id.id,
726 'group_id': group_id,
727 'procurement_id': False,
728 'origin': order.name,
729 '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 [],
730 'warehouse_id':order.picking_type_id.warehouse_id.id,
731 'invoice_state': order.invoice_method == 'picking' and '2binvoiced' or 'none',
734 diff_quantity = order_line.product_qty
735 for procurement in order_line.procurement_ids:
736 procurement_qty = product_uom._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, to_uom_id=order_line.product_uom.id)
737 tmp = move_template.copy()
739 'product_uom_qty': min(procurement_qty, diff_quantity),
740 'product_uos_qty': min(procurement_qty, diff_quantity),
741 'move_dest_id': procurement.move_dest_id.id, #move destination is same as procurement destination
742 'group_id': procurement.group_id.id or group_id, #move group is same as group of procurements if it exists, otherwise take another group
743 'procurement_id': procurement.id,
744 '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
745 'propagate': procurement.rule_id.propagate,
747 diff_quantity -= min(procurement_qty, diff_quantity)
749 #if the order line has a bigger quantity than the procurement it was for (manually changed or minimal quantity), then
750 #split the future stock move in two because the route followed may be different.
751 if diff_quantity > 0:
752 move_template['product_uom_qty'] = diff_quantity
753 move_template['product_uos_qty'] = diff_quantity
754 res.append(move_template)
757 def _create_stock_moves(self, cr, uid, order, order_lines, picking_id=False, context=None):
758 """Creates appropriate stock moves for given order lines, whose can optionally create a
759 picking if none is given or no suitable is found, then confirms the moves, makes them
760 available, and confirms the pickings.
762 If ``picking_id`` is provided, the stock moves will be added to it, otherwise a standard
763 incoming picking will be created to wrap the stock moves (default behavior of the stock.move)
765 Modules that wish to customize the procurements or partition the stock moves over
766 multiple stock pickings may override this method and call ``super()`` with
767 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
769 :param browse_record order: purchase order to which the order lines belong
770 :param list(browse_record) order_lines: purchase order line records for which picking
771 and moves should be created.
772 :param int picking_id: optional ID of a stock picking to which the created stock moves
773 will be added. A new picking will be created if omitted.
776 stock_move = self.pool.get('stock.move')
778 new_group = self.pool.get("procurement.group").create(cr, uid, {'name': order.name, 'partner_id': order.partner_id.id}, context=context)
780 for order_line in order_lines:
781 if not order_line.product_id:
784 if order_line.product_id.type in ('product', 'consu'):
785 for vals in self._prepare_order_line_move(cr, uid, order, order_line, picking_id, new_group, context=context):
786 move = stock_move.create(cr, uid, vals, context=context)
787 todo_moves.append(move)
789 todo_moves = stock_move.action_confirm(cr, uid, todo_moves)
790 stock_move.force_assign(cr, uid, todo_moves)
792 def test_moves_done(self, cr, uid, ids, context=None):
793 '''PO is done at the delivery side if all the incoming shipments are done'''
794 for purchase in self.browse(cr, uid, ids, context=context):
795 for picking in purchase.picking_ids:
796 if picking.state != 'done':
800 def test_moves_except(self, cr, uid, ids, context=None):
801 ''' PO is in exception at the delivery side if one of the picking is canceled
802 and the other pickings are completed (done or canceled)
804 at_least_one_canceled = False
805 alldoneorcancel = True
806 for purchase in self.browse(cr, uid, ids, context=context):
807 for picking in purchase.picking_ids:
808 if picking.state == 'cancel':
809 at_least_one_canceled = True
810 if picking.state not in ['done', 'cancel']:
811 alldoneorcancel = False
812 return at_least_one_canceled and alldoneorcancel
814 def move_lines_get(self, cr, uid, ids, *args):
816 for order in self.browse(cr, uid, ids, context={}):
817 for line in order.order_line:
818 res += [x.id for x in line.move_ids]
821 def action_picking_create(self, cr, uid, ids, context=None):
822 for order in self.browse(cr, uid, ids):
824 'picking_type_id': order.picking_type_id.id,
825 'partner_id': order.dest_address_id.id or order.partner_id.id,
826 'date': max([l.date_planned for l in order.order_line]),
829 picking_id = self.pool.get('stock.picking').create(cr, uid, picking_vals, context=context)
830 self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
832 def picking_done(self, cr, uid, ids, context=None):
833 self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
834 # Do check on related procurements:
835 proc_obj = self.pool.get("procurement.order")
837 for po in self.browse(cr, uid, ids, context=context):
838 po_lines += [x.id for x in po.order_line]
840 procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', po_lines)], context=context)
842 proc_obj.check(cr, uid, procs, context=context)
843 self.message_post(cr, uid, ids, body=_("Products received"), context=context)
846 def do_merge(self, cr, uid, ids, context=None):
848 To merge similar type of purchase orders.
849 Orders will only be merged if:
850 * Purchase Orders are in draft
851 * Purchase Orders belong to the same partner
852 * Purchase Orders are have same stock location, same pricelist
853 Lines will only be merged if:
854 * Order lines are exactly the same except for the quantity and unit
856 @param self: The object pointer.
857 @param cr: A database cursor
858 @param uid: ID of the user currently logged in
859 @param ids: the ID or list of IDs
860 @param context: A standard dictionary
862 @return: new purchase order id
865 #TOFIX: merged order line should be unlink
866 def make_key(br, fields):
869 field_val = getattr(br, field)
870 if field in ('product_id', 'account_analytic_id'):
873 if isinstance(field_val, browse_record):
874 field_val = field_val.id
875 elif isinstance(field_val, browse_null):
877 elif isinstance(field_val, browse_record_list):
878 field_val = ((6, 0, tuple([v.id for v in field_val])),)
879 list_key.append((field, field_val))
881 return tuple(list_key)
883 context = dict(context or {})
885 # Compute what the new orders should contain
888 order_lines_to_move = []
889 for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
890 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
891 new_order = new_orders.setdefault(order_key, ({}, []))
892 new_order[1].append(porder.id)
893 order_infos = new_order[0]
897 'origin': porder.origin,
898 'date_order': porder.date_order,
899 'partner_id': porder.partner_id.id,
900 'dest_address_id': porder.dest_address_id.id,
901 'picking_type_id': porder.picking_type_id.id,
902 'location_id': porder.location_id.id,
903 'pricelist_id': porder.pricelist_id.id,
906 'notes': '%s' % (porder.notes or '',),
907 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
910 if porder.date_order < order_infos['date_order']:
911 order_infos['date_order'] = porder.date_order
913 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
915 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
917 for order_line in porder.order_line:
918 order_lines_to_move += [order_line.id]
922 for order_key, (order_data, old_ids) in new_orders.iteritems():
923 # skip merges with only one order
925 allorders += (old_ids or [])
928 # cleanup order line data
929 for key, value in order_data['order_line'].iteritems():
930 del value['uom_factor']
931 value.update(dict(key))
932 order_data['order_line'] = [(6, 0, order_lines_to_move)]
934 # create the new order
935 context.update({'mail_create_nolog': True})
936 neworder_id = self.create(cr, uid, order_data)
937 self.message_post(cr, uid, [neworder_id], body=_("RFQ created"), context=context)
938 orders_info.update({neworder_id: old_ids})
939 allorders.append(neworder_id)
941 # make triggers pointing to the old orders point to the new order
942 for old_id in old_ids:
943 self.redirect_workflow(cr, uid, [(old_id, neworder_id)])
944 self.signal_workflow(cr, uid, [old_id], 'purchase_cancel')
949 class purchase_order_line(osv.osv):
950 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
952 cur_obj=self.pool.get('res.currency')
953 tax_obj = self.pool.get('account.tax')
954 for line in self.browse(cr, uid, ids, context=context):
955 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, line.order_id.partner_id)
956 cur = line.order_id.pricelist_id.currency_id
957 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
960 def _get_uom_id(self, cr, uid, context=None):
962 proxy = self.pool.get('ir.model.data')
963 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
965 except Exception, ex:
969 'name': fields.text('Description', required=True),
970 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
971 'date_planned': fields.date('Scheduled Date', required=True, select=True),
972 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
973 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
974 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
975 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
976 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
977 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
978 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
979 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
980 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
981 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')],
982 'Status', required=True, readonly=True, copy=False,
983 help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
984 \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
985 \n* The \'Done\' status is set automatically when purchase order is set as done. \
986 \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
987 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel',
988 'order_line_id', 'invoice_id', 'Invoice Lines',
989 readonly=True, copy=False),
990 'invoiced': fields.boolean('Invoiced', readonly=True, copy=False),
991 'partner_id': fields.related('order_id', 'partner_id', string='Partner', readonly=True, type="many2one", relation="res.partner", store=True),
992 'date_order': fields.related('order_id', 'date_order', string='Order Date', readonly=True, type="datetime"),
993 'procurement_ids': fields.one2many('procurement.order', 'purchase_line_id', string='Associated procurements'),
996 'product_uom' : _get_uom_id,
997 'product_qty': lambda *a: 1.0,
998 'state': lambda *args: 'draft',
999 'invoiced': lambda *a: 0,
1001 _table = 'purchase_order_line'
1002 _name = 'purchase.order.line'
1003 _description = 'Purchase Order Line'
1005 def unlink(self, cr, uid, ids, context=None):
1006 for line in self.browse(cr, uid, ids, context=context):
1007 if line.state not in ['draft', 'cancel']:
1008 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
1009 procurement_obj = self.pool.get('procurement.order')
1010 procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context)
1011 if procurement_ids_to_cancel:
1012 self.pool['procurement.order'].cancel(cr, uid, procurement_ids_to_cancel)
1013 return super(purchase_order_line, self).unlink(cr, uid, ids, context=context)
1015 def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1016 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1017 name=False, price_unit=False, state='draft', context=None):
1019 onchange handler of product_uom.
1024 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1025 context = dict(context, purchase_uom_check=True)
1026 return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1027 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
1028 name=name, price_unit=price_unit, state=state, context=context)
1030 def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
1031 """Return the datetime value to use as Schedule Date (``date_planned``) for
1032 PO Lines that correspond to the given product.supplierinfo,
1033 when ordered at `date_order_str`.
1035 :param browse_record | False supplier_info: product.supplierinfo, used to
1036 determine delivery delay (if False, default delay = 0)
1037 :param str date_order_str: date of order field, as a string in
1038 DEFAULT_SERVER_DATETIME_FORMAT
1040 :return: desired Schedule Date for the PO line
1042 supplier_delay = int(supplier_info.delay) if supplier_info else 0
1043 return datetime.strptime(date_order_str, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=supplier_delay)
1045 def action_cancel(self, cr, uid, ids, context=None):
1046 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1047 for po_line in self.browse(cr, uid, ids, context=context):
1048 if all([l.state == 'cancel' for l in po_line.order_id.order_line]):
1049 self.pool.get('purchase.order').action_cancel(cr, uid, [po_line.order_id.id], context=context)
1051 def _check_product_uom_group(self, cr, uid, context=None):
1052 group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom')
1053 res = [user for user in group_uom.users if user.id == uid]
1054 return len(res) and True or False
1057 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1058 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1059 name=False, price_unit=False, state='draft', context=None):
1061 onchange handler of product_id.
1066 res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1070 product_product = self.pool.get('product.product')
1071 product_uom = self.pool.get('product.uom')
1072 res_partner = self.pool.get('res.partner')
1073 product_pricelist = self.pool.get('product.pricelist')
1074 account_fiscal_position = self.pool.get('account.fiscal.position')
1075 account_tax = self.pool.get('account.tax')
1077 # - check for the presence of partner_id and pricelist_id
1079 # raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
1080 #if not pricelist_id:
1081 # raise osv.except_osv(_('No Pricelist !'), _('Select a price list in the purchase order form before choosing a product.'))
1083 # - determine name and notes based on product in partner lang.
1084 context_partner = context.copy()
1086 lang = res_partner.browse(cr, uid, partner_id).lang
1087 context_partner.update( {'lang': lang, 'partner_id': partner_id} )
1088 product = product_product.browse(cr, uid, product_id, context=context_partner)
1089 #call name_get() with partner in the context to eventually match name and description in the seller_ids field
1090 dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0]
1091 if product.description_purchase:
1092 name += '\n' + product.description_purchase
1093 res['value'].update({'name': name})
1095 # - set a domain on product_uom
1096 res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
1098 # - check that uom and product uom belong to the same category
1099 product_uom_po_id = product.uom_po_id.id
1101 uom_id = product_uom_po_id
1103 if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
1104 if context.get('purchase_uom_check') and self._check_product_uom_group(cr, uid, context=context):
1105 res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
1106 uom_id = product_uom_po_id
1108 res['value'].update({'product_uom': uom_id})
1110 # - determine product_qty and date_planned based on seller info
1112 date_order = fields.datetime.now()
1115 supplierinfo = False
1116 precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Unit of Measure')
1117 for supplier in product.seller_ids:
1118 if partner_id and (supplier.name.id == partner_id):
1119 supplierinfo = supplier
1120 if supplierinfo.product_uom.id != uom_id:
1121 res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
1122 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
1123 if float_compare(min_qty , qty, precision_digits=precision) == 1: # If the supplier quantity is greater than entered from user, set minimal.
1125 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)}
1127 dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1129 res['value'].update({'date_planned': date_planned or dt})
1131 res['value'].update({'product_qty': qty})
1134 if price_unit is False or price_unit is None:
1135 # - determine price_unit and taxes_id
1137 date_order_str = datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT)
1138 price = product_pricelist.price_get(cr, uid, [pricelist_id],
1139 product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order_str})[pricelist_id]
1141 price = product.standard_price
1143 taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
1144 fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
1145 taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
1146 res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
1150 product_id_change = onchange_product_id
1151 product_uom_change = onchange_product_uom
1153 def action_confirm(self, cr, uid, ids, context=None):
1154 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
1158 class procurement_rule(osv.osv):
1159 _inherit = 'procurement.rule'
1161 def _get_action(self, cr, uid, context=None):
1162 return [('buy', _('Buy'))] + super(procurement_rule, self)._get_action(cr, uid, context=context)
1165 class procurement_order(osv.osv):
1166 _inherit = 'procurement.order'
1168 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
1169 'purchase_id': fields.related('purchase_line_id', 'order_id', type='many2one', relation='purchase.order', string='Purchase Order'),
1172 def propagate_cancel(self, cr, uid, procurement, context=None):
1173 if procurement.rule_id.action == 'buy' and procurement.purchase_line_id:
1174 purchase_line_obj = self.pool.get('purchase.order.line')
1175 if procurement.purchase_line_id.product_qty > procurement.product_qty and procurement.purchase_line_id.order_id.state == 'draft':
1176 purchase_line_obj.write(cr, uid, [procurement.purchase_line_id.id], {'product_qty': procurement.purchase_line_id.product_qty - procurement.product_qty}, context=context)
1178 purchase_line_obj.action_cancel(cr, uid, [procurement.purchase_line_id.id], context=context)
1179 return super(procurement_order, self).propagate_cancel(cr, uid, procurement, context=context)
1181 def _run(self, cr, uid, procurement, context=None):
1182 if procurement.rule_id and procurement.rule_id.action == 'buy':
1183 #make a purchase order for the procurement
1184 return self.make_po(cr, uid, [procurement.id], context=context)[procurement.id]
1185 return super(procurement_order, self)._run(cr, uid, procurement, context=context)
1187 def _check(self, cr, uid, procurement, context=None):
1188 if procurement.purchase_line_id and procurement.purchase_line_id.order_id.shipped: # TOCHECK: does it work for several deliveries?
1190 return super(procurement_order, self)._check(cr, uid, procurement, context=context)
1192 def _check_supplier_info(self, cr, uid, ids, context=None):
1193 ''' Check the supplier info field of a product and write an error message on the procurement if needed.
1194 Returns True if all needed information is there, False if some configuration mistake is detected.
1196 partner_obj = self.pool.get('res.partner')
1197 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1198 for procurement in self.browse(cr, uid, ids, context=context):
1200 partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
1202 if not procurement.product_id.seller_ids:
1203 message = _('No supplier defined for this product !')
1205 message = _('No default supplier defined for this product')
1206 elif not partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']:
1207 message = _('No address defined for the supplier')
1210 if procurement.message != message:
1211 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
1214 if user.company_id and user.company_id.partner_id:
1215 if partner.id == user.company_id.partner_id.id:
1216 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))
1220 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
1221 """Create the purchase order from the procurement, using
1222 the provided field values, after adding the given purchase
1223 order line in the purchase order.
1225 :params procurement: the procurement object generating the purchase order
1226 :params dict po_vals: field values for the new purchase order (the
1227 ``order_line`` field will be overwritten with one
1228 single line, as passed in ``line_vals``).
1229 :params dict line_vals: field values of the single purchase order line that
1230 the purchase order will contain.
1231 :return: id of the newly created purchase order
1234 po_vals.update({'order_line': [(0,0,line_vals)]})
1235 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
1237 def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
1238 """Return the datetime value to use as Schedule Date (``date_planned``) for the
1239 Purchase Order Lines created to satisfy the given procurement.
1241 :param browse_record procurement: the procurement for which a PO will be created.
1242 :param browse_report company: the company to which the new PO will belong to.
1244 :return: the desired Schedule Date for the PO lines
1246 procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
1247 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
1248 return schedule_date
1250 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
1251 """Return the datetime value to use as Order Date (``date_order``) for the
1252 Purchase Order 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.
1256 :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1258 :return: the desired Order Date for the PO
1260 seller_delay = int(procurement.product_id.seller_delay)
1261 return schedule_date - relativedelta(days=seller_delay)
1263 def _get_product_supplier(self, cr, uid, procurement, context=None):
1264 ''' returns the main supplier of the procurement's product given as argument'''
1265 return procurement.product_id.seller_id
1267 def _get_po_line_values_from_proc(self, cr, uid, procurement, partner, company, schedule_date, context=None):
1270 uom_obj = self.pool.get('product.uom')
1271 pricelist_obj = self.pool.get('product.pricelist')
1272 prod_obj = self.pool.get('product.product')
1273 acc_pos_obj = self.pool.get('account.fiscal.position')
1275 seller_qty = procurement.product_id.seller_qty
1276 pricelist_id = partner.property_product_pricelist_purchase.id
1277 uom_id = procurement.product_id.uom_po_id.id
1278 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1280 qty = max(qty, seller_qty)
1281 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner.id, {'uom': uom_id})[pricelist_id]
1283 #Passing partner_id to context for purchase order line integrity of Line name
1284 new_context = context.copy()
1285 new_context.update({'lang': partner.lang, 'partner_id': partner.id})
1286 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
1287 taxes_ids = procurement.product_id.supplier_taxes_id
1288 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1289 name = product.display_name
1290 if product.description_purchase:
1291 name += '\n' + product.description_purchase
1296 'product_id': procurement.product_id.id,
1297 'product_uom': uom_id,
1298 'price_unit': price or 0.0,
1299 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1300 'taxes_id': [(6, 0, taxes)],
1303 def make_po(self, cr, uid, ids, context=None):
1304 """ 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.
1305 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)
1307 @return: dictionary giving for each procurement its related resolving PO line.
1310 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1311 po_obj = self.pool.get('purchase.order')
1312 po_line_obj = self.pool.get('purchase.order.line')
1313 seq_obj = self.pool.get('ir.sequence')
1316 sum_po_line_ids = []
1317 for procurement in self.browse(cr, uid, ids, context=context):
1318 partner = self._get_product_supplier(cr, uid, procurement, context=context)
1320 self.message_post(cr, uid, [procurement.id], _('There is no supplier associated to product %s') % (procurement.product_id.name))
1321 res[procurement.id] = False
1323 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1324 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1325 line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context)
1326 #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one
1327 available_draft_po_ids = po_obj.search(cr, uid, [
1328 ('partner_id', '=', partner.id), ('state', '=', 'draft'), ('picking_type_id', '=', procurement.rule_id.picking_type_id.id),
1329 ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context)
1330 if available_draft_po_ids:
1331 po_id = available_draft_po_ids[0]
1332 po_rec = po_obj.browse(cr, uid, po_id, context=context)
1333 #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
1334 if datetime.strptime(po_rec.date_order, DEFAULT_SERVER_DATETIME_FORMAT) > purchase_date:
1335 po_obj.write(cr, uid, [po_id], {'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
1336 #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
1337 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)
1338 if available_po_line_ids:
1339 po_line = po_line_obj.browse(cr, uid, available_po_line_ids[0], context=context)
1340 po_line_obj.write(cr, SUPERUSER_ID, po_line.id, {'product_qty': po_line.product_qty + line_vals['product_qty']}, context=context)
1341 po_line_id = po_line.id
1342 sum_po_line_ids.append(procurement.id)
1344 line_vals.update(order_id=po_id)
1345 po_line_id = po_line_obj.create(cr, SUPERUSER_ID, line_vals, context=context)
1346 linked_po_ids.append(procurement.id)
1348 name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1351 'origin': procurement.origin,
1352 'partner_id': partner.id,
1353 'location_id': procurement.location_id.id,
1354 'picking_type_id': procurement.rule_id.picking_type_id.id,
1355 'pricelist_id': partner.property_product_pricelist_purchase.id,
1356 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1357 'company_id': procurement.company_id.id,
1358 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False,
1359 'payment_term_id': partner.property_supplier_payment_term.id or False,
1360 'dest_address_id': procurement.partner_dest_id.id,
1362 po_id = self.create_procurement_purchase_order(cr, SUPERUSER_ID, procurement, po_vals, line_vals, context=context)
1363 po_line_id = po_obj.browse(cr, uid, po_id, context=context).order_line[0].id
1364 pass_ids.append(procurement.id)
1365 res[procurement.id] = po_line_id
1366 self.write(cr, uid, [procurement.id], {'purchase_line_id': po_line_id}, context=context)
1368 self.message_post(cr, uid, pass_ids, body=_("Draft Purchase Order created"), context=context)
1370 self.message_post(cr, uid, linked_po_ids, body=_("Purchase line created and linked to an existing Purchase Order"), context=context)
1372 self.message_post(cr, uid, sum_po_line_ids, body=_("Quantity added in existing Purchase Order Line"), context=context)
1376 class mail_mail(osv.Model):
1378 _inherit = 'mail.mail'
1380 def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
1381 if mail_sent and mail.model == 'purchase.order':
1382 obj = self.pool.get('purchase.order').browse(cr, uid, mail.res_id, context=context)
1383 if obj.state == 'draft':
1384 self.pool.get('purchase.order').signal_workflow(cr, uid, [mail.res_id], 'send_rfq')
1385 return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
1388 class product_template(osv.Model):
1389 _name = 'product.template'
1390 _inherit = 'product.template'
1392 def _get_buy_route(self, cr, uid, context=None):
1394 buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy')
1399 def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
1400 res = dict.fromkeys(ids, 0)
1401 for template in self.browse(cr, uid, ids, context=context):
1402 res[template.id] = sum([p.purchase_count for p in template.product_variant_ids])
1406 'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
1407 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
1412 'route_ids': _get_buy_route,
1415 def action_view_purchases(self, cr, uid, ids, context=None):
1416 products = self._get_products(cr, uid, ids, context=context)
1417 result = self._get_act_window_dict(cr, uid, 'purchase.action_purchase_line_product_tree', context=context)
1418 result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
1421 class product_product(osv.Model):
1422 _name = 'product.product'
1423 _inherit = 'product.product'
1425 def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
1426 Purchase = self.pool['purchase.order']
1428 product_id: Purchase.search_count(cr,uid, [('order_line.product_id', '=', product_id)], context=context)
1429 for product_id in ids
1433 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
1438 class mail_compose_message(osv.Model):
1439 _inherit = 'mail.compose.message'
1441 def send_mail(self, cr, uid, ids, context=None):
1442 context = context or {}
1443 if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
1444 context = dict(context, mail_post_autofollow=True)
1445 self.pool.get('purchase.order').signal_workflow(cr, uid, [context['default_res_id']], 'send_rfq')
1446 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1449 class account_invoice(osv.Model):
1450 """ Override account_invoice to add Chatter messages on the related purchase
1451 orders, logging the invoice receipt or payment. """
1452 _inherit = 'account.invoice'
1454 def invoice_validate(self, cr, uid, ids, context=None):
1455 res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context)
1456 purchase_order_obj = self.pool.get('purchase.order')
1457 # read access on purchase.order object is not required
1458 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1459 user_id = SUPERUSER_ID
1462 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1463 for order in purchase_order_obj.browse(cr, uid, po_ids, context=context):
1464 purchase_order_obj.message_post(cr, user_id, order.id, body=_("Invoice received"), context=context)
1466 for po_line in order.order_line:
1467 if any(line.invoice_id.state not in ['draft', 'cancel'] for line in po_line.invoice_lines):
1468 invoiced.append(po_line.id)
1470 self.pool['purchase.order.line'].write(cr, uid, invoiced, {'invoiced': True})
1471 workflow.trg_write(uid, 'purchase.order', order.id, cr)
1474 def confirm_paid(self, cr, uid, ids, context=None):
1475 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1476 purchase_order_obj = self.pool.get('purchase.order')
1477 # read access on purchase.order object is not required
1478 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1479 user_id = SUPERUSER_ID
1482 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1483 for po_id in po_ids:
1484 purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice paid"), context=context)
1487 class account_invoice_line(osv.Model):
1488 """ Override account_invoice_line to add the link to the purchase order line it is related to"""
1489 _inherit = 'account.invoice.line'
1491 'purchase_line_id': fields.many2one('purchase.order.line',
1492 'Purchase Order Line', ondelete='set null', select=True,
1497 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: