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