[commit]
[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):
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: allows you to generate suppliers invoices by chosing in the uninvoiced lines of all manual purchase orders."
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         return True
271
272     #TODO: implement messages system
273     def wkf_confirm_order(self, cr, uid, ids, context={}):
274         todo = []
275         for po in self.browse(cr, uid, ids):
276             if not po.order_line:
277                 raise osv.except_osv(_('Error !'),_('You can not confirm purchase order without Purchase Order Lines.'))
278             for line in po.order_line:
279                 if line.state=='draft':
280                     todo.append(line.id)
281             message = _("Purchase order '%s' is confirmed.") % (po.name,)
282             self.log(cr, uid, po.id, message)
283 #        current_name = self.name_get(cr, uid, ids)[0][1]
284         self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
285         for id in ids:
286             self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
287         return True
288
289     def wkf_warn_buyer(self, cr, uid, ids):
290         self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
291         request = pooler.get_pool(cr.dbname).get('res.request')
292         for po in self.browse(cr, uid, ids):
293             managers = []
294             for oline in po.order_line:
295                 manager = oline.product_id.product_manager
296                 if manager and not (manager.id in managers):
297                     managers.append(manager.id)
298             for manager_id in managers:
299                 request.create(cr, uid,{
300                        'name' : _("Purchase amount over the limit"),
301                        'act_from' : uid,
302                        'act_to' : manager_id,
303                        'body': _('Somebody has just confirmed a purchase with an amount over the defined limit'),
304                        'ref_partner_id': po.partner_id.id,
305                        'ref_doc1': 'purchase.order,%d' % (po.id,),
306                 })
307     def inv_line_create(self, cr, uid, a, ol):
308         return (0, False, {
309             'name': ol.name,
310             'account_id': a,
311             'price_unit': ol.price_unit or 0.0,
312             'quantity': ol.product_qty,
313             'product_id': ol.product_id.id or False,
314             'uos_id': ol.product_uom.id or False,
315             'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
316             'account_analytic_id': ol.account_analytic_id.id or False,
317         })
318
319     def action_cancel_draft(self, cr, uid, ids, *args):
320         if not len(ids):
321             return False
322         self.write(cr, uid, ids, {'state':'draft','shipped':0})
323         wf_service = netsvc.LocalService("workflow")
324         for p_id in ids:
325             # Deleting the existing instance of workflow for PO
326             wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
327             wf_service.trg_create(uid, 'purchase.order', p_id, cr)
328         for (id,name) in self.name_get(cr, uid, ids):
329             message = _("Purchase order '%s' has been set in draft state.") % name
330             self.log(cr, uid, id, message)
331         return True
332
333     def action_invoice_create(self, cr, uid, ids, *args):
334         res = False
335
336         journal_obj = self.pool.get('account.journal')
337         for o in self.browse(cr, uid, ids):
338             il = []
339             todo = []
340             for ol in o.order_line:
341                 todo.append(ol.id)
342                 if ol.product_id:
343                     a = ol.product_id.product_tmpl_id.property_account_expense.id
344                     if not a:
345                         a = ol.product_id.categ_id.property_account_expense_categ.id
346                     if not a:
347                         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,))
348                 else:
349                     a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category').id
350                 fpos = o.fiscal_position or False
351                 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
352                 il.append(self.inv_line_create(cr, uid, a, ol))
353
354             a = o.partner_id.property_account_payable.id
355             journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', o.company_id.id)], limit=1)
356             if not journal_ids:
357                 raise osv.except_osv(_('Error !'),
358                     _('There is no purchase journal defined for this company: "%s" (id:%d)') % (o.company_id.name, o.company_id.id))
359             inv = {
360                 'name': o.partner_ref or o.name,
361                 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
362                 'account_id': a,
363                 'type': 'in_invoice',
364                 'partner_id': o.partner_id.id,
365                 'currency_id': o.pricelist_id.currency_id.id,
366                 'address_invoice_id': o.partner_address_id.id,
367                 'address_contact_id': o.partner_address_id.id,
368                 'journal_id': len(journal_ids) and journal_ids[0] or False,
369                 'origin': o.name,
370                 'invoice_line': il,
371                 'fiscal_position': o.fiscal_position.id or o.partner_id.property_account_position.id,
372                 'payment_term': o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
373                 'company_id': o.company_id.id,
374             }
375             inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
376             self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
377             self.pool.get('purchase.order.line').write(cr, uid, todo, {'invoiced':True})
378             self.write(cr, uid, [o.id], {'invoice_id': inv_id})
379             res = inv_id
380         return res
381
382     def has_stockable_product(self,cr, uid, ids, *args):
383         for order in self.browse(cr, uid, ids):
384             for order_line in order.order_line:
385                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
386                     return True
387         return False
388
389     def action_cancel(self, cr, uid, ids, context={}):
390         for purchase in self.browse(cr, uid, ids):
391             for pick in purchase.picking_ids:
392                 if pick.state not in ('draft','cancel'):
393                     raise osv.except_osv(
394                         _('Could not cancel purchase order !'),
395                         _('You must first cancel all picking attached to this purchase order.'))
396             for pick in purchase.picking_ids:
397                 wf_service = netsvc.LocalService("workflow")
398                 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
399             inv = purchase.invoice_id
400             if inv and inv.state not in ('cancel','draft'):
401                 raise osv.except_osv(
402                     _('Could not cancel this purchase order !'),
403                     _('You must first cancel all invoices attached to this purchase order.'))
404             if inv:
405                 wf_service = netsvc.LocalService("workflow")
406                 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
407         self.write(cr,uid,ids,{'state':'cancel'})
408         for (id,name) in self.name_get(cr, uid, ids):
409             message = _("Purchase order '%s' is cancelled.") % name
410             self.log(cr, uid, id, message)
411         return True
412
413     def action_picking_create(self,cr, uid, ids, *args):
414         picking_id = False
415         for order in self.browse(cr, uid, ids):
416             loc_id = order.partner_id.property_stock_supplier.id
417             istate = 'none'
418             if order.invoice_method=='picking':
419                 istate = '2binvoiced'
420             pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in')
421             picking_id = self.pool.get('stock.picking').create(cr, uid, {
422                 'name': pick_name,
423                 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
424                 'type': 'in',
425                 'address_id': order.dest_address_id.id or order.partner_address_id.id,
426                 'invoice_state': istate,
427                 'purchase_id': order.id,
428                 'company_id': order.company_id.id,
429                 'move_lines' : [],
430             })
431             todo_moves = []
432             for order_line in order.order_line:
433                 if not order_line.product_id:
434                     continue
435                 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
436                     dest = order.location_id.id
437                     move = self.pool.get('stock.move').create(cr, uid, {
438                         'name': 'PO:'+order_line.name,
439                         'product_id': order_line.product_id.id,
440                         'product_qty': order_line.product_qty,
441                         'product_uos_qty': order_line.product_qty,
442                         'product_uom': order_line.product_uom.id,
443                         'product_uos': order_line.product_uom.id,
444                         'date': order_line.date_planned,
445                         'date_expected': order_line.date_planned,
446                         'location_id': loc_id,
447                         'location_dest_id': dest,
448                         'picking_id': picking_id,
449                         'move_dest_id': order_line.move_dest_id.id,
450                         'state': 'draft',
451                         'purchase_line_id': order_line.id,
452                         'company_id': order.company_id.id,
453                     })
454                     if order_line.move_dest_id:
455                         self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
456                     todo_moves.append(move)
457             self.pool.get('stock.move').action_confirm(cr, uid, todo_moves)
458             self.pool.get('stock.move').force_assign(cr, uid, todo_moves)
459             wf_service = netsvc.LocalService("workflow")
460             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
461         return picking_id
462
463     def copy(self, cr, uid, id, default=None,context={}):
464         if not default:
465             default = {}
466         default.update({
467             'state':'draft',
468             'shipped':False,
469             'invoiced':False,
470             'invoice_id':False,
471             'picking_ids':[],
472             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
473         })
474         return super(purchase_order, self).copy(cr, uid, id, default, context)
475
476
477     def do_merge(self, cr, uid, ids, context):
478         """
479         To merge similar type of purchase orders.
480         Orders will only be merged if:
481         * Purchase Orders are in draft
482         * Purchase Orders belong to the same partner
483         * Purchase Orders are have same stock location, same pricelist
484         Lines will only be merged if:
485         * Order lines are exactly the same except for the quantity and unit
486
487          @param self: The object pointer.
488          @param cr: A database cursor
489          @param uid: ID of the user currently logged in
490          @param ids: the ID or list of IDs
491          @param context: A standard dictionary
492
493          @return: new purchase order id
494
495         """
496         wf_service = netsvc.LocalService("workflow")
497         def make_key(br, fields):
498             list_key = []
499             for field in fields:
500                 field_val = getattr(br, field)
501                 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
502                     if not field_val:
503                         field_val = False
504                 if isinstance(field_val, browse_record):
505                     field_val = field_val.id
506                 elif isinstance(field_val, browse_null):
507                     field_val = False
508                 elif isinstance(field_val, list):
509                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
510                 list_key.append((field, field_val))
511             list_key.sort()
512             return tuple(list_key)
513
514     # compute what the new orders should contain
515
516         new_orders = {}
517
518         for porder in [order for order in self.browse(cr, uid, ids) if order.state == 'draft']:
519             order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
520             new_order = new_orders.setdefault(order_key, ({}, []))
521             new_order[1].append(porder.id)
522             order_infos = new_order[0]
523             if not order_infos:
524                 order_infos.update({
525                     'origin': porder.origin,
526                     'date_order': time.strftime('%Y-%m-%d'),
527                     'partner_id': porder.partner_id.id,
528                     'partner_address_id': porder.partner_address_id.id,
529                     'dest_address_id': porder.dest_address_id.id,
530                     'warehouse_id': porder.warehouse_id.id,
531                     'location_id': porder.location_id.id,
532                     'pricelist_id': porder.pricelist_id.id,
533                     'state': 'draft',
534                     'order_line': {},
535                     'notes': '%s' % (porder.notes or '',),
536                     'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
537                 })
538             else:
539                 if porder.notes:
540                     order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
541                 if porder.origin:
542                     order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
543
544             for order_line in porder.order_line:
545                 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
546                 o_line = order_infos['order_line'].setdefault(line_key, {})
547                 if o_line:
548                     # merge the line with an existing line
549                     o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
550                 else:
551                     # append a new "standalone" line
552                     for field in ('product_qty', 'product_uom'):
553                         field_val = getattr(order_line, field)
554                         if isinstance(field_val, browse_record):
555                             field_val = field_val.id
556                         o_line[field] = field_val
557                     o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
558
559
560
561         allorders = []
562         orders_info = {}
563         for order_key, (order_data, old_ids) in new_orders.iteritems():
564             # skip merges with only one order
565             if len(old_ids) < 2:
566                 allorders += (old_ids or [])
567                 continue
568
569             # cleanup order line data
570             for key, value in order_data['order_line'].iteritems():
571                 del value['uom_factor']
572                 value.update(dict(key))
573             order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
574
575             # create the new order
576             neworder_id = self.create(cr, uid, order_data)
577             orders_info.update({neworder_id: old_ids})
578             allorders.append(neworder_id)
579
580             # make triggers pointing to the old orders point to the new order
581             for old_id in old_ids:
582                 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
583                 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
584         return orders_info
585
586 purchase_order()
587
588 class purchase_order_line(osv.osv):
589     def _amount_line(self, cr, uid, ids, prop, arg,context):
590         res = {}
591         cur_obj=self.pool.get('res.currency')
592         tax_obj = self.pool.get('account.tax')
593         for line in self.browse(cr, uid, ids, context=context):
594             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
595             cur = line.order_id.pricelist_id.currency_id
596             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
597         return res
598
599     _columns = {
600         'name': fields.char('Description', size=256, required=True),
601         'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
602         'date_planned': fields.date('Scheduled Date', required=True),
603         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
604         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
605         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
606         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
607         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
608         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
609         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
610         'notes': fields.text('Notes'),
611         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
612         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
613         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company'),
614         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
615                                   help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
616                                        \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
617                                        \n* The \'Done\' state is set automatically when purchase order is set as done. \
618                                        \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
619         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
620         'invoiced': fields.boolean('Invoiced', readonly=True),
621         'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
622         'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
623
624     }
625     _defaults = {
626         'product_qty': lambda *a: 1.0,
627         'state': lambda *args: 'draft',
628         'invoiced': lambda *a: 0,
629     }
630     _table = 'purchase_order_line'
631     _name = 'purchase.order.line'
632     _description = 'Purchase Order Line'
633
634     def copy_data(self, cr, uid, id, default=None,context={}):
635         if not default:
636             default = {}
637         default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
638         return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
639
640     def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
641             partner_id, date_order=False, fiscal_position=False, date_planned=False,
642             name=False, price_unit=False, notes=False):
643         if not pricelist:
644             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.'))
645         if not  partner_id:
646             raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
647         if not product:
648             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
649                 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
650         prod= self.pool.get('product.product').browse(cr, uid, product)
651
652         product_uom_pool = self.pool.get('product.uom')
653         lang=False
654         if partner_id:
655             lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
656         context={'lang':lang}
657         context['partner_id'] = partner_id
658
659         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
660         prod_uom_po = prod.uom_po_id.id
661         if not uom:
662             uom = prod_uom_po
663         if not date_order:
664             date_order = time.strftime('%Y-%m-%d')
665         qty = qty or 1.0
666         seller_delay = 0
667
668         prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id], context=context)[0][1]
669  
670         for s in prod.seller_ids:
671             if s.name.id == partner_id:
672                 seller_delay = s.delay
673                 temp_qty = s.min_qty # supplier _qty assigned to temp
674                 if s.product_uom:
675                     qty = product_uom_pool._compute_qty(cr, uid, s.product_uom.id, s.min_qty, to_uom_id=prod_uom_po)
676                     uom = prod_uom_po
677                 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
678                     qty = temp_qty
679         if price_unit:
680             price = price_unit
681         else:
682             price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
683                     product, 1.0, partner_id, {
684                         'uom': prod_uom_po,
685                         'date': date_order,
686                         })[pricelist]
687         dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
688
689
690         res = {'value': {'price_unit': price, 'name': name or prod_name,
691             'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
692             'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
693             'product_qty': qty,
694             'product_uom': uom}}
695         domain = {}
696
697         taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
698         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
699         res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
700
701         res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
702         res3 = prod.uom_id.category_id.id
703         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
704         if res2[0]['category_id'][0] != res3:
705             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'))
706
707         res['domain'] = domain
708         return res
709
710     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
711             partner_id, date_order=False,fiscal_position=False):
712         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
713                 partner_id, date_order=date_order,fiscal_position=fiscal_position)
714         if 'product_uom' in res['value']:
715             del res['value']['product_uom']
716         if not uom:
717             res['value']['price_unit'] = 0.0
718         return res
719
720     def action_confirm(self, cr, uid, ids, context={}):
721         self.write(cr, uid, ids, {'state': 'confirmed'}, context)
722         return True
723
724 purchase_order_line()
725
726 class procurement_order(osv.osv):
727     _inherit = 'procurement.order'
728     _columns = {
729         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
730     }
731
732     def action_po_assign(self, cr, uid, ids, context={}):
733         """ This is action which call from workflow to assign purchase order to procurements
734         @return: True
735         """
736         res = self.make_po(cr, uid, ids, context=context)
737         res = res.values()
738         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
739
740     def make_po(self, cr, uid, ids, context={}):
741         """ Make purchase order from procurement
742         @return: New created Purchase Orders procurement wise
743         """
744         res = {}
745         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
746         partner_obj = self.pool.get('res.partner')
747         uom_obj = self.pool.get('product.uom')
748         pricelist_obj = self.pool.get('product.pricelist')
749         prod_obj = self.pool.get('product.product')
750         acc_pos_obj = self.pool.get('account.fiscal.position')
751         po_obj = self.pool.get('purchase.order')
752         for procurement in self.browse(cr, uid, ids):
753             res_id = procurement.move_id.id
754             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
755             seller_qty = procurement.product_id.seller_qty
756             seller_delay = int(procurement.product_id.seller_delay)
757             partner_id = partner.id
758             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
759             pricelist_id = partner.property_product_pricelist_purchase.id
760
761             uom_id = procurement.product_id.uom_po_id.id
762
763             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
764             if seller_qty:
765                 qty = max(qty,seller_qty)
766
767             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
768
769             newdate = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
770             newdate = (newdate - relativedelta(days=company.po_lead)) - relativedelta(days=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: