[MERGE] forward port of branch 8.0 up to e883193
[odoo/odoo.git] / addons / purchase / purchase.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import pytz
23 from openerp import SUPERUSER_ID, workflow
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
26 from operator import attrgetter
27 from openerp.tools.safe_eval import safe_eval as eval
28 from openerp.osv import fields, osv
29 from openerp.tools.translate import _
30 import openerp.addons.decimal_precision as dp
31 from openerp.osv.orm import browse_record_list, browse_record, browse_null
32 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
33 from openerp.tools.float_utils import float_compare
34
35 class purchase_order(osv.osv):
36
37     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
38         res = {}
39         cur_obj=self.pool.get('res.currency')
40         for order in self.browse(cr, uid, ids, context=context):
41             res[order.id] = {
42                 'amount_untaxed': 0.0,
43                 'amount_tax': 0.0,
44                 'amount_total': 0.0,
45             }
46             val = val1 = 0.0
47             cur = order.pricelist_id.currency_id
48             for line in order.order_line:
49                val1 += line.price_subtotal
50                for c in self.pool.get('account.tax').compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, order.partner_id)['taxes']:
51                     val += c.get('amount', 0.0)
52             res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
53             res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
54             res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
55         return res
56
57     def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
58         if not value: return False
59         if type(ids)!=type([]):
60             ids=[ids]
61         for po in self.browse(cr, uid, ids, context=context):
62             if po.order_line:
63                 cr.execute("""update purchase_order_line set
64                         date_planned=%s
65                     where
66                         order_id=%s and
67                         (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
68             cr.execute("""update purchase_order set
69                     minimum_planned_date=%s where id=%s""", (value, po.id))
70         self.invalidate_cache(cr, uid, context=context)
71         return True
72
73     def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
74         res={}
75         purchase_obj=self.browse(cr, uid, ids, context=context)
76         for purchase in purchase_obj:
77             res[purchase.id] = False
78             if purchase.order_line:
79                 min_date=purchase.order_line[0].date_planned
80                 for line in purchase.order_line:
81                     if line.date_planned < min_date:
82                         min_date=line.date_planned
83                 res[purchase.id]=min_date
84         return res
85
86
87     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
88         res = {}
89         for purchase in self.browse(cursor, user, ids, context=context):
90             tot = 0.0
91             for invoice in purchase.invoice_ids:
92                 if invoice.state not in ('draft','cancel'):
93                     tot += invoice.amount_untaxed
94             if purchase.amount_untaxed:
95                 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
96             else:
97                 res[purchase.id] = 0.0
98         return res
99
100     def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
101         if not ids: return {}
102         res = {}
103         for id in ids:
104             res[id] = [0.0,0.0]
105         cr.execute('''SELECT
106                 p.order_id, sum(m.product_qty), m.state
107             FROM
108                 stock_move m
109             LEFT JOIN
110                 purchase_order_line p on (p.id=m.purchase_line_id)
111             WHERE
112                 p.order_id IN %s GROUP BY m.state, p.order_id''',(tuple(ids),))
113         for oid,nbr,state in cr.fetchall():
114             if state=='cancel':
115                 continue
116             if state=='done':
117                 res[oid][0] += nbr or 0.0
118                 res[oid][1] += nbr or 0.0
119             else:
120                 res[oid][1] += nbr or 0.0
121         for r in res:
122             if not res[r][1]:
123                 res[r] = 0.0
124             else:
125                 res[r] = 100.0 * res[r][0] / res[r][1]
126         return res
127
128     def _get_order(self, cr, uid, ids, context=None):
129         result = {}
130         for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
131             result[line.order_id.id] = True
132         return result.keys()
133
134     def _invoiced(self, cursor, user, ids, name, arg, context=None):
135         res = {}
136         for purchase in self.browse(cursor, user, ids, context=context):
137             res[purchase.id] = all(line.invoiced for line in purchase.order_line)
138         return res
139     
140     def _get_journal(self, cr, uid, context=None):
141         if context is None:
142             context = {}
143         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
144         company_id = context.get('company_id', user.company_id.id)
145         journal_obj = self.pool.get('account.journal')
146         res = journal_obj.search(cr, uid, [('type', '=', 'purchase'),
147                                             ('company_id', '=', company_id)],
148                                                 limit=1)
149         return res and res[0] or False  
150
151     def _get_picking_in(self, cr, uid, context=None):
152         obj_data = self.pool.get('ir.model.data')
153         return obj_data.get_object_reference(cr, uid, 'stock','picking_type_in') and obj_data.get_object_reference(cr, uid, 'stock','picking_type_in')[1] or False
154
155     def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
156         res = {}
157         for po_id in ids:
158             res[po_id] = []
159         query = """
160         SELECT picking_id, po.id FROM stock_picking p, stock_move m, purchase_order_line pol, purchase_order po
161             WHERE po.id in %s and po.id = pol.order_id and pol.id = m.purchase_line_id and m.picking_id = p.id
162             GROUP BY picking_id, po.id
163              
164         """
165         cr.execute(query, (tuple(ids), ))
166         picks = cr.fetchall()
167         for pick_id, po_id in picks:
168             res[po_id].append(pick_id)
169         return res
170
171     def _count_all(self, cr, uid, ids, field_name, arg, context=None):
172         return {
173             purchase.id: {
174                 'shipment_count': len(purchase.picking_ids),
175                 'invoice_count': len(purchase.invoice_ids),                
176             }
177             for purchase in self.browse(cr, uid, ids, context=context)
178         }
179
180     STATE_SELECTION = [
181         ('draft', 'Draft PO'),
182         ('sent', 'RFQ'),
183         ('bid', 'Bid Received'),
184         ('confirmed', 'Waiting Approval'),
185         ('approved', 'Purchase Confirmed'),
186         ('except_picking', 'Shipping Exception'),
187         ('except_invoice', 'Invoice Exception'),
188         ('done', 'Done'),
189         ('cancel', 'Cancelled')
190     ]
191     _track = {
192         'state': {
193             'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirmed',
194             'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'approved',
195             'purchase.mt_rfq_done': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
196         },
197     }
198     _columns = {
199         'name': fields.char('Order Reference', required=True, select=True, copy=False,
200                             help="Unique number of the purchase order, "
201                                  "computed automatically when the purchase order is created."),
202         'origin': fields.char('Source Document', copy=False,
203                               help="Reference of the document that generated this purchase order "
204                                    "request; a sales order or an internal procurement request."),
205         'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)],
206                                                                  'approved':[('readonly',True)],
207                                                                  'done':[('readonly',True)]},
208                                    copy=False,
209                                    help="Reference of the sales order or bid sent by your supplier. "
210                                         "It's mainly used to do the matching when you receive the "
211                                         "products as this reference is usually written on the "
212                                         "delivery order sent by your supplier."),
213         'date_order':fields.datetime('Order Date', required=True, states={'confirmed':[('readonly',True)],
214                                                                       'approved':[('readonly',True)]},
215                                  select=True, help="Depicts the date where the Quotation should be validated and converted into a Purchase Order, by default it's the creation date.",
216                                  copy=False),
217         'date_approve':fields.date('Date Approved', readonly=1, select=True, copy=False,
218                                    help="Date on which purchase order has been approved"),
219         'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
220             change_default=True, track_visibility='always'),
221         'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
222             states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
223             help="Put an address if you want to deliver directly from the supplier to the customer. " \
224                 "Otherwise, keep empty to deliver to your own company."
225         ),
226         'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]} ),
227         'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, help="The pricelist sets the currency used for this purchase order. It also computes the supplier price for the selected products/quantities."),
228         'currency_id': fields.many2one('res.currency','Currency', readonly=True, required=True,states={'draft': [('readonly', False)],'sent': [('readonly', False)]}),
229         'state': fields.selection(STATE_SELECTION, 'Status', readonly=True,
230                                   help="The status of the purchase order or the quotation request. "
231                                        "A request for quotation is a purchase order in a 'Draft' status. "
232                                        "Then the order has to be confirmed by the user, the status switch "
233                                        "to 'Confirmed'. Then the supplier must confirm the order to change "
234                                        "the status to 'Approved'. When the purchase order is paid and "
235                                        "received, the status becomes 'Done'. If a cancel action occurs in "
236                                        "the invoice or in the receipt of goods, the status becomes "
237                                        "in exception.",
238                                   select=True, copy=False),
239         'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines',
240                                       states={'approved':[('readonly',True)],
241                                               'done':[('readonly',True)]},
242                                       copy=True),
243         'validator' : fields.many2one('res.users', 'Validated by', readonly=True, copy=False),
244         'notes': fields.text('Terms and Conditions'),
245         'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id',
246                                         'invoice_id', 'Invoices', copy=False,
247                                         help="Invoices generated for a purchase order"),
248         'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking List', help="This is the list of receipts that have been generated for this purchase order."),
249         'shipped':fields.boolean('Received', readonly=True, select=True, copy=False,
250                                  help="It indicates that a picking has been done"),
251         'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'),
252         'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', copy=False,
253                                     help="It indicates that an invoice has been validated"),
254         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
255         'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on incoming shipments')], 'Invoicing Control', required=True,
256             readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)],'bid':[('readonly',False)]},
257             help="Based on Purchase Order lines: place individual lines in 'Invoice Control / On Purchase Order lines' from where you can selectively create an invoice.\n" \
258                 "Based on generated invoice: create a draft invoice you can validate later.\n" \
259                 "Based on incoming shipments: let you create an invoice when receipts are validated."
260         ),
261         'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, string='Expected Date', type='date', select=True, help="This is computed as the minimum scheduled date of all purchase order lines' products.",
262             store = {
263                 'purchase.order.line': (_get_order, ['date_planned'], 10),
264             }
265         ),
266         'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
267             store={
268                 'purchase.order.line': (_get_order, None, 10),
269             }, multi="sums", help="The amount without tax", track_visibility='always'),
270         'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
271             store={
272                 'purchase.order.line': (_get_order, None, 10),
273             }, multi="sums", help="The tax amount"),
274         'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
275             store={
276                 'purchase.order.line': (_get_order, None, 10),
277             }, multi="sums", help="The total amount"),
278         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
279         'payment_term_id': fields.many2one('account.payment.term', 'Payment Term'),
280         'incoterm_id': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
281         'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
282         'create_uid': fields.many2one('res.users', 'Responsible'),
283         'company_id': fields.many2one('res.company', 'Company', required=True, select=1, states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)]}),
284         'journal_id': fields.many2one('account.journal', 'Journal'),
285         'bid_date': fields.date('Bid Received On', readonly=True, help="Date on which the bid was received"),
286         'bid_validity': fields.date('Bid Valid Until', help="Date on which the bid expired"),
287         'picking_type_id': fields.many2one('stock.picking.type', 'Deliver To', help="This will determine picking type of incoming shipment", required=True,
288                                            states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}),
289         'related_location_id': fields.related('picking_type_id', 'default_location_dest_id', type='many2one', relation='stock.location', string="Related location", store=True),        
290         'shipment_count': fields.function(_count_all, type='integer', string='Incoming Shipments', multi=True),
291         'invoice_count': fields.function(_count_all, type='integer', string='Invoices', multi=True)
292     }
293     _defaults = {
294         'date_order': fields.datetime.now,
295         'state': 'draft',
296         'name': lambda obj, cr, uid, context: '/',
297         'shipped': 0,
298         'invoice_method': 'order',
299         'invoiced': 0,
300         'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist_purchase.id,
301         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
302         'journal_id': _get_journal,
303         'currency_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id,
304         'picking_type_id': _get_picking_in,
305     }
306     _sql_constraints = [
307         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
308     ]
309     _name = "purchase.order"
310     _inherit = ['mail.thread', 'ir.needaction_mixin']
311     _description = "Purchase Order"
312     _order = 'date_order desc, id desc'
313
314     def create(self, cr, uid, vals, context=None):
315         if vals.get('name','/')=='/':
316             vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'purchase.order') or '/'
317         context = dict(context or {}, mail_create_nolog=True)
318         order =  super(purchase_order, self).create(cr, uid, vals, context=context)
319         self.message_post(cr, uid, [order], body=_("RFQ created"), context=context)
320         return order
321
322     def unlink(self, cr, uid, ids, context=None):
323         purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
324         unlink_ids = []
325         for s in purchase_orders:
326             if s['state'] in ['draft','cancel']:
327                 unlink_ids.append(s['id'])
328             else:
329                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.'))
330
331         # automatically sending subflow.delete upon deletion
332         self.signal_workflow(cr, uid, unlink_ids, 'purchase_cancel')
333
334         return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
335
336     def set_order_line_status(self, cr, uid, ids, status, context=None):
337         line = self.pool.get('purchase.order.line')
338         order_line_ids = []
339         proc_obj = self.pool.get('procurement.order')
340         for order in self.browse(cr, uid, ids, context=context):
341             order_line_ids += [po_line.id for po_line in order.order_line]
342         if order_line_ids:
343             line.write(cr, uid, order_line_ids, {'state': status}, context=context)
344         if order_line_ids and status == 'cancel':
345             procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', order_line_ids)], context=context)
346             if procs:
347                 proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
348         return True
349
350     def button_dummy(self, cr, uid, ids, context=None):
351         return True
352
353     def onchange_pricelist(self, cr, uid, ids, pricelist_id, context=None):
354         if not pricelist_id:
355             return {}
356         return {'value': {'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id}}
357
358     #Destination address is used when dropshipping
359     def onchange_dest_address_id(self, cr, uid, ids, address_id, context=None):
360         if not address_id:
361             return {}
362         address = self.pool.get('res.partner')
363         values = {}
364         supplier = address.browse(cr, uid, address_id, context=context)
365         if supplier:
366             location_id = supplier.property_stock_customer.id
367             values.update({'location_id': location_id})
368         return {'value':values}
369
370     def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None):
371         value = {}
372         if picking_type_id:
373             picktype = self.pool.get("stock.picking.type").browse(cr, uid, picking_type_id, context=context)
374             if picktype.default_location_dest_id:
375                 value.update({'location_id': picktype.default_location_dest_id.id})
376             value.update({'related_location_id': picktype.default_location_dest_id and picktype.default_location_dest_id.id or False})
377         return {'value': value}
378
379     def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
380         partner = self.pool.get('res.partner')
381         if not partner_id:
382             return {'value': {
383                 'fiscal_position': False,
384                 'payment_term_id': False,
385                 }}
386         supplier_address = partner.address_get(cr, uid, [partner_id], ['default'], context=context)
387         supplier = partner.browse(cr, uid, partner_id, context=context)
388         return {'value': {
389             'pricelist_id': supplier.property_product_pricelist_purchase.id,
390             'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
391             'payment_term_id': supplier.property_supplier_payment_term.id or False,
392             }}
393
394     def invoice_open(self, cr, uid, ids, context=None):
395         mod_obj = self.pool.get('ir.model.data')
396         act_obj = self.pool.get('ir.actions.act_window')
397
398         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree2')
399         id = result and result[1] or False
400         result = act_obj.read(cr, uid, [id], context=context)[0]
401         inv_ids = []
402         for po in self.browse(cr, uid, ids, context=context):
403             inv_ids+= [invoice.id for invoice in po.invoice_ids]
404         if not inv_ids:
405             raise osv.except_osv(_('Error!'), _('Please create Invoices.'))
406          #choose the view_mode accordingly
407         if len(inv_ids)>1:
408             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
409         else:
410             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
411             result['views'] = [(res and res[1] or False, 'form')]
412             result['res_id'] = inv_ids and inv_ids[0] or False
413         return result
414
415     def view_invoice(self, cr, uid, ids, context=None):
416         '''
417         This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
418         '''
419         context = dict(context or {})
420         mod_obj = self.pool.get('ir.model.data')
421         wizard_obj = self.pool.get('purchase.order.line_invoice')
422         #compute the number of invoices to display
423         inv_ids = []
424         for po in self.browse(cr, uid, ids, context=context):
425             if po.invoice_method == 'manual':
426                 if not po.invoice_ids:
427                     context.update({'active_ids' :  [line.id for line in po.order_line]})
428                     wizard_obj.makeInvoices(cr, uid, [], context=context)
429
430         for po in self.browse(cr, uid, ids, context=context):
431             inv_ids+= [invoice.id for invoice in po.invoice_ids]
432         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
433         res_id = res and res[1] or False
434
435         return {
436             'name': _('Supplier Invoices'),
437             'view_type': 'form',
438             'view_mode': 'form',
439             'view_id': [res_id],
440             'res_model': 'account.invoice',
441             'context': "{'type':'in_invoice', 'journal_type': 'purchase'}",
442             'type': 'ir.actions.act_window',
443             'nodestroy': True,
444             'target': 'current',
445             'res_id': inv_ids and inv_ids[0] or False,
446         }
447
448     def view_picking(self, cr, uid, ids, context=None):
449         '''
450         This function returns an action that display existing picking orders of given purchase order ids.
451         '''
452         if context is None:
453             context = {}
454         mod_obj = self.pool.get('ir.model.data')
455         dummy, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree'))
456         action = self.pool.get('ir.actions.act_window').read(cr, uid, action_id, context=context)
457
458         pick_ids = []
459         for po in self.browse(cr, uid, ids, context=context):
460             pick_ids += [picking.id for picking in po.picking_ids]
461
462         #override the context to get rid of the default filtering on picking type
463         action['context'] = {}
464         #choose the view_mode accordingly
465         if len(pick_ids) > 1:
466             action['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
467         else:
468             res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
469             action['views'] = [(res and res[1] or False, 'form')]
470             action['res_id'] = pick_ids and pick_ids[0] or False
471         return action
472
473     def wkf_approve_order(self, cr, uid, ids, context=None):
474         self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)})
475         return True
476
477     def wkf_bid_received(self, cr, uid, ids, context=None):
478         return self.write(cr, uid, ids, {'state':'bid', 'bid_date': fields.date.context_today(self,cr,uid,context=context)})
479
480     def wkf_send_rfq(self, cr, uid, ids, context=None):
481         '''
482         This function opens a window to compose an email, with the edi purchase template message loaded by default
483         '''
484         if not context:
485             context= {}
486         ir_model_data = self.pool.get('ir.model.data')
487         try:
488             if context.get('send_rfq', False):
489                 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
490             else:
491                 template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase_done')[1]
492         except ValueError:
493             template_id = False
494         try:
495             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
496         except ValueError:
497             compose_form_id = False 
498         ctx = dict(context)
499         ctx.update({
500             'default_model': 'purchase.order',
501             'default_res_id': ids[0],
502             'default_use_template': bool(template_id),
503             'default_template_id': template_id,
504             'default_composition_mode': 'comment',
505         })
506         return {
507             'name': _('Compose Email'),
508             'type': 'ir.actions.act_window',
509             'view_type': 'form',
510             'view_mode': 'form',
511             'res_model': 'mail.compose.message',
512             'views': [(compose_form_id, 'form')],
513             'view_id': compose_form_id,
514             'target': 'new',
515             'context': ctx,
516         }
517
518     def print_quotation(self, cr, uid, ids, context=None):
519         '''
520         This function prints the request for quotation and mark it as sent, so that we can see more easily the next step of the workflow
521         '''
522         assert len(ids) == 1, 'This option should only be used for a single id at a time'
523         self.signal_workflow(cr, uid, ids, 'send_rfq')
524         return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
525
526     def wkf_confirm_order(self, cr, uid, ids, context=None):
527         todo = []
528         for po in self.browse(cr, uid, ids, context=context):
529             if not po.order_line:
530                 raise osv.except_osv(_('Error!'),_('You cannot confirm a purchase order without any purchase order line.'))
531             for line in po.order_line:
532                 if line.state=='draft':
533                     todo.append(line.id)        
534         self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
535         for id in ids:
536             self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
537         return True
538
539     def _choose_account_from_po_line(self, cr, uid, po_line, context=None):
540         fiscal_obj = self.pool.get('account.fiscal.position')
541         property_obj = self.pool.get('ir.property')
542         if po_line.product_id:
543             acc_id = po_line.product_id.property_account_expense.id
544             if not acc_id:
545                 acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
546             if not acc_id:
547                 raise osv.except_osv(_('Error!'), _('Define an expense account for this product: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
548         else:
549             acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context=context).id
550         fpos = po_line.order_id.fiscal_position or False
551         return fiscal_obj.map_account(cr, uid, fpos, acc_id)
552
553     def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
554         """Collects require data from purchase order line that is used to create invoice line
555         for that purchase order line
556         :param account_id: Expense account of the product of PO line if any.
557         :param browse_record order_line: Purchase order line browse record
558         :return: Value for fields of invoice lines.
559         :rtype: dict
560         """
561         return {
562             'name': order_line.name,
563             'account_id': account_id,
564             'price_unit': order_line.price_unit or 0.0,
565             'quantity': order_line.product_qty,
566             'product_id': order_line.product_id.id or False,
567             'uos_id': order_line.product_uom.id or False,
568             'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
569             'account_analytic_id': order_line.account_analytic_id.id or False,
570             'purchase_line_id': order_line.id,
571         }
572
573     def _prepare_invoice(self, cr, uid, order, line_ids, context=None):
574         """Prepare the dict of values to create the new invoice for a
575            purchase order. This method may be overridden to implement custom
576            invoice generation (making sure to call super() to establish
577            a clean extension chain).
578
579            :param browse_record order: purchase.order record to invoice
580            :param list(int) line_ids: list of invoice line IDs that must be
581                                       attached to the invoice
582            :return: dict of value to create() the invoice
583         """
584         journal_ids = self.pool['account.journal'].search(
585                             cr, uid, [('type', '=', 'purchase'),
586                                       ('company_id', '=', order.company_id.id)],
587                             limit=1)
588         if not journal_ids:
589             raise osv.except_osv(
590                 _('Error!'),
591                 _('Define purchase journal for this company: "%s" (id:%d).') % \
592                     (order.company_id.name, order.company_id.id))
593         return {
594             'name': order.partner_ref or order.name,
595             'reference': order.partner_ref or order.name,
596             'account_id': order.partner_id.property_account_payable.id,
597             'type': 'in_invoice',
598             'partner_id': order.partner_id.id,
599             'currency_id': order.currency_id.id,
600             'journal_id': len(journal_ids) and journal_ids[0] or False,
601             'invoice_line': [(6, 0, line_ids)],
602             'origin': order.name,
603             'fiscal_position': order.fiscal_position.id or False,
604             'payment_term': order.payment_term_id.id or False,
605             'company_id': order.company_id.id,
606         }
607
608     def action_cancel_draft(self, cr, uid, ids, context=None):
609         if not len(ids):
610             return False
611         self.write(cr, uid, ids, {'state':'draft','shipped':0})
612         self.set_order_line_status(cr, uid, ids, 'draft', context=context)
613         for p_id in ids:
614             # Deleting the existing instance of workflow for PO
615             self.delete_workflow(cr, uid, [p_id]) # TODO is it necessary to interleave the calls?
616             self.create_workflow(cr, uid, [p_id])
617         return True
618
619     def wkf_po_done(self, cr, uid, ids, context=None):
620         self.write(cr, uid, ids, {'state': 'done'}, context=context)
621         self.set_order_line_status(cr, uid, ids, 'done', context=context)
622
623     def action_invoice_create(self, cr, uid, ids, context=None):
624         """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
625         :param ids: list of ids of purchase orders.
626         :return: ID of created invoice.
627         :rtype: int
628         """
629         context = dict(context or {})
630         
631         inv_obj = self.pool.get('account.invoice')
632         inv_line_obj = self.pool.get('account.invoice.line')
633
634         res = False
635         uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
636         for order in self.browse(cr, uid, ids, context=context):
637             context.pop('force_company', None)
638             if order.company_id.id != uid_company_id:
639                 #if the company of the document is different than the current user company, force the company in the context
640                 #then re-do a browse to read the property fields for the good company.
641                 context['force_company'] = order.company_id.id
642                 order = self.browse(cr, uid, order.id, context=context)
643             
644             # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
645             inv_lines = []
646             for po_line in order.order_line:
647                 acc_id = self._choose_account_from_po_line(cr, uid, po_line, context=context)
648                 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
649                 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
650                 inv_lines.append(inv_line_id)
651                 po_line.write({'invoice_lines': [(4, inv_line_id)]})
652
653             # get invoice data and create invoice
654             inv_data = self._prepare_invoice(cr, uid, order, inv_lines, context=context)
655             inv_id = inv_obj.create(cr, uid, inv_data, context=context)
656
657             # compute the invoice
658             inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
659
660             # Link this new invoice to related purchase order
661             order.write({'invoice_ids': [(4, inv_id)]})
662             res = inv_id
663         return res
664
665     def invoice_done(self, cr, uid, ids, context=None):
666         self.write(cr, uid, ids, {'state': 'approved'}, context=context)
667         return True
668
669     def has_stockable_product(self, cr, uid, ids, *args):
670         for order in self.browse(cr, uid, ids):
671             for order_line in order.order_line:
672                 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
673                     return True
674         return False
675
676     def wkf_action_cancel(self, cr, uid, ids, context=None):
677         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
678         self.set_order_line_status(cr, uid, ids, 'cancel', context=context)
679
680     def action_cancel(self, cr, uid, ids, context=None):
681         for purchase in self.browse(cr, uid, ids, context=context):
682             for pick in purchase.picking_ids:
683                 for move in pick.move_lines:
684                     if pick.state == 'done':
685                         raise osv.except_osv(
686                             _('Unable to cancel the purchase order %s.') % (purchase.name),
687                             _('You have already received some goods for it.  '))
688             self.pool.get('stock.picking').action_cancel(cr, uid, [x.id for x in purchase.picking_ids if x.state != 'cancel'], context=context)
689             for inv in purchase.invoice_ids:
690                 if inv and inv.state not in ('cancel', 'draft'):
691                     raise osv.except_osv(
692                         _('Unable to cancel this purchase order.'),
693                         _('You must first cancel all invoices related to this purchase order.'))
694             self.pool.get('account.invoice') \
695                 .signal_workflow(cr, uid, map(attrgetter('id'), purchase.invoice_ids), 'invoice_cancel')
696         self.signal_workflow(cr, uid, ids, 'purchase_cancel')
697         return True
698
699     def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
700         ''' prepare the stock move data from the PO line. This function returns a list of dictionary ready to be used in stock.move's create()'''
701         product_uom = self.pool.get('product.uom')
702         price_unit = order_line.price_unit
703         if order_line.product_uom.id != order_line.product_id.uom_id.id:
704             price_unit *= order_line.product_uom.factor
705         if order.currency_id.id != order.company_id.currency_id.id:
706             #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency
707             price_unit = self.pool.get('res.currency').compute(cr, uid, order.currency_id.id, order.company_id.currency_id.id, price_unit, round=False, context=context)
708         res = []
709         move_template = {
710             'name': order_line.name or '',
711             'product_id': order_line.product_id.id,
712             'product_uom': order_line.product_uom.id,
713             'product_uos': order_line.product_uom.id,
714             'date': order.date_order,
715             'date_expected': fields.date.date_to_datetime(self, cr, uid, order_line.date_planned, context),
716             'location_id': order.partner_id.property_stock_supplier.id,
717             'location_dest_id': order.location_id.id,
718             'picking_id': picking_id,
719             'partner_id': order.dest_address_id.id or order.partner_id.id,
720             'move_dest_id': False,
721             'state': 'draft',
722             'purchase_line_id': order_line.id,
723             'company_id': order.company_id.id,
724             'price_unit': price_unit,
725             'picking_type_id': order.picking_type_id.id,
726             'group_id': group_id,
727             'procurement_id': False,
728             'origin': order.name,
729             'route_ids': order.picking_type_id.warehouse_id and [(6, 0, [x.id for x in order.picking_type_id.warehouse_id.route_ids])] or [],
730             'warehouse_id':order.picking_type_id.warehouse_id.id,
731             'invoice_state': order.invoice_method == 'picking' and '2binvoiced' or 'none',
732         }
733
734         diff_quantity = order_line.product_qty
735         for procurement in order_line.procurement_ids:
736             procurement_qty = product_uom._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, to_uom_id=order_line.product_uom.id)
737             tmp = move_template.copy()
738             tmp.update({
739                 'product_uom_qty': min(procurement_qty, diff_quantity),
740                 'product_uos_qty': min(procurement_qty, diff_quantity),
741                 'move_dest_id': procurement.move_dest_id.id,  #move destination is same as procurement destination
742                 'group_id': procurement.group_id.id or group_id,  #move group is same as group of procurements if it exists, otherwise take another group
743                 'procurement_id': procurement.id,
744                 'invoice_state': procurement.rule_id.invoice_state or (procurement.location_id and procurement.location_id.usage == 'customer' and procurement.invoice_state=='picking' and '2binvoiced') or (order.invoice_method == 'picking' and '2binvoiced') or 'none', #dropship case takes from sale
745                 'propagate': procurement.rule_id.propagate,
746             })
747             diff_quantity -= min(procurement_qty, diff_quantity)
748             res.append(tmp)
749         #if the order line has a bigger quantity than the procurement it was for (manually changed or minimal quantity), then
750         #split the future stock move in two because the route followed may be different.
751         if diff_quantity > 0:
752             move_template['product_uom_qty'] = diff_quantity
753             move_template['product_uos_qty'] = diff_quantity
754             res.append(move_template)
755         return res
756
757     def _create_stock_moves(self, cr, uid, order, order_lines, picking_id=False, context=None):
758         """Creates appropriate stock moves for given order lines, whose can optionally create a
759         picking if none is given or no suitable is found, then confirms the moves, makes them
760         available, and confirms the pickings.
761
762         If ``picking_id`` is provided, the stock moves will be added to it, otherwise a standard
763         incoming picking will be created to wrap the stock moves (default behavior of the stock.move)
764
765         Modules that wish to customize the procurements or partition the stock moves over
766         multiple stock pickings may override this method and call ``super()`` with
767         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
768
769         :param browse_record order: purchase order to which the order lines belong
770         :param list(browse_record) order_lines: purchase order line records for which picking
771                                                 and moves should be created.
772         :param int picking_id: optional ID of a stock picking to which the created stock moves
773                                will be added. A new picking will be created if omitted.
774         :return: None
775         """
776         stock_move = self.pool.get('stock.move')
777         todo_moves = []
778         new_group = self.pool.get("procurement.group").create(cr, uid, {'name': order.name, 'partner_id': order.partner_id.id}, context=context)
779
780         for order_line in order_lines:
781             if not order_line.product_id:
782                 continue
783
784             if order_line.product_id.type in ('product', 'consu'):
785                 for vals in self._prepare_order_line_move(cr, uid, order, order_line, picking_id, new_group, context=context):
786                     move = stock_move.create(cr, uid, vals, context=context)
787                     todo_moves.append(move)
788
789         todo_moves = stock_move.action_confirm(cr, uid, todo_moves)
790         stock_move.force_assign(cr, uid, todo_moves)
791
792     def test_moves_done(self, cr, uid, ids, context=None):
793         '''PO is done at the delivery side if all the incoming shipments are done'''
794         for purchase in self.browse(cr, uid, ids, context=context):
795             for picking in purchase.picking_ids:
796                 if picking.state != 'done':
797                     return False
798         return True
799
800     def test_moves_except(self, cr, uid, ids, context=None):
801         ''' PO is in exception at the delivery side if one of the picking is canceled
802             and the other pickings are completed (done or canceled)
803         '''
804         at_least_one_canceled = False
805         alldoneorcancel = True
806         for purchase in self.browse(cr, uid, ids, context=context):
807             for picking in purchase.picking_ids:
808                 if picking.state == 'cancel':
809                     at_least_one_canceled = True
810                 if picking.state not in ['done', 'cancel']:
811                     alldoneorcancel = False
812         return at_least_one_canceled and alldoneorcancel
813
814     def move_lines_get(self, cr, uid, ids, *args):
815         res = []
816         for order in self.browse(cr, uid, ids, context={}):
817             for line in order.order_line:
818                 res += [x.id for x in line.move_ids]
819         return res
820
821     def action_picking_create(self, cr, uid, ids, context=None):
822         for order in self.browse(cr, uid, ids):
823             picking_vals = {
824                 'picking_type_id': order.picking_type_id.id,
825                 'partner_id': order.dest_address_id.id or order.partner_id.id,
826                 'date': max([l.date_planned for l in order.order_line]),
827                 'origin': order.name
828             }
829             picking_id = self.pool.get('stock.picking').create(cr, uid, picking_vals, context=context)
830             self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
831
832     def picking_done(self, cr, uid, ids, context=None):
833         self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
834         # Do check on related procurements:
835         proc_obj = self.pool.get("procurement.order")
836         po_lines = []
837         for po in self.browse(cr, uid, ids, context=context):
838             po_lines += [x.id for x in po.order_line]
839         if po_lines:
840             procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', po_lines)], context=context)
841             if procs:
842                 proc_obj.check(cr, uid, procs, context=context)
843         self.message_post(cr, uid, ids, body=_("Products received"), context=context)
844         return True
845
846     def do_merge(self, cr, uid, ids, context=None):
847         """
848         To merge similar type of purchase orders.
849         Orders will only be merged if:
850         * Purchase Orders are in draft
851         * Purchase Orders belong to the same partner
852         * Purchase Orders are have same stock location, same pricelist
853         Lines will only be merged if:
854         * Order lines are exactly the same except for the quantity and unit
855
856          @param self: The object pointer.
857          @param cr: A database cursor
858          @param uid: ID of the user currently logged in
859          @param ids: the ID or list of IDs
860          @param context: A standard dictionary
861
862          @return: new purchase order id
863
864         """
865         #TOFIX: merged order line should be unlink
866         def make_key(br, fields):
867             list_key = []
868             for field in fields:
869                 field_val = getattr(br, field)
870                 if field in ('product_id', 'account_analytic_id'):
871                     if not field_val:
872                         field_val = False
873                 if isinstance(field_val, browse_record):
874                     field_val = field_val.id
875                 elif isinstance(field_val, browse_null):
876                     field_val = False
877                 elif isinstance(field_val, browse_record_list):
878                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
879                 list_key.append((field, field_val))
880             list_key.sort()
881             return tuple(list_key)
882
883         context = dict(context or {})
884
885         # Compute what the new orders should contain
886         new_orders = {}
887
888         order_lines_to_move = []
889         for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
890             order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
891             new_order = new_orders.setdefault(order_key, ({}, []))
892             new_order[1].append(porder.id)
893             order_infos = new_order[0]
894
895             if not order_infos:
896                 order_infos.update({
897                     'origin': porder.origin,
898                     'date_order': porder.date_order,
899                     'partner_id': porder.partner_id.id,
900                     'dest_address_id': porder.dest_address_id.id,
901                     'picking_type_id': porder.picking_type_id.id,
902                     'location_id': porder.location_id.id,
903                     'pricelist_id': porder.pricelist_id.id,
904                     'state': 'draft',
905                     'order_line': {},
906                     'notes': '%s' % (porder.notes or '',),
907                     'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
908                 })
909             else:
910                 if porder.date_order < order_infos['date_order']:
911                     order_infos['date_order'] = porder.date_order
912                 if porder.notes:
913                     order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
914                 if porder.origin:
915                     order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
916
917             for order_line in porder.order_line:
918                 order_lines_to_move += [order_line.id]
919
920         allorders = []
921         orders_info = {}
922         for order_key, (order_data, old_ids) in new_orders.iteritems():
923             # skip merges with only one order
924             if len(old_ids) < 2:
925                 allorders += (old_ids or [])
926                 continue
927
928             # cleanup order line data
929             for key, value in order_data['order_line'].iteritems():
930                 del value['uom_factor']
931                 value.update(dict(key))
932             order_data['order_line'] = [(6, 0, order_lines_to_move)]
933
934             # create the new order
935             context.update({'mail_create_nolog': True})
936             neworder_id = self.create(cr, uid, order_data)
937             self.message_post(cr, uid, [neworder_id], body=_("RFQ created"), context=context)
938             orders_info.update({neworder_id: old_ids})
939             allorders.append(neworder_id)
940
941             # make triggers pointing to the old orders point to the new order
942             for old_id in old_ids:
943                 self.redirect_workflow(cr, uid, [(old_id, neworder_id)])
944                 self.signal_workflow(cr, uid, [old_id], 'purchase_cancel')
945
946         return orders_info
947
948
949 class purchase_order_line(osv.osv):
950     def _amount_line(self, cr, uid, ids, prop, arg, context=None):
951         res = {}
952         cur_obj=self.pool.get('res.currency')
953         tax_obj = self.pool.get('account.tax')
954         for line in self.browse(cr, uid, ids, context=context):
955             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, line.order_id.partner_id)
956             cur = line.order_id.pricelist_id.currency_id
957             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
958         return res
959
960     def _get_uom_id(self, cr, uid, context=None):
961         try:
962             proxy = self.pool.get('ir.model.data')
963             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
964             return result[1]
965         except Exception, ex:
966             return False
967
968     _columns = {
969         'name': fields.text('Description', required=True),
970         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
971         'date_planned': fields.date('Scheduled Date', required=True, select=True),
972         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
973         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
974         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
975         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
976         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
977         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
978         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
979         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
980         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
981         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')],
982                                   'Status', required=True, readonly=True, copy=False,
983                                   help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
984                                        \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
985                                        \n* The \'Done\' status is set automatically when purchase order is set as done. \
986                                        \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
987         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel',
988                                           'order_line_id', 'invoice_id', 'Invoice Lines',
989                                           readonly=True, copy=False),
990         'invoiced': fields.boolean('Invoiced', readonly=True, copy=False),
991         'partner_id': fields.related('order_id', 'partner_id', string='Partner', readonly=True, type="many2one", relation="res.partner", store=True),
992         'date_order': fields.related('order_id', 'date_order', string='Order Date', readonly=True, type="datetime"),
993         'procurement_ids': fields.one2many('procurement.order', 'purchase_line_id', string='Associated procurements'),
994     }
995     _defaults = {
996         'product_uom' : _get_uom_id,
997         'product_qty': lambda *a: 1.0,
998         'state': lambda *args: 'draft',
999         'invoiced': lambda *a: 0,
1000     }
1001     _table = 'purchase_order_line'
1002     _name = 'purchase.order.line'
1003     _description = 'Purchase Order Line'
1004
1005     def unlink(self, cr, uid, ids, context=None):
1006         for line in self.browse(cr, uid, ids, context=context):
1007             if line.state not in ['draft', 'cancel']:
1008                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
1009         procurement_obj = self.pool.get('procurement.order')
1010         procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context)
1011         if procurement_ids_to_cancel:
1012             self.pool['procurement.order'].cancel(cr, uid, procurement_ids_to_cancel)
1013         return super(purchase_order_line, self).unlink(cr, uid, ids, context=context)
1014
1015     def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1016             partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1017             name=False, price_unit=False, state='draft', context=None):
1018         """
1019         onchange handler of product_uom.
1020         """
1021         if context is None:
1022             context = {}
1023         if not uom_id:
1024             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1025         context = dict(context, purchase_uom_check=True)
1026         return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1027             partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
1028             name=name, price_unit=price_unit, state=state, context=context)
1029
1030     def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
1031         """Return the datetime value to use as Schedule Date (``date_planned``) for
1032            PO Lines that correspond to the given product.supplierinfo,
1033            when ordered at `date_order_str`.
1034
1035            :param browse_record | False supplier_info: product.supplierinfo, used to
1036                determine delivery delay (if False, default delay = 0)
1037            :param str date_order_str: date of order field, as a string in
1038                DEFAULT_SERVER_DATETIME_FORMAT
1039            :rtype: datetime
1040            :return: desired Schedule Date for the PO line
1041         """
1042         supplier_delay = int(supplier_info.delay) if supplier_info else 0
1043         return datetime.strptime(date_order_str, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=supplier_delay)
1044
1045     def action_cancel(self, cr, uid, ids, context=None):
1046         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1047         for po_line in self.browse(cr, uid, ids, context=context):
1048             if all([l.state == 'cancel' for l in po_line.order_id.order_line]):
1049                 self.pool.get('purchase.order').action_cancel(cr, uid, [po_line.order_id.id], context=context)
1050
1051     def _check_product_uom_group(self, cr, uid, context=None):
1052         group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom')
1053         res = [user for user in group_uom.users if user.id == uid]
1054         return len(res) and True or False
1055
1056
1057     def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
1058             partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
1059             name=False, price_unit=False, state='draft', context=None):
1060         """
1061         onchange handler of product_id.
1062         """
1063         if context is None:
1064             context = {}
1065
1066         res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
1067         if not product_id:
1068             return res
1069
1070         product_product = self.pool.get('product.product')
1071         product_uom = self.pool.get('product.uom')
1072         res_partner = self.pool.get('res.partner')
1073         product_pricelist = self.pool.get('product.pricelist')
1074         account_fiscal_position = self.pool.get('account.fiscal.position')
1075         account_tax = self.pool.get('account.tax')
1076
1077         # - check for the presence of partner_id and pricelist_id
1078         #if not partner_id:
1079         #    raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
1080         #if not pricelist_id:
1081         #    raise osv.except_osv(_('No Pricelist !'), _('Select a price list in the purchase order form before choosing a product.'))
1082
1083         # - determine name and notes based on product in partner lang.
1084         context_partner = context.copy()
1085         if partner_id:
1086             lang = res_partner.browse(cr, uid, partner_id).lang
1087             context_partner.update( {'lang': lang, 'partner_id': partner_id} )
1088         product = product_product.browse(cr, uid, product_id, context=context_partner)
1089         #call name_get() with partner in the context to eventually match name and description in the seller_ids field
1090         dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0]
1091         if product.description_purchase:
1092             name += '\n' + product.description_purchase
1093         res['value'].update({'name': name})
1094
1095         # - set a domain on product_uom
1096         res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
1097
1098         # - check that uom and product uom belong to the same category
1099         product_uom_po_id = product.uom_po_id.id
1100         if not uom_id:
1101             uom_id = product_uom_po_id
1102
1103         if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
1104             if context.get('purchase_uom_check') and self._check_product_uom_group(cr, uid, context=context):
1105                 res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
1106             uom_id = product_uom_po_id
1107
1108         res['value'].update({'product_uom': uom_id})
1109
1110         # - determine product_qty and date_planned based on seller info
1111         if not date_order:
1112             date_order = fields.datetime.now()
1113
1114
1115         supplierinfo = False
1116         precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Unit of Measure')
1117         for supplier in product.seller_ids:
1118             if partner_id and (supplier.name.id == partner_id):
1119                 supplierinfo = supplier
1120                 if supplierinfo.product_uom.id != uom_id:
1121                     res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
1122                 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
1123                 if float_compare(min_qty , qty, precision_digits=precision) == 1: # If the supplier quantity is greater than entered from user, set minimal.
1124                     if qty:
1125                         res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier has a minimal quantity set to %s %s, you should not purchase less.') % (supplierinfo.min_qty, supplierinfo.product_uom.name)}
1126                     qty = min_qty
1127         dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1128         qty = qty or 1.0
1129         res['value'].update({'date_planned': date_planned or dt})
1130         if qty:
1131             res['value'].update({'product_qty': qty})
1132
1133         price = price_unit
1134         if price_unit is False or price_unit is None:
1135             # - determine price_unit and taxes_id
1136             if pricelist_id:
1137                 date_order_str = datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT)
1138                 price = product_pricelist.price_get(cr, uid, [pricelist_id],
1139                         product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order_str})[pricelist_id]
1140             else:
1141                 price = product.standard_price
1142
1143         taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
1144         fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
1145         taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
1146         res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
1147
1148         return res
1149
1150     product_id_change = onchange_product_id
1151     product_uom_change = onchange_product_uom 
1152
1153     def action_confirm(self, cr, uid, ids, context=None):
1154         self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
1155         return True
1156
1157
1158 class procurement_rule(osv.osv):
1159     _inherit = 'procurement.rule'
1160
1161     def _get_action(self, cr, uid, context=None):
1162         return [('buy', _('Buy'))] + super(procurement_rule, self)._get_action(cr, uid, context=context)
1163
1164
1165 class procurement_order(osv.osv):
1166     _inherit = 'procurement.order'
1167     _columns = {
1168         'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
1169         'purchase_id': fields.related('purchase_line_id', 'order_id', type='many2one', relation='purchase.order', string='Purchase Order'),
1170     }
1171
1172     def propagate_cancel(self, cr, uid, procurement, context=None):
1173         if procurement.rule_id.action == 'buy' and procurement.purchase_line_id:
1174             purchase_line_obj = self.pool.get('purchase.order.line')
1175             if procurement.purchase_line_id.product_qty > procurement.product_qty and procurement.purchase_line_id.order_id.state == 'draft':
1176                 purchase_line_obj.write(cr, uid, [procurement.purchase_line_id.id], {'product_qty': procurement.purchase_line_id.product_qty - procurement.product_qty}, context=context)
1177             else:
1178                 purchase_line_obj.action_cancel(cr, uid, [procurement.purchase_line_id.id], context=context)
1179         return super(procurement_order, self).propagate_cancel(cr, uid, procurement, context=context)
1180
1181     def _run(self, cr, uid, procurement, context=None):
1182         if procurement.rule_id and procurement.rule_id.action == 'buy':
1183             #make a purchase order for the procurement
1184             return self.make_po(cr, uid, [procurement.id], context=context)[procurement.id]
1185         return super(procurement_order, self)._run(cr, uid, procurement, context=context)
1186
1187     def _check(self, cr, uid, procurement, context=None):
1188         if procurement.purchase_line_id and procurement.purchase_line_id.order_id.shipped:  # TOCHECK: does it work for several deliveries?
1189             return True
1190         return super(procurement_order, self)._check(cr, uid, procurement, context=context)
1191
1192     def _check_supplier_info(self, cr, uid, ids, context=None):
1193         ''' Check the supplier info field of a product and write an error message on the procurement if needed.
1194         Returns True if all needed information is there, False if some configuration mistake is detected.
1195         '''
1196         partner_obj = self.pool.get('res.partner')
1197         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1198         for procurement in self.browse(cr, uid, ids, context=context):
1199             message = ''
1200             partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
1201
1202             if not procurement.product_id.seller_ids:
1203                 message = _('No supplier defined for this product !')
1204             elif not partner:
1205                 message = _('No default supplier defined for this product')
1206             elif not partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']:
1207                 message = _('No address defined for the supplier')
1208
1209             if message:
1210                 if procurement.message != message:
1211                     cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
1212                 return False
1213
1214             if user.company_id and user.company_id.partner_id:
1215                 if partner.id == user.company_id.partner_id.id:
1216                     raise osv.except_osv(_('Configuration Error!'), _('The product "%s" has been defined with your company as reseller which seems to be a configuration error!' % procurement.product_id.name))
1217
1218         return True
1219
1220     def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
1221         """Create the purchase order from the procurement, using
1222            the provided field values, after adding the given purchase
1223            order line in the purchase order.
1224
1225            :params procurement: the procurement object generating the purchase order
1226            :params dict po_vals: field values for the new purchase order (the
1227                                  ``order_line`` field will be overwritten with one
1228                                  single line, as passed in ``line_vals``).
1229            :params dict line_vals: field values of the single purchase order line that
1230                                    the purchase order will contain.
1231            :return: id of the newly created purchase order
1232            :rtype: int
1233         """
1234         po_vals.update({'order_line': [(0,0,line_vals)]})
1235         return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
1236
1237     def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
1238         """Return the datetime value to use as Schedule Date (``date_planned``) for the
1239            Purchase Order Lines created to satisfy the given procurement.
1240
1241            :param browse_record procurement: the procurement for which a PO will be created.
1242            :param browse_report company: the company to which the new PO will belong to.
1243            :rtype: datetime
1244            :return: the desired Schedule Date for the PO lines
1245         """
1246         procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
1247         schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
1248         return schedule_date
1249
1250     def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
1251         """Return the datetime value to use as Order Date (``date_order``) for the
1252            Purchase Order created to satisfy the given procurement.
1253
1254            :param browse_record procurement: the procurement for which a PO will be created.
1255            :param browse_report company: the company to which the new PO will belong to.
1256            :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1257            :rtype: datetime
1258            :return: the desired Order Date for the PO
1259         """
1260         seller_delay = int(procurement.product_id.seller_delay)
1261         return schedule_date - relativedelta(days=seller_delay)
1262
1263     def _get_product_supplier(self, cr, uid, procurement, context=None):
1264         ''' returns the main supplier of the procurement's product given as argument'''
1265         return procurement.product_id.seller_id
1266
1267     def _get_po_line_values_from_proc(self, cr, uid, procurement, partner, company, schedule_date, context=None):
1268         if context is None:
1269             context = {}
1270         uom_obj = self.pool.get('product.uom')
1271         pricelist_obj = self.pool.get('product.pricelist')
1272         prod_obj = self.pool.get('product.product')
1273         acc_pos_obj = self.pool.get('account.fiscal.position')
1274
1275         seller_qty = procurement.product_id.seller_qty
1276         pricelist_id = partner.property_product_pricelist_purchase.id
1277         uom_id = procurement.product_id.uom_po_id.id
1278         qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1279         if seller_qty:
1280             qty = max(qty, seller_qty)
1281         price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner.id, {'uom': uom_id})[pricelist_id]
1282
1283         #Passing partner_id to context for purchase order line integrity of Line name
1284         new_context = context.copy()
1285         new_context.update({'lang': partner.lang, 'partner_id': partner.id})
1286         product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
1287         taxes_ids = procurement.product_id.supplier_taxes_id
1288         taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1289         name = product.display_name
1290         if product.description_purchase:
1291             name += '\n' + product.description_purchase
1292
1293         return {
1294             'name': name,
1295             'product_qty': qty,
1296             'product_id': procurement.product_id.id,
1297             'product_uom': uom_id,
1298             'price_unit': price or 0.0,
1299             'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1300             'taxes_id': [(6, 0, taxes)],
1301         }
1302
1303     def make_po(self, cr, uid, ids, context=None):
1304         """ Resolve the purchase from procurement, which may result in a new PO creation, a new PO line creation or a quantity change on existing PO line.
1305         Note that some operations (as the PO creation) are made as SUPERUSER because the current user may not have rights to do it (mto product launched by a sale for example)
1306
1307         @return: dictionary giving for each procurement its related resolving PO line.
1308         """
1309         res = {}
1310         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1311         po_obj = self.pool.get('purchase.order')
1312         po_line_obj = self.pool.get('purchase.order.line')
1313         seq_obj = self.pool.get('ir.sequence')
1314         pass_ids = []
1315         linked_po_ids = []
1316         sum_po_line_ids = []
1317         for procurement in self.browse(cr, uid, ids, context=context):
1318             partner = self._get_product_supplier(cr, uid, procurement, context=context)
1319             if not partner:
1320                 self.message_post(cr, uid, [procurement.id], _('There is no supplier associated to product %s') % (procurement.product_id.name))
1321                 res[procurement.id] = False
1322             else:
1323                 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1324                 purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context) 
1325                 line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context)
1326                 #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one
1327                 available_draft_po_ids = po_obj.search(cr, uid, [
1328                     ('partner_id', '=', partner.id), ('state', '=', 'draft'), ('picking_type_id', '=', procurement.rule_id.picking_type_id.id),
1329                     ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context)
1330                 if available_draft_po_ids:
1331                     po_id = available_draft_po_ids[0]
1332                     po_rec = po_obj.browse(cr, uid, po_id, context=context)
1333                     #if the product has to be ordered earlier those in the existing PO, we replace the purchase date on the order to avoid ordering it too late
1334                     if datetime.strptime(po_rec.date_order, DEFAULT_SERVER_DATETIME_FORMAT) > purchase_date:
1335                         po_obj.write(cr, uid, [po_id], {'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
1336                     #look for any other PO line in the selected PO with same product and UoM to sum quantities instead of creating a new po line
1337                     available_po_line_ids = po_line_obj.search(cr, uid, [('order_id', '=', po_id), ('product_id', '=', line_vals['product_id']), ('product_uom', '=', line_vals['product_uom'])], context=context)
1338                     if available_po_line_ids:
1339                         po_line = po_line_obj.browse(cr, uid, available_po_line_ids[0], context=context)
1340                         po_line_obj.write(cr, SUPERUSER_ID, po_line.id, {'product_qty': po_line.product_qty + line_vals['product_qty']}, context=context)
1341                         po_line_id = po_line.id
1342                         sum_po_line_ids.append(procurement.id)
1343                     else:
1344                         line_vals.update(order_id=po_id)
1345                         po_line_id = po_line_obj.create(cr, SUPERUSER_ID, line_vals, context=context)
1346                         linked_po_ids.append(procurement.id)
1347                 else:
1348                     name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1349                     po_vals = {
1350                         'name': name,
1351                         'origin': procurement.origin,
1352                         'partner_id': partner.id,
1353                         'location_id': procurement.location_id.id,
1354                         'picking_type_id': procurement.rule_id.picking_type_id.id,
1355                         'pricelist_id': partner.property_product_pricelist_purchase.id,
1356                         'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1357                         'company_id': procurement.company_id.id,
1358                         'fiscal_position': partner.property_account_position and partner.property_account_position.id or False,
1359                         'payment_term_id': partner.property_supplier_payment_term.id or False,
1360                         'dest_address_id': procurement.partner_dest_id.id,
1361                     }
1362                     po_id = self.create_procurement_purchase_order(cr, SUPERUSER_ID, procurement, po_vals, line_vals, context=context)
1363                     po_line_id = po_obj.browse(cr, uid, po_id, context=context).order_line[0].id
1364                     pass_ids.append(procurement.id)
1365                 res[procurement.id] = po_line_id
1366                 self.write(cr, uid, [procurement.id], {'purchase_line_id': po_line_id}, context=context)
1367         if pass_ids:
1368             self.message_post(cr, uid, pass_ids, body=_("Draft Purchase Order created"), context=context)
1369         if linked_po_ids:
1370             self.message_post(cr, uid, linked_po_ids, body=_("Purchase line created and linked to an existing Purchase Order"), context=context)
1371         if sum_po_line_ids:
1372             self.message_post(cr, uid, sum_po_line_ids, body=_("Quantity added in existing Purchase Order Line"), context=context)
1373         return res
1374
1375
1376 class mail_mail(osv.Model):
1377     _name = 'mail.mail'
1378     _inherit = 'mail.mail'
1379
1380     def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
1381         if mail_sent and mail.model == 'purchase.order':
1382             obj = self.pool.get('purchase.order').browse(cr, uid, mail.res_id, context=context)
1383             if obj.state == 'draft':
1384                 self.pool.get('purchase.order').signal_workflow(cr, uid, [mail.res_id], 'send_rfq')
1385         return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
1386
1387
1388 class product_template(osv.Model):
1389     _name = 'product.template'
1390     _inherit = 'product.template'
1391     
1392     def _get_buy_route(self, cr, uid, context=None):
1393         
1394         buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy')
1395         if buy_route:
1396             return [buy_route]
1397         return []
1398
1399     def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
1400         res = dict.fromkeys(ids, 0)
1401         for template in self.browse(cr, uid, ids, context=context):
1402             res[template.id] = sum([p.purchase_count for p in template.product_variant_ids])
1403         return res
1404
1405     _columns = {
1406         'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
1407         'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
1408     }
1409
1410     _defaults = {
1411         'purchase_ok': 1,
1412         'route_ids': _get_buy_route,
1413     }
1414
1415     def action_view_purchases(self, cr, uid, ids, context=None):
1416         products = self._get_products(cr, uid, ids, context=context)
1417         result = self._get_act_window_dict(cr, uid, 'purchase.action_purchase_line_product_tree', context=context)
1418         result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
1419         return result
1420
1421 class product_product(osv.Model):
1422     _name = 'product.product'
1423     _inherit = 'product.product'
1424     
1425     def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
1426         Purchase = self.pool['purchase.order']
1427         return {
1428             product_id: Purchase.search_count(cr,uid, [('order_line.product_id', '=', product_id)], context=context) 
1429             for product_id in ids
1430         }
1431
1432     _columns = {
1433         'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
1434     }
1435
1436
1437
1438 class mail_compose_message(osv.Model):
1439     _inherit = 'mail.compose.message'
1440
1441     def send_mail(self, cr, uid, ids, context=None):
1442         context = context or {}
1443         if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
1444             context = dict(context, mail_post_autofollow=True)
1445             self.pool.get('purchase.order').signal_workflow(cr, uid, [context['default_res_id']], 'send_rfq')
1446         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1447
1448
1449 class account_invoice(osv.Model):
1450     """ Override account_invoice to add Chatter messages on the related purchase
1451         orders, logging the invoice receipt or payment. """
1452     _inherit = 'account.invoice'
1453
1454     def invoice_validate(self, cr, uid, ids, context=None):
1455         res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context)
1456         purchase_order_obj = self.pool.get('purchase.order')
1457         # read access on purchase.order object is not required
1458         if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1459             user_id = SUPERUSER_ID
1460         else:
1461             user_id = uid
1462         po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1463         for order in purchase_order_obj.browse(cr, uid, po_ids, context=context):
1464             purchase_order_obj.message_post(cr, user_id, order.id, body=_("Invoice received"), context=context)
1465             invoiced = []
1466             for po_line in order.order_line:
1467                 if any(line.invoice_id.state not in ['draft', 'cancel'] for line in po_line.invoice_lines):
1468                     invoiced.append(po_line.id)
1469             if invoiced:
1470                 self.pool['purchase.order.line'].write(cr, uid, invoiced, {'invoiced': True})
1471             workflow.trg_write(uid, 'purchase.order', order.id, cr)
1472         return res
1473
1474     def confirm_paid(self, cr, uid, ids, context=None):
1475         res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1476         purchase_order_obj = self.pool.get('purchase.order')
1477         # read access on purchase.order object is not required
1478         if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
1479             user_id = SUPERUSER_ID
1480         else:
1481             user_id = uid
1482         po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
1483         for po_id in po_ids:
1484             purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice paid"), context=context)
1485         return res
1486
1487 class account_invoice_line(osv.Model):
1488     """ Override account_invoice_line to add the link to the purchase order line it is related to"""
1489     _inherit = 'account.invoice.line'
1490     _columns = {
1491         'purchase_line_id': fields.many2one('purchase.order.line',
1492             'Purchase Order Line', ondelete='set null', select=True,
1493             readonly=True),
1494     }
1495
1496
1497 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: