[IMP] purchase: usability changes
[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         for order_key, (order_data, old_ids) in new_orders.iteritems():
563             # skip merges with only one order
564             if len(old_ids) < 2:
565                 allorders += (old_ids or [])
566                 continue
567
568             # cleanup order line data
569             for key, value in order_data['order_line'].iteritems():
570                 del value['uom_factor']
571                 value.update(dict(key))
572             order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
573
574             # create the new order
575             neworder_id = self.create(cr, uid, order_data)
576             allorders.append(neworder_id)
577
578             # make triggers pointing to the old orders point to the new order
579             for old_id in old_ids:
580                 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
581                 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
582         return allorders
583
584 purchase_order()
585
586 class purchase_order_line(osv.osv):
587     def _amount_line(self, cr, uid, ids, prop, arg,context):
588         res = {}
589         cur_obj=self.pool.get('res.currency')
590         tax_obj = self.pool.get('account.tax')
591         for line in self.browse(cr, uid, ids, context=context):
592             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
593             cur = line.order_id.pricelist_id.currency_id
594             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
595         return res
596
597     _columns = {
598         'name': fields.char('Description', size=256, required=True),
599         'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
600         'date_planned': fields.date('Scheduled Date', required=True),
601         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
602         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
603         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
604         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
605         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
606         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
607         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
608         'notes': fields.text('Notes'),
609         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
610         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
611         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company'),
612         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
613                                   help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
614                                        \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
615                                        \n* The \'Done\' state is set automatically when purchase order is set as done. \
616                                        \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
617         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
618         'invoiced': fields.boolean('Invoiced', readonly=True),
619         'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
620         'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
621
622     }
623     _defaults = {
624         'product_qty': lambda *a: 1.0,
625         'state': lambda *args: 'draft',
626         'invoiced': lambda *a: 0,
627     }
628     _table = 'purchase_order_line'
629     _name = 'purchase.order.line'
630     _description = 'Purchase Order Line'
631
632     def copy_data(self, cr, uid, id, default=None,context={}):
633         if not default:
634             default = {}
635         default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
636         return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
637
638     def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
639             partner_id, date_order=False, fiscal_position=False, date_planned=False,
640             name=False, price_unit=False, notes=False):
641         if not pricelist:
642             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.'))
643         if not  partner_id:
644             raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
645         if not product:
646             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
647                 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
648         prod= self.pool.get('product.product').browse(cr, uid, product)
649         lang=False
650         if partner_id:
651             lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
652         context={'lang':lang}
653         context['partner_id'] = partner_id
654
655         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
656         prod_uom_po = prod.uom_po_id.id
657         if not uom:
658             uom = prod_uom_po
659         if not date_order:
660             date_order = time.strftime('%Y-%m-%d')
661         qty = qty or 1.0
662         seller_delay = 0
663         for s in prod.seller_ids:
664             if s.name.id == partner_id:
665                 seller_delay = s.delay
666                 temp_qty = s.qty # supplier _qty assigned to temp
667                 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
668                     qty = temp_qty
669         if price_unit:
670             price = price_unit
671         else:
672             price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
673                     product, qty or 1.0, partner_id, {
674                         'uom': uom,
675                         'date': date_order,
676                         })[pricelist]
677         dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
678         prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id])[0][1]
679
680
681         res = {'value': {'price_unit': price, 'name': name or prod_name,
682             'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
683             'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
684             'product_qty': qty,
685             'product_uom': uom}}
686         domain = {}
687
688         taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
689         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
690         res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
691
692         res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
693         res3 = prod.uom_id.category_id.id
694         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
695         if res2[0]['category_id'][0] != res3:
696             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'))
697
698         res['domain'] = domain
699         return res
700
701     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
702             partner_id, date_order=False,fiscal_position=False):
703         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
704                 partner_id, date_order=date_order,fiscal_position=fiscal_position)
705         if 'product_uom' in res['value']:
706             del res['value']['product_uom']
707         if not uom:
708             res['value']['price_unit'] = 0.0
709         return res
710
711     def action_confirm(self, cr, uid, ids, context={}):
712         self.write(cr, uid, ids, {'state': 'confirmed'}, context)
713         return True
714
715 purchase_order_line()
716
717 class procurement_order(osv.osv):
718     _inherit = 'procurement.order'
719     _columns = {
720         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
721     }
722
723     def action_po_assign(self, cr, uid, ids, context={}):
724         """ This is action which call from workflow to assign purchase order to procurements
725         @return: True
726         """
727         res = self.make_po(cr, uid, ids, context=context)
728         res = res.values()
729         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
730
731     def make_po(self, cr, uid, ids, context={}):
732         """ Make purchase order from procurement
733         @return: New created Purchase Orders procurement wise
734         """
735         res = {}
736         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
737         partner_obj = self.pool.get('res.partner')
738         uom_obj = self.pool.get('product.uom')
739         pricelist_obj = self.pool.get('product.pricelist')
740         prod_obj = self.pool.get('product.product')
741         acc_pos_obj = self.pool.get('account.fiscal.position')
742         po_obj = self.pool.get('purchase.order')
743         for procurement in self.browse(cr, uid, ids):
744             res_id = procurement.move_id.id
745             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
746             seller_qty = procurement.product_id.seller_qty
747             seller_delay = int(procurement.product_id.seller_delay)
748             partner_id = partner.id
749             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
750             pricelist_id = partner.property_product_pricelist_purchase.id
751
752             uom_id = procurement.product_id.uom_po_id.id
753
754             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
755             if seller_qty:
756                 qty = max(qty,seller_qty)
757
758             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
759
760             newdate = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
761             newdate = (newdate - relativedelta(days=company.po_lead)) - relativedelta(days=seller_delay)
762
763             #Passing partner_id to context for purchase order line integrity of Line name
764             context.update({'lang': partner.lang, 'partner_id': partner_id})
765
766             product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
767
768             line = {
769                 'name': product.partner_ref,
770                 'product_qty': qty,
771                 'product_id': procurement.product_id.id,
772                 'product_uom': uom_id,
773                 'price_unit': price,
774                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
775                 'move_dest_id': res_id,
776                 'notes': product.description_purchase,
777             }
778
779             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
780             taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
781             line.update({
782                 'taxes_id': [(6,0,taxes)]
783             })
784             purchase_id = po_obj.create(cr, uid, {
785                 'origin': procurement.origin,
786                 'partner_id': partner_id,
787                 'partner_address_id': address_id,
788                 'location_id': procurement.location_id.id,
789                 'pricelist_id': pricelist_id,
790                 'order_line': [(0,0,line)],
791                 'company_id': procurement.company_id.id,
792                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
793             })
794             res[procurement.id] = purchase_id
795             self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': purchase_id})
796         return res
797
798 procurement_order()
799
800 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: