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