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