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 [],
700 diff_quantity = order_line.product_qty
701 for procurement in order_line.procurement_ids:
702 procurement_qty = product_uom._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, to_uom_id=order_line.product_uom.id)
703 tmp = move_template.copy()
705 'product_uom_qty': min(procurement_qty, diff_quantity),
706 'product_uos_qty': min(procurement_qty, diff_quantity),
707 'move_dest_id': procurement.move_dest_id.id, # blabla
708 'group_id': procurement.group_id.id or group_id, # blabla to check ca devrait etre bon et groupé dans le meme picking qd meme
709 'procurement_id': procurement.id,
711 diff_quantity -= min(procurement_qty, diff_quantity)
713 #if the order line has a bigger quantity than the procurement it was for (manually changed or minimal quantity), then
714 #split the future stock move in two because the route followed may be different.
715 if diff_quantity > 0:
716 move_template['product_uom_qty'] = diff_quantity
717 move_template['product_uos_qty'] = diff_quantity
718 res.append(move_template)
721 def _create_stock_moves(self, cr, uid, order, order_lines, picking_id=False, context=None):
722 """Creates appropriate stock moves for given order lines, whose can optionally create a
723 picking if none is given or no suitable is found, then confirms the moves, makes them
724 available, and confirms the pickings.
726 If ``picking_id`` is provided, the stock moves will be added to it, otherwise a standard
727 incoming picking will be created to wrap the stock moves (default behavior of the stock.move)
729 Modules that wish to customize the procurements or partition the stock moves over
730 multiple stock pickings may override this method and call ``super()`` with
731 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
733 :param browse_record order: purchase order to which the order lines belong
734 :param list(browse_record) order_lines: purchase order line records for which picking
735 and moves should be created.
736 :param int picking_id: optional ID of a stock picking to which the created stock moves
737 will be added. A new picking will be created if omitted.
740 stock_move = self.pool.get('stock.move')
742 new_group = self.pool.get("procurement.group").create(cr, uid, {'name': order.name, 'partner_id': order.partner_id.id}, context=context)
744 for order_line in order_lines:
745 if not order_line.product_id:
748 if order_line.product_id.type in ('product', 'consu'):
749 for vals in self._prepare_order_line_move(cr, uid, order, order_line, picking_id, new_group, context=context):
750 move = stock_move.create(cr, uid, vals, context=context)
751 todo_moves.append(move)
753 todo_moves = stock_move.action_confirm(cr, uid, todo_moves)
754 stock_move.force_assign(cr, uid, todo_moves)
756 def test_moves_done(self, cr, uid, ids, context=None):
757 '''PO is done at the delivery side if all the incoming shipments are done'''
758 for purchase in self.browse(cr, uid, ids, context=context):
759 for picking in purchase.picking_ids:
760 if picking.state != 'done':
764 def test_moves_except(self, cr, uid, ids, context=None):
765 ''' PO is in exception at the delivery side if one of the picking is canceled
766 and the other pickings are completed (done or canceled)
768 at_least_one_canceled = False
769 alldoneorcancel = True
770 for purchase in self.browse(cr, uid, ids, context=context):
771 for picking in purchase.picking_ids:
772 if picking.state == 'cancel':
773 at_least_one_canceled = True
774 if picking.state not in ['done', 'cancel']:
775 alldoneorcancel = False
776 return at_least_one_canceled and alldoneorcancel
778 def move_lines_get(self, cr, uid, ids, *args):
780 for order in self.browse(cr, uid, ids, context={}):
781 for line in order.order_line:
782 res += [x.id for x in line.move_ids]
785 def action_picking_create(self, cr, uid, ids, context=None):
786 for order in self.browse(cr, uid, ids):
787 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)
788 self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
790 def picking_done(self, cr, uid, ids, context=None):
791 self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
792 self.message_post(cr, uid, ids, body=_("Products received"), context=context)
795 def copy(self, cr, uid, id, default=None, context=None):
805 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
807 return super(purchase_order, self).copy(cr, uid, id, default, context)
809 def do_merge(self, cr, uid, ids, context=None):
811 To merge similar type of purchase orders.
812 Orders will only be merged if:
813 * Purchase Orders are in draft
814 * Purchase Orders belong to the same partner
815 * Purchase Orders are have same stock location, same pricelist
816 Lines will only be merged if:
817 * Order lines are exactly the same except for the quantity and unit
819 @param self: The object pointer.
820 @param cr: A database cursor
821 @param uid: ID of the user currently logged in
822 @param ids: the ID or list of IDs
823 @param context: A standard dictionary
825 @return: new purchase order id
828 #TOFIX: merged order line should be unlink
829 def make_key(br, fields):
832 field_val = getattr(br, field)
833 if field in ('product_id', 'account_analytic_id'):
836 if isinstance(field_val, browse_record):
837 field_val = field_val.id
838 elif isinstance(field_val, browse_null):
840 elif isinstance(field_val, list):
841 field_val = ((6, 0, tuple([v.id for v in field_val])),)
842 list_key.append((field, field_val))
844 return tuple(list_key)
849 # Compute what the new orders should contain
852 order_lines_to_move = []
853 for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
854 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
855 new_order = new_orders.setdefault(order_key, ({}, []))
856 new_order[1].append(porder.id)
857 order_infos = new_order[0]
861 'origin': porder.origin,
862 'date_order': porder.date_order,
863 'partner_id': porder.partner_id.id,
864 'dest_address_id': porder.dest_address_id.id,
865 'picking_type_id': porder.picking_type_id.id,
866 'location_id': porder.location_id.id,
867 'pricelist_id': porder.pricelist_id.id,
870 'notes': '%s' % (porder.notes or '',),
871 'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
874 if porder.date_order < order_infos['date_order']:
875 order_infos['date_order'] = porder.date_order
877 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
879 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
881 for order_line in porder.order_line:
882 order_lines_to_move += [order_line.id]
886 for order_key, (order_data, old_ids) in new_orders.iteritems():
887 # skip merges with only one order
889 allorders += (old_ids or [])
892 # cleanup order line data
893 for key, value in order_data['order_line'].iteritems():
894 del value['uom_factor']
895 value.update(dict(key))
896 order_data['order_line'] = [(6, 0, order_lines_to_move)]
898 # create the new order
899 context.update({'mail_create_nolog': True})
900 neworder_id = self.create(cr, uid, order_data)
901 self.message_post(cr, uid, [neworder_id], body=_("RFQ created"), context=context)
902 orders_info.update({neworder_id: old_ids})
903 allorders.append(neworder_id)
905 # make triggers pointing to the old orders point to the new order
906 for old_id in old_ids:
907 self.redirect_workflow(cr, uid, [(old_id, neworder_id)])
908 self.signal_purchase_cancel(cr, uid, [old_id])
913 class purchase_order_line(osv.osv):
914 def _amount_line(self, cr, uid, ids, prop, arg, context=None):
916 cur_obj=self.pool.get('res.currency')
917 tax_obj = self.pool.get('account.tax')
918 for line in self.browse(cr, uid, ids, context=context):
919 taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, line.order_id.partner_id)
920 cur = line.order_id.pricelist_id.currency_id
921 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
924 def _get_uom_id(self, cr, uid, context=None):
926 proxy = self.pool.get('ir.model.data')
927 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
929 except Exception, ex:
933 'name': fields.text('Description', required=True),
934 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
935 'date_planned': fields.date('Scheduled Date', required=True, select=True),
936 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
937 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
938 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
939 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
940 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
941 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
942 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
943 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
944 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
945 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
946 help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
947 \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
948 \n* The \'Done\' status is set automatically when purchase order is set as done. \
949 \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
950 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
951 'invoiced': fields.boolean('Invoiced', readonly=True),
952 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
953 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date"),
954 'procurement_ids': fields.one2many('procurement.order', 'purchase_line_id', string='Associated procurements'),
957 'product_uom' : _get_uom_id,
958 'product_qty': lambda *a: 1.0,
959 'state': lambda *args: 'draft',
960 'invoiced': lambda *a: 0,
962 _table = 'purchase_order_line'
963 _name = 'purchase.order.line'
964 _description = 'Purchase Order Line'
966 def copy_data(self, cr, uid, id, default=None, context=None):
969 default.update({'state':'draft', 'move_ids':[], 'invoiced':0, 'invoice_lines':[], 'procurement_ids': False})
970 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
972 def unlink(self, cr, uid, ids, context=None):
973 procurement_obj = self.pool.get('procurement.order')
974 procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context)
975 if procurement_ids_to_cancel:
976 self.pool['procurement.order'].cancel(cr, uid, procurement_ids_to_cancel)
977 return super(purchase_order_line, self).unlink(cr, uid, ids, context=context)
979 def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
980 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
981 name=False, price_unit=False, state='draft', context=None):
983 onchange handler of product_uom.
988 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
989 context = dict(context, purchase_uom_check=True)
990 return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
991 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
992 name=name, price_unit=price_unit, state=state, context=context)
994 def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
995 """Return the datetime value to use as Schedule Date (``date_planned``) for
996 PO Lines that correspond to the given product.supplierinfo,
997 when ordered at `date_order_str`.
999 :param browse_record | False supplier_info: product.supplierinfo, used to
1000 determine delivery delay (if False, default delay = 0)
1001 :param str date_order_str: date of order, as a string in
1002 DEFAULT_SERVER_DATE_FORMAT
1004 :return: desired Schedule Date for the PO line
1006 supplier_delay = int(supplier_info.delay) if supplier_info else 0
1007 return datetime.strptime(date_order_str, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=supplier_delay)
1009 def action_cancel(self, cr, uid, ids, context=None):
1010 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1011 for po_line in self.browse(cr, uid, ids, context=context):
1012 if all([l.state == 'cancel' for l in po_line.order_id.order_line]):
1013 self.pool.get('purchase.order').action_cancel(cr, uid, [po_line.order_id.id], context=context)
1015 def _check_product_uom_group(self, cr, uid, context=None):
1016 group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom')
1017 res = [user for user in group_uom.users if user.id == uid]
1018 return len(res) and True or False
1021 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1022 partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1023 name=False, price_unit=False, state='draft', context=None):
1025 onchange handler of product_id.
1030 res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1034 product_product = self.pool.get('product.product')
1035 product_uom = self.pool.get('product.uom')
1036 res_partner = self.pool.get('res.partner')
1037 product_pricelist = self.pool.get('product.pricelist')
1038 account_fiscal_position = self.pool.get('account.fiscal.position')
1039 account_tax = self.pool.get('account.tax')
1041 # - check for the presence of partner_id and pricelist_id
1043 # raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
1044 #if not pricelist_id:
1045 # raise osv.except_osv(_('No Pricelist !'), _('Select a price list in the purchase order form before choosing a product.'))
1047 # - determine name and notes based on product in partner lang.
1048 context_partner = context.copy()
1050 lang = res_partner.browse(cr, uid, partner_id).lang
1051 context_partner.update( {'lang': lang, 'partner_id': partner_id} )
1052 product = product_product.browse(cr, uid, product_id, context=context_partner)
1053 #call name_get() with partner in the context to eventually match name and description in the seller_ids field
1054 dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0]
1055 if product.description_purchase:
1056 name += '\n' + product.description_purchase
1057 res['value'].update({'name': name})
1059 # - set a domain on product_uom
1060 res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
1062 # - check that uom and product uom belong to the same category
1063 product_uom_po_id = product.uom_po_id.id
1065 uom_id = product_uom_po_id
1067 if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
1068 if context.get('purchase_uom_check') and self._check_product_uom_group(cr, uid, context=context):
1069 res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
1070 uom_id = product_uom_po_id
1072 res['value'].update({'product_uom': uom_id})
1074 # - determine product_qty and date_planned based on seller info
1076 date_order = fields.date.context_today(self,cr,uid,context=context)
1079 supplierinfo = False
1080 for supplier in product.seller_ids:
1081 if partner_id and (supplier.name.id == partner_id):
1082 supplierinfo = supplier
1083 if supplierinfo.product_uom.id != uom_id:
1084 res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
1085 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
1086 if (qty or 0.0) < min_qty: # If the supplier quantity is greater than entered from user, set minimal.
1088 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)}
1090 dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1092 res['value'].update({'date_planned': date_planned or dt})
1094 res['value'].update({'product_qty': qty})
1097 if state not in ('sent','bid'):
1098 # - determine price_unit and taxes_id
1100 price = product_pricelist.price_get(cr, uid, [pricelist_id],
1101 product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order})[pricelist_id]
1103 price = product.standard_price
1105 taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
1106 fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
1107 taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
1108 res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
1112 product_id_change = onchange_product_id
1113 product_uom_change = onchange_product_uom
1115 def action_confirm(self, cr, uid, ids, context=None):
1116 self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
1119 class procurement_rule(osv.osv):
1120 _inherit = 'procurement.rule'
1122 def _get_action(self, cr, uid, context=None):
1123 return [('buy', 'Buy')] + super(procurement_rule, self)._get_action(cr, uid, context=context)
1126 class procurement_order(osv.osv):
1127 _inherit = 'procurement.order'
1129 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
1130 'purchase_id': fields.related('purchase_line_id', 'order_id', type='many2one', relation='purchase.order', string='Purchase Order'),
1133 def propagate_cancel(self, cr, uid, procurement, context=None):
1134 if procurement.rule_id.action == 'buy' and procurement.purchase_line_id:
1135 self.pool.get('purchase.order.line').action_cancel(cr, uid, [procurement.purchase_line_id.id], context=context)
1136 return super(procurement_order, self).propagate_cancel(cr, uid, procurement, context=context)
1138 def _run(self, cr, uid, procurement, context=None):
1139 if procurement.rule_id and procurement.rule_id.action == 'buy':
1140 #make a purchase order for the procurement
1141 return self.make_po(cr, uid, [procurement.id], context=context)[procurement.id]
1142 return super(procurement_order, self)._run(cr, uid, procurement, context=context)
1144 def _check(self, cr, uid, procurement, context=None):
1145 if procurement.purchase_line_id and procurement.purchase_line_id.order_id.shipped: # TOCHECK: does it work for several deliveries?
1147 return super(procurement_order, self)._check(cr, uid, procurement, context=context)
1149 def _check_supplier_info(self, cr, uid, ids, context=None):
1150 ''' Check the supplier info field of a product and write an error message on the procurement if needed.
1151 Returns True if all needed information is there, False if some configuration mistake is detected.
1153 partner_obj = self.pool.get('res.partner')
1154 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1155 for procurement in self.browse(cr, uid, ids, context=context):
1157 partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
1159 if not procurement.product_id.seller_ids:
1160 message = _('No supplier defined for this product !')
1162 message = _('No default supplier defined for this product')
1163 elif not partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']:
1164 message = _('No address defined for the supplier')
1167 if procurement.message != message:
1168 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
1171 if user.company_id and user.company_id.partner_id:
1172 if partner.id == user.company_id.partner_id.id:
1173 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))
1177 def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
1178 """Create the purchase order from the procurement, using
1179 the provided field values, after adding the given purchase
1180 order line in the purchase order.
1182 :params procurement: the procurement object generating the purchase order
1183 :params dict po_vals: field values for the new purchase order (the
1184 ``order_line`` field will be overwritten with one
1185 single line, as passed in ``line_vals``).
1186 :params dict line_vals: field values of the single purchase order line that
1187 the purchase order will contain.
1188 :return: id of the newly created purchase order
1191 po_vals.update({'order_line': [(0,0,line_vals)]})
1192 return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
1194 def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
1195 """Return the datetime value to use as Schedule Date (``date_planned``) for the
1196 Purchase Order Lines created to satisfy the given procurement.
1198 :param browse_record procurement: the procurement for which a PO will be created.
1199 :param browse_report company: the company to which the new PO will belong to.
1201 :return: the desired Schedule Date for the PO lines
1203 procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
1204 schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
1205 return schedule_date
1207 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
1208 """Return the datetime value to use as Order Date (``date_order``) for the
1209 Purchase Order created to satisfy the given procurement.
1211 :param browse_record procurement: the procurement for which a PO will be created.
1212 :param browse_report company: the company to which the new PO will belong to.
1213 :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1215 :return: the desired Order Date for the PO
1217 seller_delay = int(procurement.product_id.seller_delay)
1218 return schedule_date - relativedelta(days=seller_delay)
1220 def _get_product_supplier(self, cr, uid, procurement, context=None):
1221 ''' returns the main supplier of the procurement's product given as argument'''
1222 return procurement.product_id.seller_id
1224 def _get_po_line_values_from_proc(self, cr, uid, procurement, partner, company, schedule_date, context=None):
1227 uom_obj = self.pool.get('product.uom')
1228 pricelist_obj = self.pool.get('product.pricelist')
1229 prod_obj = self.pool.get('product.product')
1230 acc_pos_obj = self.pool.get('account.fiscal.position')
1232 seller_qty = procurement.product_id.seller_qty
1233 pricelist_id = partner.property_product_pricelist_purchase.id
1234 uom_id = procurement.product_id.uom_po_id.id
1235 qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1237 qty = max(qty, seller_qty)
1238 price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner.id, {'uom': uom_id})[pricelist_id]
1240 #Passing partner_id to context for purchase order line integrity of Line name
1241 new_context = context.copy()
1242 new_context.update({'lang': partner.lang, 'partner_id': partner.id})
1243 product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
1244 taxes_ids = procurement.product_id.supplier_taxes_id
1245 taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1246 name = product.partner_ref
1247 if product.description_purchase:
1248 name += '\n' + product.description_purchase
1253 'product_id': procurement.product_id.id,
1254 'product_uom': uom_id,
1255 'price_unit': price or 0.0,
1256 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1257 'taxes_id': [(6, 0, taxes)],
1260 def make_po(self, cr, uid, ids, context=None):
1261 """ Make purchase order from procurement
1262 @return: New created Purchase Orders procurement wise
1265 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1266 po_obj = self.pool.get('purchase.order')
1267 po_line_obj = self.pool.get('purchase.order.line')
1268 seq_obj = self.pool.get('ir.sequence')
1271 sum_po_line_ids = []
1272 for procurement in self.browse(cr, uid, ids, context=context):
1273 partner = self._get_product_supplier(cr, uid, procurement, context=context)
1275 self.message_post(cr, uid, [procurement.id], _('There is no supplier associated to product %s') % (procurement.product_id.name))
1276 res[procurement.id] = False
1278 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1279 line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context)
1280 #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one
1281 available_draft_po_ids = po_obj.search(cr, uid, [
1282 ('partner_id', '=', partner.id), ('state', '=', 'draft'), ('picking_type_id', '=', procurement.rule_id.picking_type_id.id),
1283 ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context)
1284 if available_draft_po_ids:
1285 po_id = available_draft_po_ids[0]
1286 #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
1287 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)
1288 if available_po_line_ids:
1289 po_line = po_line_obj.browse(cr, uid, available_po_line_ids[0], context=context)
1290 po_line_obj.write(cr, uid, po_line.id, {'product_qty': po_line.product_qty + line_vals['product_qty']}, context=context)
1291 po_line_id = po_line.id
1292 sum_po_line_ids.append(procurement.id)
1294 line_vals.update(order_id=po_id)
1295 po_line_id = po_line_obj.create(cr, uid, line_vals, context=context)
1296 linked_po_ids.append(procurement.id)
1298 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1299 name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1302 'origin': procurement.origin,
1303 'partner_id': partner.id,
1304 'location_id': procurement.location_id.id,
1305 'picking_type_id': procurement.rule_id.picking_type_id.id,
1306 'pricelist_id': partner.property_product_pricelist_purchase.id,
1307 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1308 'company_id': procurement.company_id.id,
1309 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False,
1310 'payment_term_id': partner.property_supplier_payment_term.id or False,
1311 'dest_address_id': procurement.partner_dest_id.id,
1313 po_id = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=context)
1314 po_line_id = po_obj.browse(cr, uid, po_id, context=context).order_line[0].id
1315 pass_ids.append(procurement.id)
1316 res[procurement.id] = po_line_id
1317 self.write(cr, uid, [procurement.id], {'purchase_line_id': po_line_id}, context=context)
1319 self.message_post(cr, uid, pass_ids, body=_("Draft Purchase Order created"), context=context)
1321 self.message_post(cr, uid, linked_po_ids, body=_("Purchase line created and linked to an existing Purchase Order"), context=context)
1323 self.message_post(cr, uid, sum_po_line_ids, body=_("Quantity added in existing Purchase Order Line"), context=context)
1326 def _product_virtual_get(self, cr, uid, order_point):
1327 procurement = order_point.procurement_id
1328 if procurement and procurement.state != 'exception' and procurement.purchase_line_id and procurement.purchase_line_id.order_id.state in ('draft', 'confirmed'):
1330 return super(procurement_order, self)._product_virtual_get(cr, uid, order_point)
1333 class mail_mail(osv.Model):
1335 _inherit = 'mail.mail'
1337 def _postprocess_sent_message(self, cr, uid, mail, context=None):
1338 if mail.model == 'purchase.order':
1339 obj = self.pool.get('purchase.order').browse(cr, uid, mail.res_id, context=context)
1340 if obj.state == 'draft':
1341 self.pool.get('purchase.order').signal_send_rfq(cr, uid, [mail.res_id])
1342 return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
1345 class product_template(osv.Model):
1346 _name = 'product.template'
1347 _inherit = 'product.template'
1349 'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
1356 class mail_compose_message(osv.Model):
1357 _inherit = 'mail.compose.message'
1359 def send_mail(self, cr, uid, ids, context=None):
1360 context = context or {}
1361 if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
1362 context = dict(context, mail_post_autofollow=True)
1363 self.pool.get('purchase.order').signal_send_rfq(cr, uid, [context['default_res_id']])
1364 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1367 class account_invoice(osv.Model):
1368 """ Override account_invoice to add Chatter messages on the related purchase
1369 orders, logging the invoice reception or payment. """
1370 _inherit = 'account.invoice'
1372 def invoice_validate(self, cr, uid, ids, context=None):
1373 res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context)
1374 purchase_order_obj = self.pool.get('purchase.order')
1375 # read access on purchase.order object is not required
1376 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1377 user_id = SUPERUSER_ID
1380 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1381 for po_id in po_ids:
1382 purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice received"), context=context)
1383 workflow.trg_write(uid, 'purchase.order', po_id, cr)
1386 def confirm_paid(self, cr, uid, ids, context=None):
1387 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1388 purchase_order_obj = self.pool.get('purchase.order')
1389 # read access on purchase.order object is not required
1390 if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1391 user_id = SUPERUSER_ID
1394 po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1395 for po_id in po_ids:
1396 purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice paid"), context=context)
1399 class account_invoice_line(osv.Model):
1400 """ Override account_invoice_line to add the link to the purchase order line it is related to"""
1401 _inherit = 'account.invoice.line'
1403 'purchase_line_id': fields.many2one('purchase.order.line',
1404 'Purchase Order Line', ondelete='set null', select=True,
1408 class product_product(osv.osv):
1409 _inherit = "product.product"
1411 def _get_buy_route(self, cr, uid, context=None):
1412 buy_route = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'purchase', 'route_warehouse0_buy')[1]
1416 'route_ids': _get_buy_route,
1419 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: