[MERGE] merge with latest stable
[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         property_obj = self.pool.get('ir.property')
352         fp_obj =  self.pool.get('account.fiscal.position')
353         journal_obj = self.pool.get('account.journal')
354         for o in self.browse(cr, uid, ids):
355             il = []
356             todo = []
357             for ol in o.order_line:
358                 todo.append(ol.id)
359                 if ol.product_id:
360                     acc_id = ol.product_id.product_tmpl_id.property_account_expense.id
361                     if not acc_id:
362                         acc_id = ol.product_id.categ_id.property_account_expense_categ.id
363                     if not acc_id:
364                         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,))
365                 else:
366                     prop = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category')
367                     acc_id = prop and prop.id or False
368                 fpos = o.fiscal_position or False
369                 acc_id = fp_obj.map_account(cr, uid, fpos, acc_id)
370                 if not acc_id:
371                     raise osv.except_osv(_('Error !'),
372                         _('There is no expense account defined in default Properties for Product Category or Fiscal Position is not defined !'))
373                 il.append(self.inv_line_create(cr, uid, acc_id, ol))
374
375             acc_id = o.partner_id.property_account_payable.id
376             journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', o.company_id.id)], limit=1)
377             if not journal_ids:
378                 raise osv.except_osv(_('Error !'),
379                     _('There is no purchase journal defined for this company: "%s" (id:%d)') % (o.company_id.name, o.company_id.id))
380             inv = {
381                 'name': o.partner_ref or o.name,
382                 'reference': o.partner_ref or o.name,
383                 'account_id': acc_id,
384                 'type': 'in_invoice',
385                 'partner_id': o.partner_id.id,
386                 'currency_id': o.pricelist_id.currency_id.id,
387                 'address_invoice_id': o.partner_address_id.id,
388                 'address_contact_id': o.partner_address_id.id,
389                 'journal_id': len(journal_ids) and journal_ids[0] or False,
390                 'origin': o.name,
391                 'invoice_line': il,
392                 'fiscal_position': o.fiscal_position.id or o.partner_id.property_account_position.id,
393                 'payment_term': o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
394                 'company_id': o.company_id.id,
395             }
396             inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice', 'journal_type': 'purchase'})
397             self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
398             self.pool.get('purchase.order.line').write(cr, uid, todo, {'invoiced':True})
399             self.write(cr, uid, [o.id], {'invoice_ids': [(4, inv_id)]})
400             res = inv_id
401         return res
402
403     def has_stockable_product(self,cr, uid, ids, *args):
404         for order in self.browse(cr, uid, ids):
405             for order_line in order.order_line:
406                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
407                     return True
408         return False
409
410     def action_cancel(self, cr, uid, ids, context=None):
411         for purchase in self.browse(cr, uid, ids, context=context):
412             for pick in purchase.picking_ids:
413                 if pick.state not in ('draft','cancel'):
414                     raise osv.except_osv(
415                         _('Could not cancel purchase order !'),
416                         _('You must first cancel all picking attached to this purchase order.'))
417             for pick in purchase.picking_ids:
418                 wf_service = netsvc.LocalService("workflow")
419                 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
420             for inv in purchase.invoice_ids:
421                 if inv and inv.state not in ('cancel','draft'):
422                     raise osv.except_osv(
423                         _('Could not cancel this purchase order !'),
424                         _('You must first cancel all invoices attached to this purchase order.'))
425                 if inv:
426                     wf_service = netsvc.LocalService("workflow")
427                     wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
428         self.write(cr,uid,ids,{'state':'cancel'})
429         for (id,name) in self.name_get(cr, uid, ids):
430             message = _("Purchase order '%s' is cancelled.") % name
431             self.log(cr, uid, id, message)
432         return True
433
434     def action_picking_create(self,cr, uid, ids, *args):
435         picking_id = False
436         for order in self.browse(cr, uid, ids):
437             reception_address_id = False
438             if order.dest_address_id:
439                 reception_address_id = order.dest_address_id.id
440             elif order.warehouse_id and order.warehouse_id.partner_address_id:
441                 reception_address_id = order.warehouse_id.partner_address_id.id
442             else:
443                 if order.company_id.partner_id.address:
444                     addresses_default = [address.id for address in order.company_id.partner_id.address if address.type == 'default']
445                     addresses_delivery = [address.id for address in order.company_id.partner_id.address if address.type == 'delivery']
446                     reception_address_id = (addresses_delivery and addresses_delivery[0]) or (addresses_default and addresses_default[0]) or False
447             loc_id = order.partner_id.property_stock_supplier.id
448             istate = 'none'
449             if order.invoice_method=='picking':
450                 istate = '2binvoiced'
451             pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in')
452             picking_id = self.pool.get('stock.picking').create(cr, uid, {
453                 'name': pick_name,
454                 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
455                 'type': 'in',
456                 'address_id': reception_address_id,
457                 'invoice_state': istate,
458                 'purchase_id': order.id,
459                 'company_id': order.company_id.id,
460                 'move_lines' : [],
461             })
462             todo_moves = []
463             for order_line in order.order_line:
464                 if not order_line.product_id:
465                     continue
466                 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
467                     dest = order.location_id.id
468                     move = self.pool.get('stock.move').create(cr, uid, {
469                         'name': order.name + ': ' +(order_line.name or ''),
470                         'product_id': order_line.product_id.id,
471                         'product_qty': order_line.product_qty,
472                         'product_uos_qty': order_line.product_qty,
473                         'product_uom': order_line.product_uom.id,
474                         'product_uos': order_line.product_uom.id,
475                         'date': order_line.date_planned,
476                         'date_expected': order_line.date_planned,
477                         'location_id': loc_id,
478                         'location_dest_id': dest,
479                         'picking_id': picking_id,
480                         'move_dest_id': order_line.move_dest_id.id,
481                         'state': 'draft',
482                         'purchase_line_id': order_line.id,
483                         'company_id': order.company_id.id,
484                         'price_unit': order_line.price_unit
485                     })
486                     if order_line.move_dest_id:
487                         self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
488                     todo_moves.append(move)
489             self.pool.get('stock.move').action_confirm(cr, uid, todo_moves)
490             self.pool.get('stock.move').force_assign(cr, uid, todo_moves)
491             wf_service = netsvc.LocalService("workflow")
492             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
493         return picking_id
494
495     def copy(self, cr, uid, id, default=None, context=None):
496         if not default:
497             default = {}
498         default.update({
499             'state':'draft',
500             'shipped':False,
501             'invoiced':False,
502             'invoice_ids': [],
503             'picking_ids': [],
504             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
505         })
506         return super(purchase_order, self).copy(cr, uid, id, default, context)
507
508
509     def do_merge(self, cr, uid, ids, context=None):
510         """
511         To merge similar type of purchase orders.
512         Orders will only be merged if:
513         * Purchase Orders are in draft
514         * Purchase Orders belong to the same partner
515         * Purchase Orders are have same stock location, same pricelist
516         Lines will only be merged if:
517         * Order lines are exactly the same except for the quantity and unit
518
519          @param self: The object pointer.
520          @param cr: A database cursor
521          @param uid: ID of the user currently logged in
522          @param ids: the ID or list of IDs
523          @param context: A standard dictionary
524
525          @return: new purchase order id
526
527         """
528         wf_service = netsvc.LocalService("workflow")
529         def make_key(br, fields):
530             list_key = []
531             for field in fields:
532                 field_val = getattr(br, field)
533                 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
534                     if not field_val:
535                         field_val = False
536                 if isinstance(field_val, browse_record):
537                     field_val = field_val.id
538                 elif isinstance(field_val, browse_null):
539                     field_val = False
540                 elif isinstance(field_val, list):
541                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
542                 list_key.append((field, field_val))
543             list_key.sort()
544             return tuple(list_key)
545
546     # compute what the new orders should contain
547
548         new_orders = {}
549
550         for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
551             order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
552             new_order = new_orders.setdefault(order_key, ({}, []))
553             new_order[1].append(porder.id)
554             order_infos = new_order[0]
555             if not order_infos:
556                 order_infos.update({
557                     'origin': porder.origin,
558                     'date_order': time.strftime('%Y-%m-%d'),
559                     'partner_id': porder.partner_id.id,
560                     'partner_address_id': porder.partner_address_id.id,
561                     'dest_address_id': porder.dest_address_id.id,
562                     'warehouse_id': porder.warehouse_id.id,
563                     'location_id': porder.location_id.id,
564                     'pricelist_id': porder.pricelist_id.id,
565                     'state': 'draft',
566                     'order_line': {},
567                     'notes': '%s' % (porder.notes or '',),
568                     'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
569                 })
570             else:
571                 if porder.notes:
572                     order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
573                 if porder.origin:
574                     order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
575
576             for order_line in porder.order_line:
577                 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
578                 o_line = order_infos['order_line'].setdefault(line_key, {})
579                 if o_line:
580                     # merge the line with an existing line
581                     o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
582                 else:
583                     # append a new "standalone" line
584                     for field in ('product_qty', 'product_uom'):
585                         field_val = getattr(order_line, field)
586                         if isinstance(field_val, browse_record):
587                             field_val = field_val.id
588                         o_line[field] = field_val
589                     o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
590
591
592
593         allorders = []
594         orders_info = {}
595         for order_key, (order_data, old_ids) in new_orders.iteritems():
596             # skip merges with only one order
597             if len(old_ids) < 2:
598                 allorders += (old_ids or [])
599                 continue
600
601             # cleanup order line data
602             for key, value in order_data['order_line'].iteritems():
603                 del value['uom_factor']
604                 value.update(dict(key))
605             order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
606
607             # create the new order
608             neworder_id = self.create(cr, uid, order_data)
609             orders_info.update({neworder_id: old_ids})
610             allorders.append(neworder_id)
611
612             # make triggers pointing to the old orders point to the new order
613             for old_id in old_ids:
614                 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
615                 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
616         return orders_info
617
618 purchase_order()
619
620 class purchase_order_line(osv.osv):
621     def _amount_line(self, cr, uid, ids, prop, arg, context=None):
622         res = {}
623         cur_obj=self.pool.get('res.currency')
624         tax_obj = self.pool.get('account.tax')
625         for line in self.browse(cr, uid, ids, context=context):
626             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
627             cur = line.order_id.pricelist_id.currency_id
628             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
629         return res
630
631     _columns = {
632         'name': fields.char('Description', size=256, required=True),
633         'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
634         'date_planned': fields.date('Scheduled Date', required=True, select=True),
635         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
636         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
637         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
638         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
639         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
640         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
641         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
642         'notes': fields.text('Notes'),
643         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
644         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
645         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
646         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
647                                   help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
648                                        \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
649                                        \n* The \'Done\' state is set automatically when purchase order is set as done. \
650                                        \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
651         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
652         'invoiced': fields.boolean('Invoiced', readonly=True),
653         'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
654         'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
655
656     }
657     _defaults = {
658         'product_qty': lambda *a: 1.0,
659         'state': lambda *args: 'draft',
660         'invoiced': lambda *a: 0,
661     }
662     _table = 'purchase_order_line'
663     _name = 'purchase.order.line'
664     _description = 'Purchase Order Line'
665
666     def copy_data(self, cr, uid, id, default=None, context=None):
667         if not default:
668             default = {}
669         default.update({'state':'draft', 'move_ids':[], 'move_dest_id':False, 'invoiced':0,'invoice_lines':[]})
670         return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
671
672     def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
673             partner_id, date_order=False, fiscal_position=False, date_planned=False,
674             name=False, price_unit=False, notes=False):
675         if not pricelist:
676             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.'))
677         if not  partner_id:
678             raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
679         if not product:
680             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
681                 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
682         res = {}
683         prod= self.pool.get('product.product').browse(cr, uid, product)
684
685         product_uom_pool = self.pool.get('product.uom')
686         lang=False
687         if partner_id:
688             lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
689         context = self.pool.get('res.users').context_get(cr, uid)
690         context_partner = {'lang':lang, 'partner_id': partner_id}
691         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
692         prod_uom_po = prod.uom_po_id.id
693         if not uom:
694             uom = prod_uom_po
695         if not date_order:
696             date_order = time.strftime('%Y-%m-%d')
697         qty = qty or 1.0
698         seller_delay = 0
699
700         prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id], context=context_partner)[0][1]
701         res = {}
702         for s in prod.seller_ids:
703             if s.name.id == partner_id:
704                 seller_delay = s.delay
705                 if s.product_uom:
706                     temp_qty = product_uom_pool._compute_qty(cr, uid, s.product_uom.id, s.min_qty, to_uom_id=prod.uom_id.id)
707                     uom = s.product_uom.id #prod_uom_po
708                 temp_qty = s.min_qty # supplier _qty assigned to temp
709                 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
710                     qty = temp_qty
711                     res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier has a minimal quantity set to %s, you cannot purchase less.') % qty}})
712         qty_in_product_uom = product_uom_pool._compute_qty(cr, uid, uom, qty, to_uom_id=prod.uom_id.id)
713         price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
714                     product, qty_in_product_uom or 1.0, partner_id, {
715                         'uom': uom,
716                         'date': date_order,
717                         })[pricelist]
718         if price is False:
719             warning = {
720                 'title': 'No valid pricelist line found !',
721                 'message':
722                     "Couldn't find a pricelist line matching this product and quantity.\n"
723                     "You have to change either the product, the quantity or the pricelist."
724                 }
725             res.update({'warning': warning})
726         dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
727
728
729         res.update({'value': {'price_unit': price, 'name': prod_name,
730             'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
731             'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
732             'product_qty': qty,
733             'product_uom': uom}})
734         domain = {}
735
736         taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
737         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
738         res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
739
740         res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
741         res3 = prod.uom_id.category_id.id
742         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
743         if res2[0]['category_id'][0] != res3:
744             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'))
745
746         res['domain'] = domain
747         return res
748
749     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
750             partner_id, date_order=False, fiscal_position=False, date_planned=False,
751             name=False, price_unit=False, notes=False):
752         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
753                 partner_id, date_order=date_order, fiscal_position=fiscal_position, date_planned=date_planned,
754             name=name, price_unit=price_unit, notes=notes)
755         if 'product_uom' in res['value']:
756             if uom and (uom != res['value']['product_uom']) and res['value']['product_uom']:
757                 seller_uom_name = self.pool.get('product.uom').read(cr, uid, [res['value']['product_uom']], ['name'])[0]['name']
758                 res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier only sells this product by %s') % seller_uom_name }})
759             del res['value']['product_uom']
760         if not uom:
761             res['value']['price_unit'] = 0.0
762         return res
763
764     def action_confirm(self, cr, uid, ids, context=None):
765         self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
766         return True
767
768 purchase_order_line()
769
770 class procurement_order(osv.osv):
771     _inherit = 'procurement.order'
772     _columns = {
773         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
774     }
775
776     def action_po_assign(self, cr, uid, ids, context=None):
777         """ This is action which call from workflow to assign purchase order to procurements
778         @return: True
779         """
780         res = self.make_po(cr, uid, ids, context=context)
781         res = res.values()
782         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
783
784     def make_po(self, cr, uid, ids, context=None):
785         """ Make purchase order from procurement
786         @return: New created Purchase Orders procurement wise
787         """
788         res = {}
789         if context is None:
790             context = {}
791         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
792         partner_obj = self.pool.get('res.partner')
793         uom_obj = self.pool.get('product.uom')
794         pricelist_obj = self.pool.get('product.pricelist')
795         prod_obj = self.pool.get('product.product')
796         acc_pos_obj = self.pool.get('account.fiscal.position')
797         po_obj = self.pool.get('purchase.order')
798         po_line_obj = self.pool.get('purchase.order.line')
799         for procurement in self.browse(cr, uid, ids, context=context):
800             res_id = procurement.move_id.id
801             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
802             seller_qty = procurement.product_id.seller_qty
803             seller_delay = int(procurement.product_id.seller_delay)
804             partner_id = partner.id
805             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
806             pricelist_id = partner.property_product_pricelist_purchase.id
807             fiscal_position = partner.property_account_position and partner.property_account_position.id or False
808
809             uom_id = procurement.product_id.uom_po_id.id
810
811             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
812             if seller_qty:
813                 qty = max(qty,seller_qty)
814
815             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
816
817             newdate = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
818             newdate = (newdate - relativedelta(days=company.po_lead)) - relativedelta(days=seller_delay)
819
820             res_onchange = po_line_obj.product_id_change(cr, uid, ids, pricelist_id, procurement.product_id.id, qty, uom_id,
821                 partner_id, time.strftime('%Y-%m-%d'), fiscal_position=fiscal_position, date_planned=datetime.now() + relativedelta(days=seller_delay or 0.0),
822             name=procurement.name, price_unit=procurement.product_id.list_price, notes=procurement.product_id.description_purchase)
823
824             #Passing partner_id to context for purchase order line integrity of Line name
825             context.update({'lang': partner.lang, 'partner_id': partner_id})
826
827             product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
828
829             line = {
830                 'name': product.partner_ref,
831                 'product_qty': res_onchange['value']['product_qty'],
832                 'product_id': procurement.product_id.id,
833                 'product_uom': res_onchange['value']['product_uom'],
834                 'price_unit': res_onchange['value']['price_unit'],
835                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
836                 'move_dest_id': res_id,
837                 'notes': product.description_purchase,
838             }
839
840             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
841             taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
842             line.update({
843                 'taxes_id': [(6,0,taxes)]
844             })
845             purchase_id = po_obj.create(cr, uid, {
846                 'origin': procurement.origin,
847                 'partner_id': partner_id,
848                 'partner_address_id': address_id,
849                 'location_id': procurement.location_id.id,
850                 'pricelist_id': pricelist_id,
851                 'order_line': [(0,0,line)],
852                 'company_id': procurement.company_id.id,
853                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
854             })
855             res[procurement.id] = purchase_id
856             self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': purchase_id})
857         return res
858
859 procurement_order()
860
861 class stock_invoice_onshipping(osv.osv_memory):
862     _inherit = "stock.invoice.onshipping"
863
864     def create_invoice(self, cr, uid, ids, context=None):
865         if context is None:
866             context = {}
867         res = super(stock_invoice_onshipping,self).create_invoice(cr, uid, ids, context=context)
868         purchase_obj = self.pool.get('purchase.order')
869         picking_obj = self.pool.get('stock.picking')
870         for pick_id in res:
871             pick = picking_obj.browse(cr, uid, pick_id, context=context)
872             if pick.purchase_id:
873                 purchase_obj.write(cr, uid, [pick.purchase_id.id], {
874                     'invoice_ids': [(4, res[pick_id])]}, context=context)
875         return res
876
877 stock_invoice_onshipping()
878
879 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: