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