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