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