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