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