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