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