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