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, browse_null
32 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
34 class purchase_order(osv.osv):
36 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
38 cur_obj=self.pool.get('res.currency')
39 for order in self.browse(cr, uid, ids, context=context):
41 'amount_untaxed': 0.0,
46 cur = order.pricelist_id.currency_id
47 for line in order.order_line:
48 val1 += line.price_subtotal
49 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']:
50 val += c.get('amount', 0.0)
51 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
52 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
53 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
56 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
57 if not value: return False
58 if type(ids)!=type([]):
60 for po in self.browse(cr, uid, ids, context=context):
62 cr.execute("""update purchase_order_line set
66 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
67 cr.execute("""update purchase_order set
68 minimum_planned_date=%s where id=%s""", (value, po.id))
71 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
73 purchase_obj=self.browse(cr, uid, ids, context=context)
74 for purchase in purchase_obj:
75 res[purchase.id] = False
76 if purchase.order_line:
77 min_date=purchase.order_line[0].date_planned
78 for line in purchase.order_line:
79 if line.date_planned < min_date:
80 min_date=line.date_planned
81 res[purchase.id]=min_date
85 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
87 for purchase in self.browse(cursor, user, ids, context=context):
89 for invoice in purchase.invoice_ids:
90 if invoice.state not in ('draft','cancel'):
91 tot += invoice.amount_untaxed
92 if purchase.amount_untaxed:
93 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
95 res[purchase.id] = 0.0
98 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
104 p.order_id, sum(m.product_qty), m.state
108 purchase_order_line p on (p.id=m.purchase_line_id)
110 p.order_id IN %s GROUP BY m.state, p.order_id''',(tuple(ids),))
111 for oid,nbr,state in cr.fetchall():
115 res[oid][0] += nbr or 0.0
116 res[oid][1] += nbr or 0.0
118 res[oid][1] += nbr or 0.0
123 res[r] = 100.0 * res[r][0] / res[r][1]
126 def _get_order(self, cr, uid, ids, context=None):
128 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
129 result[line.order_id.id] = True
132 def _invoiced(self, cursor, user, ids, name, arg, context=None):
134 for purchase in self.browse(cursor, user, ids, context=context):
136 if purchase.invoiced_rate == 100.00:
138 res[purchase.id] = invoiced
141 def _get_journal(self, cr, uid, context=None):
144 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
145 company_id = context.get('company_id', user.company_id.id)
146 journal_obj = self.pool.get('account.journal')
147 res = journal_obj.search(cr, uid, [('type', '=', 'purchase'),
148 ('company_id', '=', company_id)],
150 return res and res[0] or False
152 def _get_picking_in(self, cr, uid, context=None):
153 obj_data = self.pool.get('ir.model.data')
154 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
156 def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
161 SELECT picking_id, po.id FROM stock_picking p, stock_move m, purchase_order_line pol, purchase_order po
162 WHERE po.id in %s and po.id = pol.order_id and pol.id = m.purchase_line_id and m.picking_id = p.id
163 GROUP BY picking_id, po.id
166 cr.execute(query, (tuple(ids), ))
167 picks = cr.fetchall()
168 for pick_id, po_id in picks:
169 res[po_id].append(pick_id)
173 ('draft', 'Draft PO'),
175 ('bid', 'Bid Received'),
176 ('confirmed', 'Waiting Approval'),
177 ('approved', 'Purchase Confirmed'),
178 ('except_picking', 'Shipping Exception'),
179 ('except_invoice', 'Invoice Exception'),
181 ('cancel', 'Cancelled')
185 'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirmed',
186 'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'approved',
187 'purchase.mt_rfq_done': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
191 'name': fields.char('Order Reference', size=64, required=True, select=True, help="Unique number of the purchase order, computed automatically when the purchase order is created."),
192 'origin': fields.char('Source Document', size=64,
193 help="Reference of the document that generated this purchase order request; a sales order or an internal procurement request."
195 'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, size=64,
196 help="Reference of the sales order or bid sent by your supplier. It's mainly used to do the matching when you receive the products as this reference is usually written on the delivery order sent by your supplier."),
197 'date_order':fields.date('Order Date', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, select=True, help="Date on which this document has been created."),
198 'date_approve':fields.date('Date Approved', readonly=1, select=True, help="Date on which purchase order has been approved"),
199 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
200 change_default=True, track_visibility='always'),
201 'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
202 states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
203 help="Put an address if you want to deliver directly from the supplier to the customer. " \
204 "Otherwise, keep empty to deliver to your own company."
206 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]} ),
207 '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."),
208 'currency_id': fields.many2one('res.currency','Currency', readonly=True, required=True,states={'draft': [('readonly', False)],'sent': [('readonly', False)]}),
209 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="The status of the purchase order or the quotation request. A request for quotation is a purchase order in a 'Draft' status. Then the order has to be confirmed by the user, the status switch to 'Confirmed'. Then the supplier must confirm the order to change the status to 'Approved'. When the purchase order is paid and received, the status becomes 'Done'. If a cancel action occurs in the invoice or in the reception of goods, the status becomes in exception.", select=True),
210 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
211 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
212 'notes': fields.text('Terms and Conditions'),
213 'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id', 'invoice_id', 'Invoices', help="Invoices generated for a purchase order"),
214 'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking List', help="This is the list of reception operations that have been generated for this purchase order."),
215 'shipped':fields.boolean('Received', readonly=True, select=True, help="It indicates that a picking has been done"),
216 'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'),
217 'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', help="It indicates that an invoice has been paid"),
218 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
219 '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,
220 readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)]},
221 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" \
222 "Based on generated invoice: create a draft invoice you can validate later.\n" \
223 "Based on incoming shipments: let you create an invoice when receptions are validated."
225 '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.",
227 'purchase.order.line': (_get_order, ['date_planned'], 10),
230 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
232 'purchase.order.line': (_get_order, None, 10),
233 }, multi="sums", help="The amount without tax", track_visibility='always'),
234 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
236 'purchase.order.line': (_get_order, None, 10),
237 }, multi="sums", help="The tax amount"),
238 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
240 'purchase.order.line': (_get_order, None, 10),
241 }, multi="sums", help="The total amount"),
242 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
243 'payment_term_id': fields.many2one('account.payment.term', 'Payment Term'),
244 'incoterm_id': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
245 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
246 'create_uid': fields.many2one('res.users', 'Responsible'),
247 'company_id': fields.many2one('res.company', 'Company', required=True, select=1, states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)]}),
248 'journal_id': fields.many2one('account.journal', 'Journal'),
249 'bid_date': fields.date('Bid Received On', readonly=True, help="Date on which the bid was received"),
250 'bid_validity': fields.date('Bid Valid Until', help="Date on which the bid expired"),
251 'picking_type_id': fields.many2one('stock.picking.type', 'Deliver To', help="This will determine picking type of incoming shipment", required=True,
252 states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}),
253 'related_location_id': fields.related('picking_type_id', 'default_location_dest_id', type='many2one', relation='stock.location', string="Related location", store=True),
256 'date_order': fields.date.context_today,
258 'name': lambda obj, cr, uid, context: '/',
260 'invoice_method': 'order',
262 '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,
263 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
264 'journal_id': _get_journal,
265 'currency_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id,
266 'picking_type_id': _get_picking_in,
269 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
271 _name = "purchase.order"
272 _inherit = ['mail.thread', 'ir.needaction_mixin']
273 _description = "Purchase Order"
274 _order = 'date_order desc, id desc'
276 def create(self, cr, uid, vals, context=None):
277 if vals.get('name','/')=='/':
278 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'purchase.order') or '/'
281 context.update({'mail_create_nolog': True})
282 order = super(purchase_order, self).create(cr, uid, vals, context=context)
283 self.message_post(cr, uid, [order], body=_("RFQ created"), context=context)
286 def unlink(self, cr, uid, ids, context=None):
287 purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
289 for s in purchase_orders:
290 if s['state'] in ['draft','cancel']:
291 unlink_ids.append(s['id'])
293 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.'))
295 # automatically sending subflow.delete upon deletion
296 self.signal_purchase_cancel(cr, uid, unlink_ids)
298 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
300 def set_order_line_status(self, cr, uid, ids, status, context=None):
301 line = self.pool.get('purchase.order.line')
302 for order in self.browse(cr, uid, ids, context=context):
303 order_line_ids = [order_line.id for order_line in order.order_line]
304 line.write(cr, uid, order_line_ids, {'state': status}, context=context)
307 def button_dummy(self, cr, uid, ids, context=None):
310 def onchange_pricelist(self, cr, uid, ids, pricelist_id, context=None):
313 return {'value': {'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id}}
315 #Destination address is used when dropshipping
316 def onchange_dest_address_id(self, cr, uid, ids, address_id):
319 address = self.pool.get('res.partner')
321 supplier = address.browse(cr, uid, address_id)
323 location_id = supplier.property_stock_customer.id
324 values.update({'location_id': location_id})
325 return {'value':values}
327 def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None):
330 picktype = self.pool.get("stock.picking.type").browse(cr, uid, picking_type_id, context=context)
331 if picktype.default_location_dest_id:
332 value.update({'location_id': picktype.default_location_dest_id.id})
333 value.update({'related_location_id': picktype.default_location_dest_id and picktype.default_location_dest_id.id or False})
334 return {'value': value}
336 def onchange_partner_id(self, cr, uid, ids, partner_id):
337 partner = self.pool.get('res.partner')
340 'fiscal_position': False,
341 'payment_term_id': False,
343 supplier_address = partner.address_get(cr, uid, [partner_id], ['default'])
344 supplier = partner.browse(cr, uid, partner_id)
346 'pricelist_id': supplier.property_product_pricelist_purchase.id,
347 'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
348 'payment_term_id': supplier.property_supplier_payment_term.id or False,
351 def invoice_open(self, cr, uid, ids, context=None):
352 mod_obj = self.pool.get('ir.model.data')
353 act_obj = self.pool.get('ir.actions.act_window')
355 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree2')
356 id = result and result[1] or False
357 result = act_obj.read(cr, uid, [id], context=context)[0]
359 for po in self.browse(cr, uid, ids, context=context):
360 inv_ids+= [invoice.id for invoice in po.invoice_ids]
362 raise osv.except_osv(_('Error!'), _('Please create Invoices.'))
363 #choose the view_mode accordingly
365 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
367 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
368 result['views'] = [(res and res[1] or False, 'form')]
369 result['res_id'] = inv_ids and inv_ids[0] or False
372 def view_invoice(self, cr, uid, ids, context=None):
374 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.
376 mod_obj = self.pool.get('ir.model.data')
377 wizard_obj = self.pool.get('purchase.order.line_invoice')
378 #compute the number of invoices to display
380 for po in self.browse(cr, uid, ids, context=context):
381 if po.invoice_method == 'manual':
382 if not po.invoice_ids:
383 context.update({'active_ids' : [line.id for line in po.order_line]})
384 wizard_obj.makeInvoices(cr, uid, [], context=context)
386 for po in self.browse(cr, uid, ids, context=context):
387 inv_ids+= [invoice.id for invoice in po.invoice_ids]
388 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
389 res_id = res and res[1] or False
392 'name': _('Supplier Invoices'),
396 'res_model': 'account.invoice',
397 'context': "{'type':'in_invoice', 'journal_type': 'purchase'}",
398 'type': 'ir.actions.act_window',
401 'res_id': inv_ids and inv_ids[0] or False,
404 def view_picking(self, cr, uid, ids, context=None):
406 This function returns an action that display existing picking orders of given purchase order ids.
410 mod_obj = self.pool.get('ir.model.data')
411 dummy, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree'))
412 action = self.pool.get('ir.actions.act_window').read(cr, uid, action_id, context=context)
415 #TODO: might need to change this function in order to return the whole set of operations and not only the reception(s) to input
416 for po in self.browse(cr, uid, ids, context=context):
417 pick_ids += [picking.id for picking in po.picking_ids]
419 #override the context to get rid of the default filtering on picking type
420 action['context'] = {}
421 #choose the view_mode accordingly
422 if len(pick_ids) > 1:
423 action['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
425 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
426 action['views'] = [(res and res[1] or False, 'form')]
427 action['res_id'] = pick_ids and pick_ids[0] or False
430 def wkf_approve_order(self, cr, uid, ids, context=None):
431 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)})
434 def wkf_bid_received(self, cr, uid, ids, context=None):
435 return self.write(cr, uid, ids, {'state':'bid', 'bid_date': fields.date.context_today(self,cr,uid,context=context)})
437 def wkf_send_rfq(self, cr, uid, ids, context=None):
439 This function opens a window to compose an email, with the edi purchase template message loaded by default
443 ir_model_data = self.pool.get('ir.model.data')
445 if context.get('send_rfq', False):
446 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
448 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase_done')[1]
452 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
454 compose_form_id = False
457 'default_model': 'purchase.order',
458 'default_res_id': ids[0],
459 'default_use_template': bool(template_id),
460 'default_template_id': template_id,
461 'default_composition_mode': 'comment',
464 'name': _('Compose Email'),
465 'type': 'ir.actions.act_window',
468 'res_model': 'mail.compose.message',
469 'views': [(compose_form_id, 'form')],
470 'view_id': compose_form_id,
475 def print_quotation(self, cr, uid, ids, context=None):
477 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
479 assert len(ids) == 1, 'This option should only be used for a single id at a time'
480 self.signal_send_rfq(cr, uid, ids)
481 return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
483 #TODO: implement messages system
484 def wkf_confirm_order(self, cr, uid, ids, context=None):
486 for po in self.browse(cr, uid, ids, context=context):
487 if not po.order_line:
488 raise osv.except_osv(_('Error!'),_('You cannot confirm a purchase order without any purchase order line.'))
489 for line in po.order_line:
490 if line.state=='draft':
492 self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
494 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
497 def _choose_account_from_po_line(self, cr, uid, po_line, context=None):
498 fiscal_obj = self.pool.get('account.fiscal.position')
499 property_obj = self.pool.get('ir.property')
500 if po_line.product_id:
501 acc_id = po_line.product_id.property_account_expense.id
503 acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
505 raise osv.except_osv(_('Error!'), _('Define expense account for this company: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
507 acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context=context).id
508 fpos = po_line.order_id.fiscal_position or False
509 return fiscal_obj.map_account(cr, uid, fpos, acc_id)
511 def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
512 """Collects require data from purchase order line that is used to create invoice line
513 for that purchase order line
514 :param account_id: Expense account of the product of PO line if any.
515 :param browse_record order_line: Purchase order line browse record
516 :return: Value for fields of invoice lines.
520 'name': order_line.name,
521 'account_id': account_id,
522 'price_unit': order_line.price_unit or 0.0,
523 'quantity': order_line.product_qty,
524 'product_id': order_line.product_id.id or False,
525 'uos_id': order_line.product_uom.id or False,
526 'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
527 'account_analytic_id': order_line.account_analytic_id.id or False,
528 'purchase_line_id': order_line.id,
531 def action_cancel_draft(self, cr, uid, ids, context=None):
534 self.write(cr, uid, ids, {'state':'draft','shipped':0})
535 self.set_order_line_status(cr, uid, ids, 'draft', context=context)
537 # Deleting the existing instance of workflow for PO
538 self.delete_workflow(cr, uid, [p_id]) # TODO is it necessary to interleave the calls?
539 self.create_workflow(cr, uid, [p_id])
542 def wkf_po_done(self, cr, uid, ids, context=None):
543 self.write(cr, uid, ids, {'state': 'done'}, context=context)
544 self.set_order_line_status(cr, uid, ids, 'done', context=context)
546 def action_invoice_create(self, cr, uid, ids, context=None):
547 """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
548 :param ids: list of ids of purchase orders.
549 :return: ID of created invoice.
554 journal_obj = self.pool.get('account.journal')
555 inv_obj = self.pool.get('account.invoice')
556 inv_line_obj = self.pool.get('account.invoice.line')
559 uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
560 for order in self.browse(cr, uid, ids, context=context):
561 context.pop('force_company', None)
562 if order.company_id.id != uid_company_id:
563 #if the company of the document is different than the current user company, force the company in the context
564 #then re-do a browse to read the property fields for the good company.
565 context['force_company'] = order.company_id.id
566 order = self.browse(cr, uid, order.id, context=context)
567 pay_acc_id = order.partner_id.property_account_payable.id
568 journal_ids = journal_obj.search(cr, uid, [('type', '=', 'purchase'), ('company_id', '=', order.company_id.id)], limit=1)
570 raise osv.except_osv(_('Error!'),
571 _('Define purchase journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
573 # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
575 for po_line in order.order_line:
576 acc_id = self._choose_account_from_po_line(cr, uid, po_line, context=context)
577 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
578 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
579 inv_lines.append(inv_line_id)
581 po_line.write({'invoiced': True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
583 # get invoice data and create invoice
585 'name': order.partner_ref or order.name,
586 'reference': order.partner_ref or order.name,
587 'account_id': pay_acc_id,
588 'type': 'in_invoice',
589 'partner_id': order.partner_id.id,
590 'currency_id': order.currency_id.id,
591 'journal_id': len(journal_ids) and journal_ids[0] or False,
592 'invoice_line': [(6, 0, inv_lines)],
593 'origin': order.name,
594 'fiscal_position': order.fiscal_position.id or False,
595 'payment_term': order.payment_term_id.id or False,
596 'company_id': order.company_id.id,
598 inv_id = inv_obj.create(cr, uid, inv_data, context=context)
600 # compute the invoice
601 inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
603 # Link this new invoice to related purchase order
604 order.write({'invoice_ids': [(4, inv_id)]}, context=context)
608 def invoice_done(self, cr, uid, ids, context=None):
609 self.write(cr, uid, ids, {'state': 'approved'}, context=context)
612 def has_stockable_product(self, cr, uid, ids, *args):
613 for order in self.browse(cr, uid, ids):
614 for order_line in order.order_line:
615 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
619 def wkf_action_cancel(self, cr, uid, ids, context=None):
620 self.write(cr, uid, ids, {'state': 'cancel'})
621 self.set_order_line_status(cr, uid, ids, 'cancel', context=context)
623 def action_cancel(self, cr, uid, ids, context=None):
624 for purchase in self.browse(cr, uid, ids, context=context):
625 for pick in purchase.picking_ids:
626 if pick.state not in ('draft', 'cancel'):
627 raise osv.except_osv(
628 _('Unable to cancel the purchase order %s.') % (purchase.name),
629 _('First cancel all receptions related to this purchase order.'))
630 self.pool.get('stock.picking') \
631 .signal_button_cancel(cr, uid, map(attrgetter('id'), purchase.picking_ids))
632 for inv in purchase.invoice_ids:
633 if inv and inv.state not in ('cancel', 'draft'):
634 raise osv.except_osv(
635 _('Unable to cancel this purchase order.'),
636 _('You must first cancel all receptions related to this purchase order.'))
637 self.pool.get('account.invoice') \
638 .signal_invoice_cancel(cr, uid, map(attrgetter('id'), purchase.invoice_ids))
639 self.write(cr, uid, ids, {'state': 'cancel'})
640 self.set_order_line_status(cr, uid, ids, 'cancel', context=context)
641 self.signal_purchase_cancel(cr, uid, ids)
644 def date_to_datetime(self, cr, uid, userdate, context=None):
645 """ Convert date values expressed in user's timezone to
646 server-side UTC timestamp, assuming a default arbitrary
647 time of 12:00 AM - because a time is needed.
649 :param str userdate: date string in in user time zone
650 :return: UTC datetime string for server-side use
652 # TODO: move to fields.datetime in server after 7.0
653 user_date = datetime.strptime(userdate, DEFAULT_SERVER_DATE_FORMAT)
654 if context and context.get('tz'):
655 tz_name = context['tz']
657 tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
659 utc = pytz.timezone('UTC')
660 context_tz = pytz.timezone(tz_name)
661 user_datetime = user_date + relativedelta(hours=12.0)
662 local_timestamp = context_tz.localize(user_datetime, is_dst=False)
663 user_datetime = local_timestamp.astimezone(utc)
664 return user_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
665 return user_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
667 def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
668 ''' 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()'''
669 product_uom = self.pool.get('product.uom')
670 price_unit = order_line.price_unit
671 if order_line.product_uom.id != order_line.product_id.uom_id.id:
672 price_unit *= order_line.product_uom.factor
673 if order.currency_id.id != order.company_id.currency_id.id:
674 #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency
675 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)
678 'name': order_line.name or '',
679 'product_id': order_line.product_id.id,
680 'product_uom': order_line.product_uom.id,
681 'product_uos': order_line.product_uom.id,
682 'date': self.date_to_datetime(cr, uid, order.date_order, context),
683 'date_expected': self.date_to_datetime(cr, uid, order_line.date_planned, context),
684 'location_id': order.partner_id.property_stock_supplier.id,
685 'location_dest_id': order.location_id.id,
686 'picking_id': picking_id,
687 'partner_id': order.dest_address_id.id or order.partner_id.id,
688 'move_dest_id': False,
690 'purchase_line_id': order_line.id,
691 'company_id': order.company_id.id,
692 'price_unit': price_unit,
693 'picking_type_id': order.picking_type_id.id,
694 'group_id': group_id,
695 'procurement_id': False,
696 'origin': order.name,
697 '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 [],
698 'warehouse_id':order.picking_type_id.warehouse_id.id,
701 diff_quantity = order_line.product_qty
702 for procurement in order_line.procurement_ids:
703 procurement_qty = product_uom._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, to_uom_id=order_line.product_uom.id)
704 tmp = move_template.copy()
706 'product_uom_qty': min(procurement_qty, diff_quantity),
707 'product_uos_qty': min(procurement_qty, diff_quantity),
708 'move_dest_id': procurement.move_dest_id.id, # blabla
709 'group_id': procurement.group_id.id or group_id, # blabla to check ca devrait etre bon et groupé dans le meme picking qd meme
710 'procurement_id': procurement.id,
712 diff_quantity -= min(procurement_qty, diff_quantity)
714 #if the order line has a bigger quantity than the procurement it was for (manually changed or minimal quantity), then
715 #split the future stock move in two because the route followed may be different.
716 if diff_quantity > 0:
717 move_template['product_uom_qty'] = diff_quantity
718 move_template['product_uos_qty'] = diff_quantity
719 res.append(move_template)
722 def _create_stock_moves(self, cr, uid, order, order_lines, picking_id=False, context=None):
723 """Creates appropriate stock moves for given order lines, whose can optionally create a
724 picking if none is given or no suitable is found, then confirms the moves, makes them
725 available, and confirms the pickings.
727 If ``picking_id`` is provided, the stock moves will be added to it, otherwise a standard
728 incoming picking will be created to wrap the stock moves (default behavior of the stock.move)
730 Modules that wish to customize the procurements or partition the stock moves over
731 multiple stock pickings may override this method and call ``super()`` with
732 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
734 :param browse_record order: purchase order to which the order lines belong
735 :param list(browse_record) order_lines: purchase order line records for which picking
736 and moves should be created.
737 :param int picking_id: optional ID of a stock picking to which the created stock moves
738 will be added. A new picking will be created if omitted.
741 stock_move = self.pool.get('stock.move')
743 new_group = self.pool.get("procurement.group").create(cr, uid, {'name': order.name, 'partner_id': order.partner_id.id}, context=context)
745 for order_line in order_lines:
746 if not order_line.product_id:
749 if order_line.product_id.type in ('product', 'consu'):
750 for vals in self._prepare_order_line_move(cr, uid, order, order_line, picking_id, new_group, context=context):
751 move = stock_move.create(cr, uid, vals, context=context)
752 todo_moves.append(move)
754 todo_moves = stock_move.action_confirm(cr, uid, todo_moves)
755 stock_move.force_assign(cr, uid, todo_moves)
757 def test_moves_done(self, cr, uid, ids, context=None):
758 '''PO is done at the delivery side if all the incoming shipments are done'''
759 for purchase in self.browse(cr, uid, ids, context=context):
760 for picking in purchase.picking_ids:
761 if picking.state != 'done':
765 def test_moves_except(self, cr, uid, ids, context=None):
766 ''' PO is in exception at the delivery side if one of the picking is canceled
767 and the other pickings are completed (done or canceled)
769 at_least_one_canceled = False
770 alldoneorcancel = True
771 for purchase in self.browse(cr, uid, ids, context=context):
772 for picking in purchase.picking_ids:
773 if picking.state == 'cancel':
774 at_least_one_canceled = True
775 if picking.state not in ['done', 'cancel']:
776 alldoneorcancel = False
777 return at_least_one_canceled and alldoneorcancel
779 def move_lines_get(self, cr, uid, ids, *args):
781 for order in self.browse(cr, uid, ids, context={}):
782 for line in order.order_line:
783 res += [x.id for x in line.move_ids]
786 def action_picking_create(self, cr, uid, ids, context=None):
787 for order in self.browse(cr, uid, ids):
788 picking_id = self.pool.get('stock.picking').create(cr, uid, {'picking_type_id': order.picking_type_id.id, 'partner_id': order.dest_address_id.id or order.partner_id.id}, context=context)
789 self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
791 def picking_done(self, cr, uid, ids, context=None):
792 self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
793 self.message_post(cr, uid, ids, body=_("Products received"), context=context)
796 def copy(self, cr, uid, id, default=None, context=None):
806 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
808 return super(purchase_order, self).copy(cr, uid, id, default, context)
810 def do_merge(self, cr, uid, ids, context=None):
812 To merge similar type of purchase orders.
813 Orders will only be merged if:
814 * Purchase Orders are in draft
815 * Purchase Orders belong to the same partner
816 * Purchase Orders are have same stock location, same pricelist
817 Lines will only be merged if:
818 * Order lines are exactly the same except for the quantity and unit
820 @param self: The object pointer.
821 @param cr: A database cursor
822 @param uid: ID of the user currently logged in
823 @param ids: the ID or list of IDs
824 @param context: A standard dictionary
826 @return: new purchase order id
829 #TOFIX: merged order line should be unlink
830 def make_key(br, fields):
833 field_val = getattr(br, field)
834 if field in ('product_id', 'account_analytic_id'):
837 if isinstance(field_val, browse_record):
838 field_val = field_val.id
839 elif isinstance(field_val, browse_null):
841 elif isinstance(field_val, list):
842 field_val = ((6, 0, tuple([v.id for v in field_val])),)
843 list_key.append((field, field_val))
845 return tuple(list_key)
850 # Compute what the new orders should contain
853 order_lines_to_move = []
854 for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
855 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
856 new_order = new_orders.setdefault(order_key, ({}, []))
857 new_order[1].append(porder.id)
858 order_infos = new_order[0]
862 'origin': porder.origin,
863 'date_order': porder.date_order,
864 'partner_id': porder.partner_id.id,
865 'dest_address_id': porder.dest_address_id.id,
866 'picking_type_id': porder.picking_type_id.id,
867 'location_id': porder.location_id.id,
868 'pricelist_id': porder.pricelist_id.id,
871 'notes': '%s' % (porder.notes or '',),
872 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
875 if porder.date_order < order_infos['date_order']:
876 order_infos['date_order'] = porder.date_order
878 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
880 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
882 for order_line in porder.order_line:
883 order_lines_to_move += [order_line.id]
887 for order_key, (order_data, old_ids) in new_orders.iteritems():
888 # skip merges with only one order
890 allorders += (old_ids or [])
893 # cleanup order line data
894 for key, value in order_data['order_line'].iteritems():
895 del value['uom_factor']
896 value.update(dict(key))
897 order_data['order_line'] = [(6, 0, order_lines_to_move)]
899 # create the new order
900 context.update({'mail_create_nolog': True})
901 neworder_id = self.create(cr, uid, order_data)
902 self.message_post(cr, uid, [neworder_id], body=_("RFQ created"), context=context)
903 orders_info.update({neworder_id: old_ids})
904 allorders.append(neworder_id)
906 # make triggers pointing to the old orders point to the new order
907 for old_id in old_ids:
908 self.redirect_workflow(cr, uid, [(old_id, neworder_id)])
909 self.signal_purchase_cancel(cr, uid, [old_id])
914 class purchase_order_line(osv.osv):
915 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
917 cur_obj=self.pool.get('res.currency')
918 tax_obj = self.pool.get('account.tax')
919 for line in self.browse(cr, uid, ids, context=context):
920 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, line.order_id.partner_id)
921 cur = line.order_id.pricelist_id.currency_id
922 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
925 def _get_uom_id(self, cr, uid, context=None):
927 proxy = self.pool.get('ir.model.data')
928 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
930 except Exception, ex:
934 'name': fields.text('Description', required=True),
935 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
936 'date_planned': fields.date('Scheduled Date', required=True, select=True),
937 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
938 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
939 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
940 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
941 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
942 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
943 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
944 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
945 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
946 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
947 help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
948 \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
949 \n* The \'Done\' status is set automatically when purchase order is set as done. \
950 \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
951 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
952 'invoiced': fields.boolean('Invoiced', readonly=True),
953 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
954 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date"),
955 'procurement_ids': fields.one2many('procurement.order', 'purchase_line_id', string='Associated procurements'),
958 'product_uom' : _get_uom_id,
959 'product_qty': lambda *a: 1.0,
960 'state': lambda *args: 'draft',
961 'invoiced': lambda *a: 0,
963 _table = 'purchase_order_line'
964 _name = 'purchase.order.line'
965 _description = 'Purchase Order Line'
967 def copy_data(self, cr, uid, id, default=None, context=None):
970 default.update({'state':'draft', 'move_ids':[], 'invoiced':0, 'invoice_lines':[], 'procurement_ids': False})
971 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
973 def unlink(self, cr, uid, ids, context=None):
974 procurement_obj = self.pool.get('procurement.order')
975 procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context)
976 if procurement_ids_to_cancel:
977 self.pool['procurement.order'].cancel(cr, uid, procurement_ids_to_cancel)
978 return super(purchase_order_line, self).unlink(cr, uid, ids, context=context)
980 def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
981 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
982 name=False, price_unit=False, state='draft', context=None):
984 onchange handler of product_uom.
989 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
990 context = dict(context, purchase_uom_check=True)
991 return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
992 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
993 name=name, price_unit=price_unit, state=state, context=context)
995 def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
996 """Return the datetime value to use as Schedule Date (``date_planned``) for
997 PO Lines that correspond to the given product.supplierinfo,
998 when ordered at `date_order_str`.
1000 :param browse_record | False supplier_info: product.supplierinfo, used to
1001 determine delivery delay (if False, default delay = 0)
1002 :param str date_order_str: date of order, as a string in
1003 DEFAULT_SERVER_DATE_FORMAT
1005 :return: desired Schedule Date for the PO line
1007 supplier_delay = int(supplier_info.delay) if supplier_info else 0
1008 return datetime.strptime(date_order_str, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=supplier_delay)
1010 def action_cancel(self, cr, uid, ids, context=None):
1011 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1012 for po_line in self.browse(cr, uid, ids, context=context):
1013 if all([l.state == 'cancel' for l in po_line.order_id.order_line]):
1014 self.pool.get('purchase.order').action_cancel(cr, uid, [po_line.order_id.id], context=context)
1016 def _check_product_uom_group(self, cr, uid, context=None):
1017 group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom')
1018 res = [user for user in group_uom.users if user.id == uid]
1019 return len(res) and True or False
1022 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1023 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1024 name=False, price_unit=False, state='draft', context=None):
1026 onchange handler of product_id.
1031 res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1035 product_product = self.pool.get('product.product')
1036 product_uom = self.pool.get('product.uom')
1037 res_partner = self.pool.get('res.partner')
1038 product_pricelist = self.pool.get('product.pricelist')
1039 account_fiscal_position = self.pool.get('account.fiscal.position')
1040 account_tax = self.pool.get('account.tax')
1042 # - check for the presence of partner_id and pricelist_id
1044 # raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
1045 #if not pricelist_id:
1046 # raise osv.except_osv(_('No Pricelist !'), _('Select a price list in the purchase order form before choosing a product.'))
1048 # - determine name and notes based on product in partner lang.
1049 context_partner = context.copy()
1051 lang = res_partner.browse(cr, uid, partner_id).lang
1052 context_partner.update( {'lang': lang, 'partner_id': partner_id} )
1053 product = product_product.browse(cr, uid, product_id, context=context_partner)
1054 #call name_get() with partner in the context to eventually match name and description in the seller_ids field
1055 dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0]
1056 if product.description_purchase:
1057 name += '\n' + product.description_purchase
1058 res['value'].update({'name': name})
1060 # - set a domain on product_uom
1061 res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
1063 # - check that uom and product uom belong to the same category
1064 product_uom_po_id = product.uom_po_id.id
1066 uom_id = product_uom_po_id
1068 if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
1069 if context.get('purchase_uom_check') and self._check_product_uom_group(cr, uid, context=context):
1070 res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
1071 uom_id = product_uom_po_id
1073 res['value'].update({'product_uom': uom_id})
1075 # - determine product_qty and date_planned based on seller info
1077 date_order = fields.date.context_today(self,cr,uid,context=context)
1080 supplierinfo = False
1081 for supplier in product.seller_ids:
1082 if partner_id and (supplier.name.id == partner_id):
1083 supplierinfo = supplier
1084 if supplierinfo.product_uom.id != uom_id:
1085 res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
1086 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
1087 if (qty or 0.0) < min_qty: # If the supplier quantity is greater than entered from user, set minimal.
1089 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)}
1091 dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1093 res['value'].update({'date_planned': date_planned or dt})
1095 res['value'].update({'product_qty': qty})
1098 if state not in ('sent','bid'):
1099 # - determine price_unit and taxes_id
1101 price = product_pricelist.price_get(cr, uid, [pricelist_id],
1102 product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order})[pricelist_id]
1104 price = product.standard_price
1106 taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
1107 fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
1108 taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
1109 res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
1113 product_id_change = onchange_product_id
1114 product_uom_change = onchange_product_uom
1116 def action_confirm(self, cr, uid, ids, context=None):
1117 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
1120 class procurement_rule(osv.osv):
1121 _inherit = 'procurement.rule'
1123 def _get_action(self, cr, uid, context=None):
1124 return [('buy', 'Buy')] + super(procurement_rule, self)._get_action(cr, uid, context=context)
1127 class procurement_order(osv.osv):
1128 _inherit = 'procurement.order'
1130 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
1131 'purchase_id': fields.related('purchase_line_id', 'order_id', type='many2one', relation='purchase.order', string='Purchase Order'),
1134 def propagate_cancel(self, cr, uid, procurement, context=None):
1135 if procurement.rule_id.action == 'buy' and procurement.purchase_line_id:
1136 purchase_line_obj = self.pool.get('purchase.order.line')
1137 if procurement.purchase_line_id.product_qty > procurement.product_qty and procurement.purchase_line_id.order_id.state == 'draft':
1138 purchase_line_obj.write(cr, uid, [procurement.purchase_line_id.id], {'product_qty': procurement.purchase_line_id.product_qty - procurement.product_qty}, context=context)
1140 purchase_line_obj.action_cancel(cr, uid, [procurement.purchase_line_id.id], context=context)
1141 return super(procurement_order, self).propagate_cancel(cr, uid, procurement, context=context)
1143 def _run(self, cr, uid, procurement, context=None):
1144 if procurement.rule_id and procurement.rule_id.action == 'buy':
1145 #make a purchase order for the procurement
1146 return self.make_po(cr, uid, [procurement.id], context=context)[procurement.id]
1147 return super(procurement_order, self)._run(cr, uid, procurement, context=context)
1149 def _check(self, cr, uid, procurement, context=None):
1150 if procurement.purchase_line_id and procurement.purchase_line_id.order_id.shipped: # TOCHECK: does it work for several deliveries?
1152 return super(procurement_order, self)._check(cr, uid, procurement, context=context)
1154 def _check_supplier_info(self, cr, uid, ids, context=None):
1155 ''' Check the supplier info field of a product and write an error message on the procurement if needed.
1156 Returns True if all needed information is there, False if some configuration mistake is detected.
1158 partner_obj = self.pool.get('res.partner')
1159 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1160 for procurement in self.browse(cr, uid, ids, context=context):
1162 partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
1164 if not procurement.product_id.seller_ids:
1165 message = _('No supplier defined for this product !')
1167 message = _('No default supplier defined for this product')
1168 elif not partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']:
1169 message = _('No address defined for the supplier')
1172 if procurement.message != message:
1173 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
1176 if user.company_id and user.company_id.partner_id:
1177 if partner.id == user.company_id.partner_id.id:
1178 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))
1182 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
1183 """Create the purchase order from the procurement, using
1184 the provided field values, after adding the given purchase
1185 order line in the purchase order.
1187 :params procurement: the procurement object generating the purchase order
1188 :params dict po_vals: field values for the new purchase order (the
1189 ``order_line`` field will be overwritten with one
1190 single line, as passed in ``line_vals``).
1191 :params dict line_vals: field values of the single purchase order line that
1192 the purchase order will contain.
1193 :return: id of the newly created purchase order
1196 po_vals.update({'order_line': [(0,0,line_vals)]})
1197 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
1199 def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
1200 """Return the datetime value to use as Schedule Date (``date_planned``) for the
1201 Purchase Order Lines created to satisfy the given procurement.
1203 :param browse_record procurement: the procurement for which a PO will be created.
1204 :param browse_report company: the company to which the new PO will belong to.
1206 :return: the desired Schedule Date for the PO lines
1208 procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
1209 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
1210 return schedule_date
1212 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
1213 """Return the datetime value to use as Order Date (``date_order``) for the
1214 Purchase Order created to satisfy the given procurement.
1216 :param browse_record procurement: the procurement for which a PO will be created.
1217 :param browse_report company: the company to which the new PO will belong to.
1218 :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1220 :return: the desired Order Date for the PO
1222 seller_delay = int(procurement.product_id.seller_delay)
1223 return schedule_date - relativedelta(days=seller_delay)
1225 def _get_product_supplier(self, cr, uid, procurement, context=None):
1226 ''' returns the main supplier of the procurement's product given as argument'''
1227 return procurement.product_id.seller_id
1229 def _get_po_line_values_from_proc(self, cr, uid, procurement, partner, company, schedule_date, context=None):
1232 uom_obj = self.pool.get('product.uom')
1233 pricelist_obj = self.pool.get('product.pricelist')
1234 prod_obj = self.pool.get('product.product')
1235 acc_pos_obj = self.pool.get('account.fiscal.position')
1237 seller_qty = procurement.product_id.seller_qty
1238 pricelist_id = partner.property_product_pricelist_purchase.id
1239 uom_id = procurement.product_id.uom_po_id.id
1240 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1242 qty = max(qty, seller_qty)
1243 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner.id, {'uom': uom_id})[pricelist_id]
1245 #Passing partner_id to context for purchase order line integrity of Line name
1246 new_context = context.copy()
1247 new_context.update({'lang': partner.lang, 'partner_id': partner.id})
1248 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
1249 taxes_ids = procurement.product_id.supplier_taxes_id
1250 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1251 name = product.partner_ref
1252 if product.description_purchase:
1253 name += '\n' + product.description_purchase
1258 'product_id': procurement.product_id.id,
1259 'product_uom': uom_id,
1260 'price_unit': price or 0.0,
1261 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1262 'taxes_id': [(6, 0, taxes)],
1265 def make_po(self, cr, uid, ids, context=None):
1266 """ 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.
1267 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)
1269 @return: dictionary giving for each procurement its related resolving PO line.
1272 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1273 po_obj = self.pool.get('purchase.order')
1274 po_line_obj = self.pool.get('purchase.order.line')
1275 seq_obj = self.pool.get('ir.sequence')
1278 sum_po_line_ids = []
1279 for procurement in self.browse(cr, uid, ids, context=context):
1280 partner = self._get_product_supplier(cr, uid, procurement, context=context)
1282 self.message_post(cr, uid, [procurement.id], _('There is no supplier associated to product %s') % (procurement.product_id.name))
1283 res[procurement.id] = False
1285 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1286 line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context)
1287 #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one
1288 available_draft_po_ids = po_obj.search(cr, uid, [
1289 ('partner_id', '=', partner.id), ('state', '=', 'draft'), ('picking_type_id', '=', procurement.rule_id.picking_type_id.id),
1290 ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context)
1291 if available_draft_po_ids:
1292 po_id = available_draft_po_ids[0]
1293 #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
1294 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)
1295 if available_po_line_ids:
1296 po_line = po_line_obj.browse(cr, uid, available_po_line_ids[0], context=context)
1297 po_line_obj.write(cr, SUPERUSER_ID, po_line.id, {'product_qty': po_line.product_qty + line_vals['product_qty']}, context=context)
1298 po_line_id = po_line.id
1299 sum_po_line_ids.append(procurement.id)
1301 line_vals.update(order_id=po_id)
1302 po_line_id = po_line_obj.create(cr, SUPERUSER_ID, line_vals, context=context)
1303 linked_po_ids.append(procurement.id)
1305 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1306 name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1309 'origin': procurement.origin,
1310 'partner_id': partner.id,
1311 'location_id': procurement.location_id.id,
1312 'picking_type_id': procurement.rule_id.picking_type_id.id,
1313 'pricelist_id': partner.property_product_pricelist_purchase.id,
1314 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1315 'company_id': procurement.company_id.id,
1316 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False,
1317 'payment_term_id': partner.property_supplier_payment_term.id or False,
1318 'dest_address_id': procurement.partner_dest_id.id,
1320 po_id = self.create_procurement_purchase_order(cr, SUPERUSER_ID, procurement, po_vals, line_vals, context=context)
1321 po_line_id = po_obj.browse(cr, uid, po_id, context=context).order_line[0].id
1322 pass_ids.append(procurement.id)
1323 res[procurement.id] = po_line_id
1324 self.write(cr, uid, [procurement.id], {'purchase_line_id': po_line_id}, context=context)
1326 self.message_post(cr, uid, pass_ids, body=_("Draft Purchase Order created"), context=context)
1328 self.message_post(cr, uid, linked_po_ids, body=_("Purchase line created and linked to an existing Purchase Order"), context=context)
1330 self.message_post(cr, uid, sum_po_line_ids, body=_("Quantity added in existing Purchase Order Line"), context=context)
1334 class mail_mail(osv.Model):
1336 _inherit = 'mail.mail'
1338 def _postprocess_sent_message(self, cr, uid, mail, context=None):
1339 if mail.model == 'purchase.order':
1340 obj = self.pool.get('purchase.order').browse(cr, uid, mail.res_id, context=context)
1341 if obj.state == 'draft':
1342 self.pool.get('purchase.order').signal_send_rfq(cr, uid, [mail.res_id])
1343 return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
1346 class product_template(osv.Model):
1347 _name = 'product.template'
1348 _inherit = 'product.template'
1350 'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
1357 class mail_compose_message(osv.Model):
1358 _inherit = 'mail.compose.message'
1360 def send_mail(self, cr, uid, ids, context=None):
1361 context = context or {}
1362 if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
1363 context = dict(context, mail_post_autofollow=True)
1364 self.pool.get('purchase.order').signal_send_rfq(cr, uid, [context['default_res_id']])
1365 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1368 class account_invoice(osv.Model):
1369 """ Override account_invoice to add Chatter messages on the related purchase
1370 orders, logging the invoice reception or payment. """
1371 _inherit = 'account.invoice'
1373 def invoice_validate(self, cr, uid, ids, context=None):
1374 res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context)
1375 purchase_order_obj = self.pool.get('purchase.order')
1376 # read access on purchase.order object is not required
1377 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1378 user_id = SUPERUSER_ID
1381 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1382 for po_id in po_ids:
1383 purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice received"), context=context)
1384 workflow.trg_write(uid, 'purchase.order', po_id, cr)
1387 def confirm_paid(self, cr, uid, ids, context=None):
1388 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1389 purchase_order_obj = self.pool.get('purchase.order')
1390 # read access on purchase.order object is not required
1391 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1392 user_id = SUPERUSER_ID
1395 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1396 for po_id in po_ids:
1397 purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice paid"), context=context)
1400 class account_invoice_line(osv.Model):
1401 """ Override account_invoice_line to add the link to the purchase order line it is related to"""
1402 _inherit = 'account.invoice.line'
1404 'purchase_line_id': fields.many2one('purchase.order.line',
1405 'Purchase Order Line', ondelete='set null', select=True,
1409 class product_product(osv.osv):
1410 _inherit = "product.product"
1412 def _get_buy_route(self, cr, uid, context=None):
1413 buy_route = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'purchase', 'route_warehouse0_buy')[1]
1417 'route_ids': _get_buy_route,
1420 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: