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