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