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