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