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