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