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