[FIX] purchase : changed name of updated context variable in def product_id_change...
[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 time
23 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
25
26 from osv import osv, fields
27 import netsvc
28 import pooler
29 from tools.translate import _
30 import decimal_precision as dp
31 from osv.orm import browse_record, browse_null
32 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
33
34 #
35 # Model definition
36 #
37 class purchase_order(osv.osv):
38
39     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
40         res = {}
41         cur_obj=self.pool.get('res.currency')
42         for order in self.browse(cr, uid, ids, context=context):
43             res[order.id] = {
44                 'amount_untaxed': 0.0,
45                 'amount_tax': 0.0,
46                 'amount_total': 0.0,
47             }
48             val = val1 = 0.0
49             cur = order.pricelist_id.currency_id
50             for line in order.order_line:
51                val1 += line.price_subtotal
52                for c in self.pool.get('account.tax').compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, order.partner_address_id.id, line.product_id.id, order.partner_id)['taxes']:
53                     val += c.get('amount', 0.0)
54             res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
55             res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
56             res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
57         return res
58
59     def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
60         if not value: return False
61         if type(ids)!=type([]):
62             ids=[ids]
63         for po in self.browse(cr, uid, ids, context=context):
64             if po.order_line:
65                 cr.execute("""update purchase_order_line set
66                         date_planned=%s
67                     where
68                         order_id=%s and
69                         (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
70             cr.execute("""update purchase_order set
71                     minimum_planned_date=%s where id=%s""", (value, po.id))
72         return True
73
74     def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
75         res={}
76         purchase_obj=self.browse(cr, uid, ids, context=context)
77         for purchase in purchase_obj:
78             res[purchase.id] = False
79             if purchase.order_line:
80                 min_date=purchase.order_line[0].date_planned
81                 for line in purchase.order_line:
82                     if line.date_planned < min_date:
83                         min_date=line.date_planned
84                 res[purchase.id]=min_date
85         return res
86
87
88     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
89         res = {}
90         for purchase in self.browse(cursor, user, ids, context=context):
91             tot = 0.0
92             for invoice in purchase.invoice_ids:
93                 if invoice.state not in ('draft','cancel'):
94                     tot += invoice.amount_untaxed
95             if purchase.amount_untaxed:
96                 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
97             else:
98                 res[purchase.id] = 0.0
99         return res
100
101     def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
102         if not ids: return {}
103         res = {}
104         for id in ids:
105             res[id] = [0.0,0.0]
106         cr.execute('''SELECT
107                 p.purchase_id,sum(m.product_qty), m.state
108             FROM
109                 stock_move m
110             LEFT JOIN
111                 stock_picking p on (p.id=m.picking_id)
112             WHERE
113                 p.purchase_id IN %s GROUP BY m.state, p.purchase_id''',(tuple(ids),))
114         for oid,nbr,state in cr.fetchall():
115             if state=='cancel':
116                 continue
117             if state=='done':
118                 res[oid][0] += nbr or 0.0
119                 res[oid][1] += nbr or 0.0
120             else:
121                 res[oid][1] += nbr or 0.0
122         for r in res:
123             if not res[r][1]:
124                 res[r] = 0.0
125             else:
126                 res[r] = 100.0 * res[r][0] / res[r][1]
127         return res
128
129     def _get_order(self, cr, uid, ids, context=None):
130         result = {}
131         for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
132             result[line.order_id.id] = True
133         return result.keys()
134
135     def _invoiced(self, cursor, user, ids, name, arg, context=None):
136         res = {}
137         for purchase in self.browse(cursor, user, ids, context=context):
138             invoiced = False
139             if purchase.invoiced_rate == 100.00:
140                 invoiced = True
141             res[purchase.id] = invoiced
142         return res
143
144     STATE_SELECTION = [
145         ('draft', 'Request for Quotation'),
146         ('wait', 'Waiting'),
147         ('confirmed', 'Waiting Approval'),
148         ('approved', 'Approved'),
149         ('except_picking', 'Shipping Exception'),
150         ('except_invoice', 'Invoice Exception'),
151         ('done', 'Done'),
152         ('cancel', 'Cancelled')
153     ]
154
155     _columns = {
156         'name': fields.char('Order Reference', size=64, required=True, select=True, help="unique number of the purchase order,computed automatically when the purchase order is created"),
157         'origin': fields.char('Source Document', size=64,
158             help="Reference of the document that generated this purchase order request."
159         ),
160         'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, size=64),
161         'date_order':fields.date('Order Date', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, select=True, help="Date on which this document has been created."),
162         'date_approve':fields.date('Date Approved', readonly=1, select=True, help="Date on which purchase order has been approved"),
163         'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, change_default=True),
164         'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True,
165             states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},domain="[('partner_id', '=', partner_id)]"),
166         'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', domain="[('partner_id', '!=', False)]",
167             states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
168             help="Put an address if you want to deliver directly from the supplier to the customer." \
169                 "In this case, it will remove the warehouse link and set the customer location."
170         ),
171         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}),
172         'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')]),
173         '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."),
174         'state': fields.selection(STATE_SELECTION, 'State', readonly=True, help="The state of the purchase order or the quotation request. A quotation is a purchase order in a 'Draft' state. Then the order has to be confirmed by the user, the state switch to 'Confirmed'. Then the supplier must confirm the order to change the state to 'Approved'. When the purchase order is paid and received, the state becomes 'Done'. If a cancel action occurs in the invoice or in the reception of goods, the state becomes in exception.", select=True),
175         'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
176         'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
177         'notes': fields.text('Notes'),
178         'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id', 'invoice_id', 'Invoices', help="Invoices generated for a purchase order"),
179         'picking_ids': fields.one2many('stock.picking', 'purchase_id', 'Picking List', readonly=True, help="This is the list of picking list that have been generated for this purchase"),
180         'shipped':fields.boolean('Received', readonly=True, select=True, help="It indicates that a picking has been done"),
181         'shipped_rate': fields.function(_shipped_rate, string='Received', type='float'),
182         'invoiced': fields.function(_invoiced, string='Invoiced & Paid', type='boolean', help="It indicates that an invoice has been paid"),
183         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
184         'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on receptions')], 'Invoicing Control', required=True,
185             help="Based on Purchase Order lines: place individual lines in 'Invoice Control > Based on P.O. lines' from where you can selectively create an invoice.\n" \
186                 "Based on generated invoice: create a draft invoice you can validate later.\n" \
187                 "Based on receptions: let you create an invoice when receptions are validated."
188         ),
189         '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.",
190             store = {
191                 'purchase.order.line': (_get_order, ['date_planned'], 10),
192             }
193         ),
194         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Untaxed Amount',
195             store={
196                 'purchase.order.line': (_get_order, None, 10),
197             }, multi="sums", help="The amount without tax"),
198         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Taxes',
199             store={
200                 'purchase.order.line': (_get_order, None, 10),
201             }, multi="sums", help="The tax amount"),
202         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Total',
203             store={
204                 'purchase.order.line': (_get_order, None, 10),
205             }, multi="sums",help="The total amount"),
206         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
207         'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
208         'create_uid':  fields.many2one('res.users', 'Responsible'),
209         'company_id': fields.many2one('res.company','Company',required=True,select=1),
210     }
211     _defaults = {
212         'date_order': lambda *a: time.strftime('%Y-%m-%d'),
213         'state': 'draft',
214         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
215         'shipped': 0,
216         'invoice_method': 'order',
217         'invoiced': 0,
218         'partner_address_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['default'])['default'],
219         '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,
220         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
221     }
222     _sql_constraints = [
223         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
224     ]
225     _name = "purchase.order"
226     _description = "Purchase Order"
227     _order = "name desc"
228
229     def unlink(self, cr, uid, ids, context=None):
230         purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
231         unlink_ids = []
232         for s in purchase_orders:
233             if s['state'] in ['draft','cancel']:
234                 unlink_ids.append(s['id'])
235             else:
236                 raise osv.except_osv(_('Invalid action !'), _('In order to delete a purchase order, it must be cancelled first!'))
237
238         # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
239         # automatically sending subflow.delete upon deletion
240         wf_service = netsvc.LocalService("workflow")
241         for id in unlink_ids:
242             wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
243
244         return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
245
246     def button_dummy(self, cr, uid, ids, context=None):
247         return True
248
249     def onchange_dest_address_id(self, cr, uid, ids, address_id):
250         if not address_id:
251             return {}
252         address = self.pool.get('res.partner.address')
253         values = {'warehouse_id': False}
254         supplier = address.browse(cr, uid, address_id).partner_id
255         if supplier:
256             location_id = supplier.property_stock_customer.id
257             values.update({'location_id': location_id})
258         return {'value':values}
259
260     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
261         if not warehouse_id:
262             return {}
263         warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id)
264         return {'value':{'location_id': warehouse.lot_input_id.id, 'dest_address_id': False}}
265
266     def onchange_partner_id(self, cr, uid, ids, partner_id):
267         partner = self.pool.get('res.partner')
268         if not partner_id:
269             return {'value':{'partner_address_id': False, 'fiscal_position': False}}
270         supplier_address = partner.address_get(cr, uid, [partner_id], ['default'])
271         supplier = partner.browse(cr, uid, partner_id)
272         pricelist = supplier.property_product_pricelist_purchase.id
273         fiscal_position = supplier.property_account_position and supplier.property_account_position.id or False
274         return {'value':{'partner_address_id': supplier_address['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
275
276     def wkf_approve_order(self, cr, uid, ids, context=None):
277         self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
278         return True
279
280     #TODO: implement messages system
281     def wkf_confirm_order(self, cr, uid, ids, context=None):
282         todo = []
283         for po in self.browse(cr, uid, ids, context=context):
284             if not po.order_line:
285                 raise osv.except_osv(_('Error !'),_('You cannot confirm a purchase order without any lines.'))
286             for line in po.order_line:
287                 if line.state=='draft':
288                     todo.append(line.id)
289             message = _("Purchase order '%s' is confirmed.") % (po.name,)
290             self.log(cr, uid, po.id, message)
291 #        current_name = self.name_get(cr, uid, ids)[0][1]
292         self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
293         for id in ids:
294             self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
295         return True
296
297     def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
298         """Collects require data from purchase order line that is used to create invoice line 
299         for that purchase order line
300         :param account_id: Expense account of the product of PO line if any.
301         :param browse_record order_line: Purchase order line browse record
302         :return: Value for fields of invoice lines.
303         :rtype: dict
304         """
305         return {
306             'name': order_line.name,
307             'account_id': account_id,
308             'price_unit': order_line.price_unit or 0.0,
309             'quantity': order_line.product_qty,
310             'product_id': order_line.product_id.id or False,
311             'uos_id': order_line.product_uom.id or False,
312             'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
313             'account_analytic_id': order_line.account_analytic_id.id or False,
314         }
315
316     def action_cancel_draft(self, cr, uid, ids, *args):
317         if not len(ids):
318             return False
319         self.write(cr, uid, ids, {'state':'draft','shipped':0})
320         wf_service = netsvc.LocalService("workflow")
321         for p_id in ids:
322             # Deleting the existing instance of workflow for PO
323             wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
324             wf_service.trg_create(uid, 'purchase.order', p_id, cr)
325         for (id,name) in self.name_get(cr, uid, ids):
326             message = _("Purchase order '%s' has been set in draft state.") % name
327             self.log(cr, uid, id, message)
328         return True
329
330     def action_invoice_create(self, cr, uid, ids, context=None):
331         """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
332         :param ids: list of ids of purchase orders.
333         :return: ID of created invoice.
334         :rtype: int
335         """
336         res = False
337
338         journal_obj = self.pool.get('account.journal')
339         inv_obj = self.pool.get('account.invoice')
340         inv_line_obj = self.pool.get('account.invoice.line')
341         fiscal_obj = self.pool.get('account.fiscal.position')
342         property_obj = self.pool.get('ir.property')
343
344         for order in self.browse(cr, uid, ids, context=context):
345             pay_acc_id = order.partner_id.property_account_payable.id
346             journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', order.company_id.id)], limit=1)
347             if not journal_ids:
348                 raise osv.except_osv(_('Error !'),
349                     _('There is no purchase journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
350
351             # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
352             inv_lines = []
353             for po_line in order.order_line:
354                 if po_line.product_id:
355                     acc_id = po_line.product_id.product_tmpl_id.property_account_expense.id
356                     if not acc_id:
357                         acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
358                     if not acc_id:
359                         raise osv.except_osv(_('Error !'), _('There is no expense account defined for this product: "%s" (id:%d)') % (po_line.product_id.name, po_line.product_id.id,))
360                 else:
361                     acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category').id
362                 fpos = order.fiscal_position or False
363                 acc_id = fiscal_obj.map_account(cr, uid, fpos, acc_id)
364
365                 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
366                 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
367                 inv_lines.append(inv_line_id)
368
369                 po_line.write({'invoiced':True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
370
371             # get invoice data and create invoice
372             inv_data = {
373                 'name': order.partner_ref or order.name,
374                 'reference': order.partner_ref or order.name,
375                 'account_id': pay_acc_id,
376                 'type': 'in_invoice',
377                 'partner_id': order.partner_id.id,
378                 'currency_id': order.pricelist_id.currency_id.id,
379                 'address_invoice_id': order.partner_address_id.id,
380                 'address_contact_id': order.partner_address_id.id,
381                 'journal_id': len(journal_ids) and journal_ids[0] or False,
382                 'invoice_line': [(6, 0, inv_lines)], 
383                 'origin': order.name,
384                 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
385                 'payment_term': order.partner_id.property_payment_term and order.partner_id.property_payment_term.id or False,
386                 'company_id': order.company_id.id,
387             }
388             inv_id = inv_obj.create(cr, uid, inv_data, context=context)
389
390             # compute the invoice
391             inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
392
393             # Link this new invoice to related purchase order
394             order.write({'invoice_ids': [(4, inv_id)]}, context=context)
395             res = inv_id
396         return res
397
398     def has_stockable_product(self,cr, uid, ids, *args):
399         for order in self.browse(cr, uid, ids):
400             for order_line in order.order_line:
401                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
402                     return True
403         return False
404
405     def action_cancel(self, cr, uid, ids, context=None):
406         wf_service = netsvc.LocalService("workflow")
407         for purchase in self.browse(cr, uid, ids, context=context):
408             for pick in purchase.picking_ids:
409                 if pick.state not in ('draft','cancel'):
410                     raise osv.except_osv(
411                         _('Unable to cancel this purchase order!'),
412                         _('You must first cancel all receptions related to this purchase order.'))
413             for pick in purchase.picking_ids:
414                 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
415             for inv in purchase.invoice_ids:
416                 if inv and inv.state not in ('cancel','draft'):
417                     raise osv.except_osv(
418                         _('Unable to cancel this purchase order!'),
419                         _('You must first cancel all invoices related to this purchase order.'))
420                 if inv:
421                     wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
422         self.write(cr,uid,ids,{'state':'cancel'})
423         
424         for (id, name) in self.name_get(cr, uid, ids):
425             wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
426             message = _("Purchase order '%s' is cancelled.") % name
427             self.log(cr, uid, id, message)
428         return True
429
430     def _prepare_order_picking(self, cr, uid, order, context=None):
431         return {
432             'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
433             'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
434             'date': order.date_order,
435             'type': 'in',
436             'address_id': order.dest_address_id.id or order.partner_address_id.id,
437             'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
438             'purchase_id': order.id,
439             'company_id': order.company_id.id,
440             'move_lines' : [],
441         }
442          
443     def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, context=None):
444         return {
445             'name': order.name + ': ' + (order_line.name or ''),
446             'product_id': order_line.product_id.id,
447             'product_qty': order_line.product_qty,
448             'product_uos_qty': order_line.product_qty,
449             'product_uom': order_line.product_uom.id,
450             'product_uos': order_line.product_uom.id,
451             'date': order_line.date_planned,
452             'date_expected': order_line.date_planned,
453             'location_id': order.partner_id.property_stock_supplier.id,
454             'location_dest_id': order.location_id.id,
455             'picking_id': picking_id,
456             'address_id': order.dest_address_id.id or order.partner_address_id.id,
457             'move_dest_id': order_line.move_dest_id.id,
458             'state': 'draft',
459             'purchase_line_id': order_line.id,
460             'company_id': order.company_id.id,
461             'price_unit': order_line.price_unit
462         }
463
464     def _create_pickings(self, cr, uid, order, order_lines, picking_id=False, context=None):
465         """Creates pickings and appropriate stock moves for given order lines, then
466         confirms the moves, makes them available, and confirms the picking.
467
468         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
469         a standard outgoing picking will be created to wrap the stock moves, as returned
470         by :meth:`~._prepare_order_picking`.
471
472         Modules that wish to customize the procurements or partition the stock moves over
473         multiple stock pickings may override this method and call ``super()`` with
474         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
475
476         :param browse_record order: purchase order to which the order lines belong
477         :param list(browse_record) order_lines: purchase order line records for which picking
478                                                 and moves should be created.
479         :param int picking_id: optional ID of a stock picking to which the created stock moves
480                                will be added. A new picking will be created if omitted.
481         :return: list of IDs of pickings used/created for the given order lines (usually just one)
482         """
483         if not picking_id: 
484             picking_id = self.pool.get('stock.picking').create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
485         todo_moves = []
486         stock_move = self.pool.get('stock.move')
487         wf_service = netsvc.LocalService("workflow")
488         for order_line in order_lines:
489             if not order_line.product_id:
490                 continue
491             if order_line.product_id.type in ('product', 'consu'):
492                 move = stock_move.create(cr, uid, self._prepare_order_line_move(cr, uid, order, order_line, picking_id, context=context))
493                 if order_line.move_dest_id:
494                     order_line.move_dest_id.write({'location_id': order.location_id.id})
495                 todo_moves.append(move)
496         stock_move.action_confirm(cr, uid, todo_moves)
497         stock_move.force_assign(cr, uid, todo_moves)
498         wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
499         return [picking_id]
500
501     def action_picking_create(self,cr, uid, ids, context=None):
502         picking_ids = []
503         for order in self.browse(cr, uid, ids):
504             picking_ids.extend(self._create_pickings(cr, uid, order, order.order_line, None, context=context))
505
506         # Must return one unique picking ID: the one to connect in the subflow of the purchase order.
507         # In case of multiple (split) pickings, we should return the ID of the critical one, i.e. the
508         # one that should trigger the advancement of the purchase workflow.
509         # By default we will consider the first one as most important, but this behavior can be overridden.
510         return picking_ids[0] if picking_ids else False
511
512     def copy(self, cr, uid, id, default=None, context=None):
513         if not default:
514             default = {}
515         default.update({
516             'state':'draft',
517             'shipped':False,
518             'invoiced':False,
519             'invoice_ids': [],
520             'picking_ids': [],
521             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
522         })
523         return super(purchase_order, self).copy(cr, uid, id, default, context)
524
525     def do_merge(self, cr, uid, ids, context=None):
526         """
527         To merge similar type of purchase orders.
528         Orders will only be merged if:
529         * Purchase Orders are in draft
530         * Purchase Orders belong to the same partner
531         * Purchase Orders are have same stock location, same pricelist
532         Lines will only be merged if:
533         * Order lines are exactly the same except for the quantity and unit
534
535          @param self: The object pointer.
536          @param cr: A database cursor
537          @param uid: ID of the user currently logged in
538          @param ids: the ID or list of IDs
539          @param context: A standard dictionary
540
541          @return: new purchase order id
542
543         """
544         #TOFIX: merged order line should be unlink
545         wf_service = netsvc.LocalService("workflow")
546         def make_key(br, fields):
547             list_key = []
548             for field in fields:
549                 field_val = getattr(br, field)
550                 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
551                     if not field_val:
552                         field_val = False
553                 if isinstance(field_val, browse_record):
554                     field_val = field_val.id
555                 elif isinstance(field_val, browse_null):
556                     field_val = False
557                 elif isinstance(field_val, list):
558                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
559                 list_key.append((field, field_val))
560             list_key.sort()
561             return tuple(list_key)
562
563     # compute what the new orders should contain
564
565         new_orders = {}
566
567         for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
568             order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
569             new_order = new_orders.setdefault(order_key, ({}, []))
570             new_order[1].append(porder.id)
571             order_infos = new_order[0]
572             if not order_infos:
573                 order_infos.update({
574                     'origin': porder.origin,
575                     'date_order': porder.date_order,
576                     'partner_id': porder.partner_id.id,
577                     'partner_address_id': porder.partner_address_id.id,
578                     'dest_address_id': porder.dest_address_id.id,
579                     'warehouse_id': porder.warehouse_id.id,
580                     'location_id': porder.location_id.id,
581                     'pricelist_id': porder.pricelist_id.id,
582                     'state': 'draft',
583                     'order_line': {},
584                     'notes': '%s' % (porder.notes or '',),
585                     'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
586                 })
587             else:
588                 if porder.date_order < order_infos['date_order']:
589                     order_infos['date_order'] = porder.date_order
590                 if porder.notes:
591                     order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
592                 if porder.origin:
593                     order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
594
595             for order_line in porder.order_line:
596                 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
597                 o_line = order_infos['order_line'].setdefault(line_key, {})
598                 if o_line:
599                     # merge the line with an existing line
600                     o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
601                 else:
602                     # append a new "standalone" line
603                     for field in ('product_qty', 'product_uom'):
604                         field_val = getattr(order_line, field)
605                         if isinstance(field_val, browse_record):
606                             field_val = field_val.id
607                         o_line[field] = field_val
608                     o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
609
610
611
612         allorders = []
613         orders_info = {}
614         for order_key, (order_data, old_ids) in new_orders.iteritems():
615             # skip merges with only one order
616             if len(old_ids) < 2:
617                 allorders += (old_ids or [])
618                 continue
619
620             # cleanup order line data
621             for key, value in order_data['order_line'].iteritems():
622                 del value['uom_factor']
623                 value.update(dict(key))
624             order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
625
626             # create the new order
627             neworder_id = self.create(cr, uid, order_data)
628             orders_info.update({neworder_id: old_ids})
629             allorders.append(neworder_id)
630
631             # make triggers pointing to the old orders point to the new order
632             for old_id in old_ids:
633                 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
634                 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
635         return orders_info
636
637 purchase_order()
638
639 class purchase_order_line(osv.osv):
640     def _amount_line(self, cr, uid, ids, prop, arg, context=None):
641         res = {}
642         cur_obj=self.pool.get('res.currency')
643         tax_obj = self.pool.get('account.tax')
644         for line in self.browse(cr, uid, ids, context=context):
645             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
646             cur = line.order_id.pricelist_id.currency_id
647             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
648         return res
649
650     def _get_uom_id(self, cr, uid, context=None):
651         try:
652             proxy = self.pool.get('ir.model.data')
653             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
654             return result[1]
655         except Exception, ex:
656             return False
657
658     _columns = {
659         'name': fields.char('Description', size=256, required=True),
660         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product UoM'), required=True),
661         'date_planned': fields.date('Scheduled Date', required=True, select=True),
662         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
663         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
664         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
665         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
666         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
667         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
668         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
669         'notes': fields.text('Notes'),
670         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
671         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
672         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
673         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
674                                   help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
675                                        \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
676                                        \n* The \'Done\' state is set automatically when purchase order is set as done. \
677                                        \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
678         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
679         'invoiced': fields.boolean('Invoiced', readonly=True),
680         'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
681         'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
682
683     }
684     _defaults = {
685         'product_uom' : _get_uom_id,
686         'product_qty': lambda *a: 1.0,
687         'state': lambda *args: 'draft',
688         'invoiced': lambda *a: 0,
689     }
690     _table = 'purchase_order_line'
691     _name = 'purchase.order.line'
692     _description = 'Purchase Order Line'
693
694     def copy_data(self, cr, uid, id, default=None, context=None):
695         if not default:
696             default = {}
697         default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
698         return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
699
700     #TOFIX:
701     # - name of method should "onchange_product_id"
702     # - docstring
703     # - merge 'product_uom_change' method
704     # - split into small internal methods for clearity 
705     def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
706             partner_id, date_order=False, fiscal_position=False, date_planned=False,
707             name=False, price_unit=False, notes=False, context={}):
708         if not pricelist:
709             raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist or a supplier in the purchase form !\nPlease set one before choosing a product.'))
710         if not  partner_id:
711             raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
712         if not product:
713             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
714                 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
715         res = {}
716         prod= self.pool.get('product.product').browse(cr, uid, product)
717         product_uom_pool = self.pool.get('product.uom')
718         lang=False
719         if partner_id:
720             lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
721             
722         context_partner = {'lang':lang, 'partner_id': partner_id}
723         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
724         prod_uom_po = prod.uom_po_id.id
725         if not uom:
726             uom = prod_uom_po
727         if not date_order:
728             date_order = time.strftime('%Y-%m-%d')
729         qty = qty or 1.0
730         seller_delay = 0
731         if uom:
732             uom1_cat = prod.uom_id.category_id.id
733             uom2_cat = product_uom_pool.browse(cr, uid, uom).category_id.id
734             if uom1_cat != uom2_cat:
735                 uom = False
736                 
737         prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id], context=context_partner)[0][1]
738         res = {}
739         for s in prod.seller_ids:
740             if s.name.id == partner_id:
741                 seller_delay = s.delay
742                 if s.product_uom:
743                     temp_qty = product_uom_pool._compute_qty(cr, uid, s.product_uom.id, s.min_qty, to_uom_id=prod.uom_id.id)
744                     uom = s.product_uom.id #prod_uom_po
745                 temp_qty = s.min_qty # supplier _qty assigned to temp
746                 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
747                     qty = temp_qty
748                     res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier has a minimal quantity set to %s, you should not purchase less.') % qty}})
749         qty_in_product_uom = product_uom_pool._compute_qty(cr, uid, uom, qty, to_uom_id=prod.uom_id.id)
750         price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
751                     product, qty_in_product_uom or 1.0, partner_id, {
752                         'uom': uom,
753                         'date': date_order,
754                         })[pricelist]
755         dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
756
757
758         res.update({'value': {'price_unit': price, 'name': prod_name,
759             'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
760             'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
761             'product_qty': qty,
762             'product_uom': prod.uom_id.id}})
763         domain = {}
764
765         taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
766         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
767         res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
768         res2 = self.pool.get('product.uom').read(cr, uid, [prod.uom_id.id], ['category_id'])
769         res3 = prod.uom_id.category_id.id
770         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
771         if res2[0]['category_id'][0] != res3:
772             raise osv.except_osv(_('Wrong Product UOM !'), _('You have to select a product UOM in the same category than the purchase UOM of the product'))
773
774         res['domain'] = domain
775         return res
776
777     #TOFIX:
778     # - merge into 'product_id_change' method
779     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
780             partner_id, date_order=False, fiscal_position=False, date_planned=False,
781             name=False, price_unit=False, notes=False, context={}):
782         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
783                 partner_id, date_order=date_order, fiscal_position=fiscal_position, date_planned=date_planned,
784             name=name, price_unit=price_unit, notes=notes, context=context)
785         if 'product_uom' in res['value']:
786             if uom and (uom != res['value']['product_uom']) and res['value']['product_uom']:
787                 seller_uom_name = self.pool.get('product.uom').read(cr, uid, [res['value']['product_uom']], ['name'])[0]['name']
788                 res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier only sells this product by %s') % seller_uom_name }})
789             del res['value']['product_uom']
790         if not uom:
791             res['value']['price_unit'] = 0.0
792         return res
793
794     def action_confirm(self, cr, uid, ids, context=None):
795         self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
796         return True
797
798 purchase_order_line()
799
800 class procurement_order(osv.osv):
801     _inherit = 'procurement.order'
802     _columns = {
803         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
804     }
805
806     def action_po_assign(self, cr, uid, ids, context=None):
807         """ This is action which call from workflow to assign purchase order to procurements
808         @return: True
809         """
810         res = self.make_po(cr, uid, ids, context=context)
811         res = res.values()
812         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
813
814     def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
815         """Create the purchase order from the procurement, using
816            the provided field values, after adding the given purchase
817            order line in the purchase order.
818
819            :params procurement: the procurement object generating the purchase order
820            :params dict po_vals: field values for the new purchase order (the
821                                  ``order_line`` field will be overwritten with one
822                                  single line, as passed in ``line_vals``).
823            :params dict line_vals: field values of the single purchase order line that
824                                    the purchase order will contain.
825            :return: id of the newly created purchase order
826            :rtype: int
827         """
828         po_vals.update({'order_line': [(0,0,line_vals)]})
829         return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
830
831     def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
832         """Return the datetime value to use as Schedule Date (``date_planned``) for the
833            Purchase Order Lines created to satisfy the given procurement.
834
835            :param browse_record procurement: the procurement for which a PO will be created.
836            :param browse_report company: the company to which the new PO will belong to.
837            :rtype: datetime
838            :return: the desired Schedule Date for the PO lines
839         """
840         procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
841         schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
842         return schedule_date
843
844     def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
845         """Return the datetime value to use as Order Date (``date_order``) for the
846            Purchase Order created to satisfy the given procurement.
847
848            :param browse_record procurement: the procurement for which a PO will be created.
849            :param browse_report company: the company to which the new PO will belong to.
850            :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
851            :rtype: datetime
852            :return: the desired Order Date for the PO
853         """
854         seller_delay = int(procurement.product_id.seller_delay)
855         return schedule_date - relativedelta(days=seller_delay)
856
857     def make_po(self, cr, uid, ids, context=None):
858         """ Make purchase order from procurement
859         @return: New created Purchase Orders procurement wise
860         """
861         res = {}
862         if context is None:
863             context = {}
864         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
865         partner_obj = self.pool.get('res.partner')
866         uom_obj = self.pool.get('product.uom')
867         pricelist_obj = self.pool.get('product.pricelist')
868         prod_obj = self.pool.get('product.product')
869         acc_pos_obj = self.pool.get('account.fiscal.position')
870         seq_obj = self.pool.get('ir.sequence')
871         warehouse_obj = self.pool.get('stock.warehouse')
872         for procurement in self.browse(cr, uid, ids, context=context):
873             res_id = procurement.move_id.id
874             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
875             seller_qty = procurement.product_id.seller_qty
876             partner_id = partner.id
877             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
878             pricelist_id = partner.property_product_pricelist_purchase.id
879             warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
880             uom_id = procurement.product_id.uom_po_id.id
881
882             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
883             if seller_qty:
884                 qty = max(qty,seller_qty)
885
886             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
887
888             schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
889             purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
890
891             #Passing partner_id to context for purchase order line integrity of Line name
892             context.update({'lang': partner.lang, 'partner_id': partner_id})
893
894             product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
895             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
896             taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
897
898             line_vals = {
899                 'name': product.partner_ref,
900                 'product_qty': qty,
901                 'product_id': procurement.product_id.id,
902                 'product_uom': uom_id,
903                 'price_unit': price or 0.0,
904                 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
905                 'move_dest_id': res_id,
906                 'notes': product.description_purchase,
907                 'taxes_id': [(6,0,taxes)],
908             }
909             name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
910             po_vals = {
911                 'name': name,
912                 'origin': procurement.origin,
913                 'partner_id': partner_id,
914                 'partner_address_id': address_id,
915                 'location_id': procurement.location_id.id,
916                 'warehouse_id': warehouse_id and warehouse_id[0] or False,
917                 'pricelist_id': pricelist_id,
918                 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
919                 'company_id': procurement.company_id.id,
920                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
921             }
922             res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=context)
923             self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]})
924         return res
925
926 procurement_order()
927 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: