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