[MERGE] Better phrasing in Invoice Control selection field (on purchase orders).
[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('Order Date', 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', domain="[('partner_id', '!=', False)]",
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, string='Received', type='float'),
189         'invoiced': fields.function(_invoiced, string='Invoiced & Paid', type='boolean', help="It indicates that an invoice has been paid"),
190         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
191         'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated invoice'),('picking','Based on receptions')], 'Invoicing Control', required=True,
192             help="Based on Purchase Order lines: place individual lines in 'Invoice Control > Based on P.O. lines' frow where you can selectively create an invoice.\n" \
193                 "Based on generated invoice: create a draft invoice you can validate later.\n" \
194                 "Based on receptions: let you create an invoice when receptions are validated."
195         ),
196         'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, string='Expected Date', type='date', select=True, help="This is computed as the minimum scheduled date of all purchase order lines' products.",
197             store = {
198                 'purchase.order.line': (_get_order, ['date_planned'], 10),
199             }
200         ),
201         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Untaxed Amount',
202             store={
203                 'purchase.order.line': (_get_order, None, 10),
204             }, multi="sums", help="The amount without tax"),
205         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Taxes',
206             store={
207                 'purchase.order.line': (_get_order, None, 10),
208             }, multi="sums", help="The tax amount"),
209         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Purchase Price'), string='Total',
210             store={
211                 'purchase.order.line': (_get_order, None, 10),
212             }, multi="sums",help="The total amount"),
213         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
214         'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
215         'create_uid':  fields.many2one('res.users', 'Responsible'),
216         'company_id': fields.many2one('res.company','Company',required=True,select=1),
217     }
218     _defaults = {
219         'date_order': lambda *a: time.strftime('%Y-%m-%d'),
220         'state': 'draft',
221         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
222         'shipped': 0,
223         'invoice_method': 'order',
224         'invoiced': 0,
225         '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'],
226         '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,
227         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
228     }
229     _sql_constraints = [
230         ('name_uniq', 'unique(name)', 'Order Reference must be unique !'),
231     ]
232     _name = "purchase.order"
233     _description = "Purchase Order"
234     _order = "name desc"
235
236     def unlink(self, cr, uid, ids, context=None):
237         purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
238         unlink_ids = []
239         for s in purchase_orders:
240             if s['state'] in ['draft','cancel']:
241                 unlink_ids.append(s['id'])
242             else:
243                 raise osv.except_osv(_('Invalid action !'), _('In order to delete a purchase order, it must be cancelled first!'))
244
245         # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
246         # automatically sending subflow.delete upon deletion
247         wf_service = netsvc.LocalService("workflow")
248         for id in unlink_ids:
249             wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
250
251         return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
252
253     def button_dummy(self, cr, uid, ids, context=None):
254         return True
255
256     def onchange_dest_address_id(self, cr, uid, ids, adr_id):
257         if not adr_id:
258             return {}
259         values = {'warehouse_id': False}
260         part_id = self.pool.get('res.partner.address').browse(cr, uid, adr_id).partner_id
261         if part_id:
262             loc_id = part_id.property_stock_customer.id
263             values.update({'location_id': loc_id})
264         return {'value':values}
265
266     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
267         if not warehouse_id:
268             return {}
269         res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
270         return {'value':{'location_id': res, 'dest_address_id': False}}
271
272     def onchange_partner_id(self, cr, uid, ids, part):
273
274         if not part:
275             return {'value':{'partner_address_id': False, 'fiscal_position': False}}
276         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
277         part = self.pool.get('res.partner').browse(cr, uid, part)
278         pricelist = part.property_product_pricelist_purchase.id
279         fiscal_position = part.property_account_position and part.property_account_position.id or False
280         return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
281
282     def wkf_approve_order(self, cr, uid, ids, context=None):
283         self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
284         return True
285
286     #TODO: implement messages system
287     def wkf_confirm_order(self, cr, uid, ids, context=None):
288         todo = []
289         for po in self.browse(cr, uid, ids, context=context):
290             if not po.order_line:
291                 raise osv.except_osv(_('Error !'),_('You cannot confirm a purchase order without any lines.'))
292             for line in po.order_line:
293                 if line.state=='draft':
294                     todo.append(line.id)
295             message = _("Purchase order '%s' is confirmed.") % (po.name,)
296             self.log(cr, uid, po.id, message)
297 #        current_name = self.name_get(cr, uid, ids)[0][1]
298         self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
299         for id in ids:
300             self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
301         return True
302
303     def wkf_warn_buyer(self, cr, uid, ids):
304         self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
305         request = pooler.get_pool(cr.dbname).get('res.request')
306         for po in self.browse(cr, uid, ids):
307             managers = []
308             for oline in po.order_line:
309                 manager = oline.product_id.product_manager
310                 if manager and not (manager.id in managers):
311                     managers.append(manager.id)
312             for manager_id in managers:
313                 request.create(cr, uid,{
314                        'name' : _("Purchase amount over the limit"),
315                        'act_from' : uid,
316                        'act_to' : manager_id,
317                        'body': _('Somebody has just confirmed a purchase with an amount over the defined limit'),
318                        'ref_partner_id': po.partner_id.id,
319                        'ref_doc1': 'purchase.order,%d' % (po.id,),
320                 })
321     def inv_line_create(self, cr, uid, a, ol):
322         return (0, False, {
323             'name': ol.name,
324             'account_id': a,
325             'price_unit': ol.price_unit or 0.0,
326             'quantity': ol.product_qty,
327             'product_id': ol.product_id.id or False,
328             'uos_id': ol.product_uom.id or False,
329             'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
330             'account_analytic_id': ol.account_analytic_id.id or False,
331         })
332
333     def action_cancel_draft(self, cr, uid, ids, *args):
334         if not len(ids):
335             return False
336         self.write(cr, uid, ids, {'state':'draft','shipped':0})
337         wf_service = netsvc.LocalService("workflow")
338         for p_id in ids:
339             # Deleting the existing instance of workflow for PO
340             wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
341             wf_service.trg_create(uid, 'purchase.order', p_id, cr)
342         for (id,name) in self.name_get(cr, uid, ids):
343             message = _("Purchase order '%s' has been set in draft state.") % name
344             self.log(cr, uid, id, message)
345         return True
346
347     def action_invoice_create(self, cr, uid, ids, *args):
348         res = False
349
350         journal_obj = self.pool.get('account.journal')
351         for o in self.browse(cr, uid, ids):
352             il = []
353             todo = []
354             for ol in o.order_line:
355                 todo.append(ol.id)
356                 if ol.product_id:
357                     a = ol.product_id.product_tmpl_id.property_account_expense.id
358                     if not a:
359                         a = ol.product_id.categ_id.property_account_expense_categ.id
360                     if not a:
361                         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,))
362                 else:
363                     a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category').id
364                 fpos = o.fiscal_position or False
365                 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
366                 il.append(self.inv_line_create(cr, uid, a, ol))
367
368             a = o.partner_id.property_account_payable.id
369             journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', o.company_id.id)], limit=1)
370             if not journal_ids:
371                 raise osv.except_osv(_('Error !'),
372                     _('There is no purchase journal defined for this company: "%s" (id:%d)') % (o.company_id.name, o.company_id.id))
373             inv = {
374                 'name': o.partner_ref or o.name,
375                 'reference': o.partner_ref or o.name,
376                 'account_id': a,
377                 'type': 'in_invoice',
378                 'partner_id': o.partner_id.id,
379                 'currency_id': o.pricelist_id.currency_id.id,
380                 'address_invoice_id': o.partner_address_id.id,
381                 'address_contact_id': o.partner_address_id.id,
382                 'journal_id': len(journal_ids) and journal_ids[0] or False,
383                 'origin': o.name,
384                 'invoice_line': il,
385                 'fiscal_position': o.fiscal_position.id or o.partner_id.property_account_position.id,
386                 'payment_term': o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
387                 'company_id': o.company_id.id,
388             }
389             inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
390             self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
391             self.pool.get('purchase.order.line').write(cr, uid, todo, {'invoiced':True})
392             self.write(cr, uid, [o.id], {'invoice_ids': [(4, inv_id)]})
393             res = inv_id
394         return res
395
396     def has_stockable_product(self,cr, uid, ids, *args):
397         for order in self.browse(cr, uid, ids):
398             for order_line in order.order_line:
399                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
400                     return True
401         return False
402
403     def action_cancel(self, cr, uid, ids, context=None):
404         for purchase in self.browse(cr, uid, ids, context=context):
405             for pick in purchase.picking_ids:
406                 if pick.state not in ('draft','cancel'):
407                     raise osv.except_osv(
408                         _('Unable to cancel this purchase order!'),
409                         _('You must first cancel all receptions related to this purchase order.'))
410             for pick in purchase.picking_ids:
411                 wf_service = netsvc.LocalService("workflow")
412                 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
413             for inv in purchase.invoice_ids:
414                 if inv and inv.state not in ('cancel','draft'):
415                     raise osv.except_osv(
416                         _('Unable to cancel this purchase order!'),
417                         _('You must first cancel all invoices related to this purchase order.'))
418                 if inv:
419                     wf_service = netsvc.LocalService("workflow")
420                     wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
421         self.write(cr,uid,ids,{'state':'cancel'})
422         for (id,name) in self.name_get(cr, uid, ids):
423             message = _("Purchase order '%s' is cancelled.") % name
424             self.log(cr, uid, id, message)
425         return True
426
427     def action_picking_create(self,cr, uid, ids, *args):
428         picking_id = False
429         for order in self.browse(cr, uid, ids):
430             loc_id = order.partner_id.property_stock_supplier.id
431             istate = 'none'
432             if order.invoice_method=='picking':
433                 istate = '2binvoiced'
434             pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in')
435             picking_id = self.pool.get('stock.picking').create(cr, uid, {
436                 'name': pick_name,
437                 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
438                 'date': order.date_order,
439                 'type': 'in',
440                 'address_id': order.dest_address_id.id or order.partner_address_id.id,
441                 'invoice_state': istate,
442                 'purchase_id': order.id,
443                 'company_id': order.company_id.id,
444                 'move_lines' : [],
445             })
446             todo_moves = []
447             for order_line in order.order_line:
448                 if not order_line.product_id:
449                     continue
450                 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
451                     dest = order.location_id.id
452                     move = self.pool.get('stock.move').create(cr, uid, {
453                         'name': order.name + ': ' +(order_line.name or ''),
454                         'product_id': order_line.product_id.id,
455                         'product_qty': order_line.product_qty,
456                         'product_uos_qty': order_line.product_qty,
457                         'product_uom': order_line.product_uom.id,
458                         'product_uos': order_line.product_uom.id,
459                         'date': order_line.date_planned,
460                         'date_expected': order_line.date_planned,
461                         'location_id': loc_id,
462                         'location_dest_id': dest,
463                         'picking_id': picking_id,
464                         'address_id': order.dest_address_id.id or order.partner_address_id.id,
465                         'move_dest_id': order_line.move_dest_id.id,
466                         'state': 'draft',
467                         'purchase_line_id': order_line.id,
468                         'company_id': order.company_id.id,
469                         'price_unit': order_line.price_unit
470                     })
471                     if order_line.move_dest_id:
472                         self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
473                     todo_moves.append(move)
474             self.pool.get('stock.move').action_confirm(cr, uid, todo_moves)
475             self.pool.get('stock.move').force_assign(cr, uid, todo_moves)
476             wf_service = netsvc.LocalService("workflow")
477             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
478         return picking_id
479
480     def copy(self, cr, uid, id, default=None, context=None):
481         if not default:
482             default = {}
483         default.update({
484             'state':'draft',
485             'shipped':False,
486             'invoiced':False,
487             'invoice_ids': [],
488             'picking_ids': [],
489             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
490         })
491         return super(purchase_order, self).copy(cr, uid, id, default, context)
492
493
494     def do_merge(self, cr, uid, ids, context=None):
495         """
496         To merge similar type of purchase orders.
497         Orders will only be merged if:
498         * Purchase Orders are in draft
499         * Purchase Orders belong to the same partner
500         * Purchase Orders are have same stock location, same pricelist
501         Lines will only be merged if:
502         * Order lines are exactly the same except for the quantity and unit
503
504          @param self: The object pointer.
505          @param cr: A database cursor
506          @param uid: ID of the user currently logged in
507          @param ids: the ID or list of IDs
508          @param context: A standard dictionary
509
510          @return: new purchase order id
511
512         """
513         wf_service = netsvc.LocalService("workflow")
514         def make_key(br, fields):
515             list_key = []
516             for field in fields:
517                 field_val = getattr(br, field)
518                 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
519                     if not field_val:
520                         field_val = False
521                 if isinstance(field_val, browse_record):
522                     field_val = field_val.id
523                 elif isinstance(field_val, browse_null):
524                     field_val = False
525                 elif isinstance(field_val, list):
526                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
527                 list_key.append((field, field_val))
528             list_key.sort()
529             return tuple(list_key)
530
531     # compute what the new orders should contain
532
533         new_orders = {}
534
535         for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
536             order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
537             new_order = new_orders.setdefault(order_key, ({}, []))
538             new_order[1].append(porder.id)
539             order_infos = new_order[0]
540             if not order_infos:
541                 order_infos.update({
542                     'origin': porder.origin,
543                     'date_order': porder.date_order,
544                     'partner_id': porder.partner_id.id,
545                     'partner_address_id': porder.partner_address_id.id,
546                     'dest_address_id': porder.dest_address_id.id,
547                     'warehouse_id': porder.warehouse_id.id,
548                     'location_id': porder.location_id.id,
549                     'pricelist_id': porder.pricelist_id.id,
550                     'state': 'draft',
551                     'order_line': {},
552                     'notes': '%s' % (porder.notes or '',),
553                     'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
554                 })
555             else:
556                 if porder.date_order < order_infos['date_order']:
557                     order_infos['date_order'] = porder.date_order
558                 if porder.notes:
559                     order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
560                 if porder.origin:
561                     order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
562
563             for order_line in porder.order_line:
564                 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
565                 o_line = order_infos['order_line'].setdefault(line_key, {})
566                 if o_line:
567                     # merge the line with an existing line
568                     o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
569                 else:
570                     # append a new "standalone" line
571                     for field in ('product_qty', 'product_uom'):
572                         field_val = getattr(order_line, field)
573                         if isinstance(field_val, browse_record):
574                             field_val = field_val.id
575                         o_line[field] = field_val
576                     o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
577
578
579
580         allorders = []
581         orders_info = {}
582         for order_key, (order_data, old_ids) in new_orders.iteritems():
583             # skip merges with only one order
584             if len(old_ids) < 2:
585                 allorders += (old_ids or [])
586                 continue
587
588             # cleanup order line data
589             for key, value in order_data['order_line'].iteritems():
590                 del value['uom_factor']
591                 value.update(dict(key))
592             order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
593
594             # create the new order
595             neworder_id = self.create(cr, uid, order_data)
596             orders_info.update({neworder_id: old_ids})
597             allorders.append(neworder_id)
598
599             # make triggers pointing to the old orders point to the new order
600             for old_id in old_ids:
601                 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
602                 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
603         return orders_info
604
605 purchase_order()
606
607 class purchase_order_line(osv.osv):
608     def _amount_line(self, cr, uid, ids, prop, arg, context=None):
609         res = {}
610         cur_obj=self.pool.get('res.currency')
611         tax_obj = self.pool.get('account.tax')
612         for line in self.browse(cr, uid, ids, context=context):
613             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
614             cur = line.order_id.pricelist_id.currency_id
615             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
616         return res
617
618     def _get_uom_id(self, cr, uid, context=None):
619         try:
620             proxy = self.pool.get('ir.model.data')
621             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
622             return result[1]
623         except Exception, ex:
624             return False
625
626     _columns = {
627         'name': fields.char('Description', size=256, required=True),
628         'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
629         'date_planned': fields.date('Scheduled Date', required=True, select=True),
630         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
631         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
632         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
633         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
634         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
635         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
636         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
637         'notes': fields.text('Notes'),
638         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
639         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
640         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
641         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
642                                   help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
643                                        \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
644                                        \n* The \'Done\' state is set automatically when purchase order is set as done. \
645                                        \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
646         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
647         'invoiced': fields.boolean('Invoiced', readonly=True),
648         'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
649         'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
650
651     }
652     _defaults = {
653         'product_uom' : _get_uom_id,
654         'product_qty': lambda *a: 1.0,
655         'state': lambda *args: 'draft',
656         'invoiced': lambda *a: 0,
657     }
658     _table = 'purchase_order_line'
659     _name = 'purchase.order.line'
660     _description = 'Purchase Order Line'
661
662     def copy_data(self, cr, uid, id, default=None, context=None):
663         if not default:
664             default = {}
665         default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
666         return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
667
668     def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
669             partner_id, date_order=False, fiscal_position=False, date_planned=False,
670             name=False, price_unit=False, notes=False, context={}):
671         if not pricelist:
672             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.'))
673         if not  partner_id:
674             raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
675         if not product:
676             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
677                 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
678         res = {}
679         prod= self.pool.get('product.product').browse(cr, uid, product)
680         product_uom_pool = self.pool.get('product.uom')
681         lang=False
682         if partner_id:
683             lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
684         context={'lang':lang}
685         context['partner_id'] = partner_id
686
687         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
688         prod_uom_po = prod.uom_po_id.id
689         if not uom:
690             uom = prod_uom_po
691         if not date_order:
692             date_order = time.strftime('%Y-%m-%d')
693         qty = qty or 1.0
694         seller_delay = 0
695         if uom:
696             uom1_cat = prod.uom_id.category_id.id
697             uom2_cat = product_uom_pool.browse(cr, uid, uom).category_id.id
698             if uom1_cat != uom2_cat:
699                 uom = False
700
701         prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id], context=context)[0][1]
702         res = {}
703         for s in prod.seller_ids:
704             if s.name.id == partner_id:
705                 seller_delay = s.delay
706                 if s.product_uom:
707                     temp_qty = product_uom_pool._compute_qty(cr, uid, s.product_uom.id, s.min_qty, to_uom_id=prod.uom_id.id)
708                     uom = s.product_uom.id #prod_uom_po
709                 temp_qty = s.min_qty # supplier _qty assigned to temp
710                 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
711                     qty = temp_qty
712                     res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier has a minimal quantity set to %s, you should not purchase less.') % qty}})
713         qty_in_product_uom = product_uom_pool._compute_qty(cr, uid, uom, qty, to_uom_id=prod.uom_id.id)
714         price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
715                     product, qty_in_product_uom or 1.0, partner_id, {
716                         'uom': uom,
717                         'date': date_order,
718                         })[pricelist]
719         dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
720
721
722         res.update({'value': {'price_unit': price, 'name': prod_name,
723             'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
724             'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
725             'product_qty': qty,
726             'product_uom': prod.uom_id.id}})
727         domain = {}
728
729         taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
730         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
731         res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
732         res2 = self.pool.get('product.uom').read(cr, uid, [prod.uom_id.id], ['category_id'])
733         res3 = prod.uom_id.category_id.id
734         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
735         if res2[0]['category_id'][0] != res3:
736             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'))
737
738         res['domain'] = domain
739         return res
740
741     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
742             partner_id, date_order=False, fiscal_position=False, date_planned=False,
743             name=False, price_unit=False, notes=False, context={}):
744         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
745                 partner_id, date_order=date_order, fiscal_position=fiscal_position, date_planned=date_planned,
746             name=name, price_unit=price_unit, notes=notes, context=context)
747         if 'product_uom' in res['value']:
748             if uom and (uom != res['value']['product_uom']) and res['value']['product_uom']:
749                 seller_uom_name = self.pool.get('product.uom').read(cr, uid, [res['value']['product_uom']], ['name'])[0]['name']
750                 res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier only sells this product by %s') % seller_uom_name }})
751             del res['value']['product_uom']
752         if not uom:
753             res['value']['price_unit'] = 0.0
754         return res
755
756     def action_confirm(self, cr, uid, ids, context=None):
757         self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
758         return True
759
760 purchase_order_line()
761
762 class procurement_order(osv.osv):
763     _inherit = 'procurement.order'
764     _columns = {
765         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
766     }
767
768     def action_po_assign(self, cr, uid, ids, context=None):
769         """ This is action which call from workflow to assign purchase order to procurements
770         @return: True
771         """
772         res = self.make_po(cr, uid, ids, context=context)
773         res = res.values()
774         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
775
776     def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
777         """Create the purchase order from the procurement, using
778            the provided field values, after adding the given purchase
779            order line in the purchase order.
780
781            :params procurement: the procurement object generating the purchase order
782            :params dict po_vals: field values for the new purchase order (the
783                                  ``order_line`` field will be overwritten with one
784                                  single line, as passed in ``line_vals``).
785            :params dict line_vals: field values of the single purchase order line that
786                                    the purchase order will contain.
787            :return: id of the newly created purchase order
788            :rtype: int
789         """
790         po_vals.update({'order_line': [(0,0,line_vals)]})
791         return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
792
793     def make_po(self, cr, uid, ids, context=None):
794         """ Make purchase order from procurement
795         @return: New created Purchase Orders procurement wise
796         """
797         res = {}
798         if context is None:
799             context = {}
800         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
801         partner_obj = self.pool.get('res.partner')
802         uom_obj = self.pool.get('product.uom')
803         pricelist_obj = self.pool.get('product.pricelist')
804         prod_obj = self.pool.get('product.product')
805         acc_pos_obj = self.pool.get('account.fiscal.position')
806         seq_obj = self.pool.get('ir.sequence')
807         warehouse_obj = self.pool.get('stock.warehouse')
808         for procurement in self.browse(cr, uid, ids, context=context):
809             res_id = procurement.move_id.id
810             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
811             seller_qty = procurement.product_id.seller_qty
812             seller_delay = int(procurement.product_id.seller_delay)
813             partner_id = partner.id
814             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
815             pricelist_id = partner.property_product_pricelist_purchase.id
816             warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
817             uom_id = procurement.product_id.uom_po_id.id
818
819             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
820             if seller_qty:
821                 qty = max(qty,seller_qty)
822
823             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
824
825             order_date = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
826             schedule_date = (order_date - relativedelta(days=company.po_lead))
827             order_dates = schedule_date - relativedelta(days=seller_delay)
828
829             #Passing partner_id to context for purchase order line integrity of Line name
830             context.update({'lang': partner.lang, 'partner_id': partner_id})
831
832             product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
833             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
834             taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
835
836             line_vals = {
837                 'name': product.partner_ref,
838                 'product_qty': qty,
839                 'product_id': procurement.product_id.id,
840                 'product_uom': uom_id,
841                 'price_unit': price or 0.0,
842                 'date_planned': schedule_date.strftime('%Y-%m-%d %H:%M:%S'),
843                 'move_dest_id': res_id,
844                 'notes': product.description_purchase,
845                 'taxes_id': [(6,0,taxes)],
846             }
847             name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
848             po_vals = {
849                 'name': name,
850                 'origin': procurement.origin,
851                 'partner_id': partner_id,
852                 'partner_address_id': address_id,
853                 'location_id': procurement.location_id.id,
854                 'warehouse_id': warehouse_id and warehouse_id[0] or False,
855                 'pricelist_id': pricelist_id,
856                 'date_order': order_dates.strftime('%Y-%m-%d %H:%M:%S'),
857                 'company_id': procurement.company_id.id,
858                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
859             }
860             res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=context)
861             self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]})
862         return res
863
864 procurement_order()
865
866 class stock_invoice_onshipping(osv.osv_memory):
867     _inherit = "stock.invoice.onshipping"
868
869     def create_invoice(self, cr, uid, ids, context=None):
870         if context is None:
871             context = {}
872         res = super(stock_invoice_onshipping,self).create_invoice(cr, uid, ids, context=context)
873         purchase_obj = self.pool.get('purchase.order')
874         picking_obj = self.pool.get('stock.picking')
875         for pick_id in res:
876             pick = picking_obj.browse(cr, uid, pick_id, context=context)
877             if pick.purchase_id:
878                 purchase_obj.write(cr, uid, [pick.purchase_id.id], {
879                     'invoice_ids': [(4, res[pick_id])]}, context=context)
880         return res
881
882 stock_invoice_onshipping()
883
884 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: