[FIX]Purchase:PO Line description doesnt take Supplier Product Name or Code in Produc...
[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         if prod.product_tmpl_id.seller_ids:
650             seller_get_id = prod.product_tmpl_id.seller_ids[0].name.id
651         else:
652             seller_get_id = False
653
654         lang=False
655         if partner_id:
656             lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
657         context={'lang':lang}
658         context['partner_id'] = partner_id
659
660         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
661         prod_uom_po = prod.uom_po_id.id
662         if not uom:
663             uom = prod_uom_po
664         if not date_order:
665             date_order = time.strftime('%Y-%m-%d')
666         qty = qty or 1.0
667         seller_delay = 0
668         for s in prod.seller_ids:
669             if s.name.id == partner_id:
670                 seller_delay = s.delay
671                 temp_qty = s.qty # supplier _qty assigned to temp
672                 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
673                     qty = temp_qty
674         if price_unit:
675             price = price_unit
676         else:
677             price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
678                     product, qty or 1.0, partner_id, {
679                         'uom': uom,
680                         'date': date_order,
681                         })[pricelist]
682         dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
683         if seller_get_id == partner_id:
684             prod_suppl_name = self.pool.get('product.product').browse(cr, uid, prod.id).seller_ids[0].product_name
685             prod_suppl_code = self.pool.get('product.product').browse(cr, uid, prod.id).seller_ids[0].product_code
686             if prod_suppl_name == False or prod_suppl_code == False:
687                prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id])[0][1]
688             else:
689                 prod_name= '[' + prod_suppl_code + '] '+ prod_suppl_name
690         else:
691             prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id])[0][1]
692
693         res = {'value': {'price_unit': price, 'name': name or prod_name,
694             'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
695             'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
696             'product_qty': qty,
697             'product_uom': uom}}
698         domain = {}
699
700         taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
701         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
702         res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
703
704         res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
705         res3 = prod.uom_id.category_id.id
706         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
707         if res2[0]['category_id'][0] != res3:
708             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'))
709
710         res['domain'] = domain
711         return res
712
713     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
714             partner_id, date_order=False,fiscal_position=False):
715         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
716                 partner_id, date_order=date_order,fiscal_position=fiscal_position)
717         if 'product_uom' in res['value']:
718             del res['value']['product_uom']
719         if not uom:
720             res['value']['price_unit'] = 0.0
721         return res
722
723     def action_confirm(self, cr, uid, ids, context={}):
724         self.write(cr, uid, ids, {'state': 'confirmed'}, context)
725         return True
726
727 purchase_order_line()
728
729 class procurement_order(osv.osv):
730     _inherit = 'procurement.order'
731     _columns = {
732         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
733     }
734
735     def action_po_assign(self, cr, uid, ids, context={}):
736         """ This is action which call from workflow to assign purchase order to procurements
737         @return: True
738         """
739         res = self.make_po(cr, uid, ids, context=context)
740         res = res.values()
741         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
742
743     def make_po(self, cr, uid, ids, context={}):
744         """ Make purchase order from procurement
745         @return: New created Purchase Orders procurement wise
746         """
747         res = {}
748         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
749         partner_obj = self.pool.get('res.partner')
750         uom_obj = self.pool.get('product.uom')
751         pricelist_obj = self.pool.get('product.pricelist')
752         prod_obj = self.pool.get('product.product')
753         acc_pos_obj = self.pool.get('account.fiscal.position')
754         po_obj = self.pool.get('purchase.order')
755         for procurement in self.browse(cr, uid, ids):
756             res_id = procurement.move_id.id
757             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
758             seller_qty = procurement.product_id.seller_qty
759             seller_delay = int(procurement.product_id.seller_delay)
760             partner_id = partner.id
761             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
762             pricelist_id = partner.property_product_pricelist_purchase.id
763
764             uom_id = procurement.product_id.uom_po_id.id
765
766             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
767             if seller_qty:
768                 qty = max(qty,seller_qty)
769
770             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
771
772             newdate = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
773             newdate = (newdate - relativedelta(days=company.po_lead)) - relativedelta(days=seller_delay)
774
775             #Passing partner_id to context for purchase order line integrity of Line name
776             context.update({'lang': partner.lang, 'partner_id': partner_id})
777
778             product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
779
780             line = {
781                 'name': product.partner_ref,
782                 'product_qty': qty,
783                 'product_id': procurement.product_id.id,
784                 'product_uom': uom_id,
785                 'price_unit': price,
786                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
787                 'move_dest_id': res_id,
788                 'notes': product.description_purchase,
789             }
790
791             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
792             taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
793             line.update({
794                 'taxes_id': [(6,0,taxes)]
795             })
796             purchase_id = po_obj.create(cr, uid, {
797                 'origin': procurement.origin,
798                 'partner_id': partner_id,
799                 'partner_address_id': address_id,
800                 'location_id': procurement.location_id.id,
801                 'pricelist_id': pricelist_id,
802                 'order_line': [(0,0,line)],
803                 'company_id': procurement.company_id.id,
804                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
805             })
806             res[procurement.id] = purchase_id
807             self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': purchase_id})
808         return res
809
810 procurement_order()
811
812 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: