[IMP] purchase: cleanup/improve refactored code, including:
[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 _prepare_order_picking(self, cr, uid, order, *args):
428         return {
429             'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
430             'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
431             'date': order.date_order,
432             'type': 'in',
433             'address_id': order.dest_address_id.id or order.partner_address_id.id,
434             'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
435             'purchase_id': order.id,
436             'company_id': order.company_id.id,
437             'move_lines' : [],
438         }
439          
440     def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, *args):
441         return {
442             'name': order.name + ': ' + (order_line.name or ''),
443             'product_id': order_line.product_id.id,
444             'product_qty': order_line.product_qty,
445             'product_uos_qty': order_line.product_qty,
446             'product_uom': order_line.product_uom.id,
447             'product_uos': order_line.product_uom.id,
448             'date': order_line.date_planned,
449             'date_expected': order_line.date_planned,
450             'location_id': order.partner_id.property_stock_supplier.id,
451             'location_dest_id': order.location_id.id,
452             'picking_id': picking_id,
453             'address_id': order.dest_address_id.id or order.partner_address_id.id,
454             'move_dest_id': order_line.move_dest_id.id,
455             'state': 'draft',
456             'purchase_line_id': order_line.id,
457             'company_id': order.company_id.id,
458             'price_unit': order_line.price_unit
459         }
460
461     def _create_pickings(self, cr, uid, order, order_lines, picking_id=False, *args):
462         """Creates pickings and appropriate stock moves for given order lines, then
463         confirms the moves, makes them available, and confirms the picking.
464
465         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
466         a standard outgoing picking will be created to wrap the stock moves, as returned
467         by :meth:`~._prepare_order_picking`.
468
469         Modules that wish to customize the procurements or partition the stock moves over
470         multiple stock pickings may override this method and call ``super()`` with
471         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
472
473         :param browse_record order: purchase order to which the order lines belong
474         :param list(browse_record) order_lines: purchase order line records for which picking
475                                                 and moves should be created.
476         :param int picking_id: optional ID of a stock picking to which the created stock moves
477                                will be added. A new picking will be created if omitted.
478         :return: list of IDs of pickings used/created for the given order lines (usually just one)
479         """
480         if not picking_id: 
481             picking_id = self.pool.get('stock.picking').create(cr, uid, self._prepare_order_picking(cr, uid, order, *args))
482         todo_moves = []
483         stock_move = self.pool.get('stock.move')
484         wf_service = netsvc.LocalService("workflow")
485         for order_line in order_lines:
486             if not order_line.product_id:
487                 continue
488             if order_line.product_id.type in ('product', 'consu'):
489                 move = stock_move.create(cr, uid, self._prepare_order_line_move(cr, uid, order, order_line, picking_id, *args))
490                 if order_line.move_dest_id:
491                     order_line.move_dest_id.write({'location_id': order.location_id.id})
492                 todo_moves.append(move)
493         stock_move.action_confirm(cr, uid, todo_moves)
494         stock_move.force_assign(cr, uid, todo_moves)
495         wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
496         return [picking_id]
497
498     def action_picking_create(self,cr, uid, ids, *args):
499         picking_ids = []
500         for order in self.browse(cr, uid, ids):
501             picking_ids.extend(self._create_pickings(cr, uid, order, order.order_line, None, *args))
502         return picking_ids
503
504     def copy(self, cr, uid, id, default=None, context=None):
505         if not default:
506             default = {}
507         default.update({
508             'state':'draft',
509             'shipped':False,
510             'invoiced':False,
511             'invoice_ids': [],
512             'picking_ids': [],
513             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
514         })
515         return super(purchase_order, self).copy(cr, uid, id, default, context)
516
517     def do_merge(self, cr, uid, ids, context=None):
518         """
519         To merge similar type of purchase orders.
520         Orders will only be merged if:
521         * Purchase Orders are in draft
522         * Purchase Orders belong to the same partner
523         * Purchase Orders are have same stock location, same pricelist
524         Lines will only be merged if:
525         * Order lines are exactly the same except for the quantity and unit
526
527          @param self: The object pointer.
528          @param cr: A database cursor
529          @param uid: ID of the user currently logged in
530          @param ids: the ID or list of IDs
531          @param context: A standard dictionary
532
533          @return: new purchase order id
534
535         """
536         wf_service = netsvc.LocalService("workflow")
537         def make_key(br, fields):
538             list_key = []
539             for field in fields:
540                 field_val = getattr(br, field)
541                 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
542                     if not field_val:
543                         field_val = False
544                 if isinstance(field_val, browse_record):
545                     field_val = field_val.id
546                 elif isinstance(field_val, browse_null):
547                     field_val = False
548                 elif isinstance(field_val, list):
549                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
550                 list_key.append((field, field_val))
551             list_key.sort()
552             return tuple(list_key)
553
554     # compute what the new orders should contain
555
556         new_orders = {}
557
558         for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
559             order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
560             new_order = new_orders.setdefault(order_key, ({}, []))
561             new_order[1].append(porder.id)
562             order_infos = new_order[0]
563             if not order_infos:
564                 order_infos.update({
565                     'origin': porder.origin,
566                     'date_order': porder.date_order,
567                     'partner_id': porder.partner_id.id,
568                     'partner_address_id': porder.partner_address_id.id,
569                     'dest_address_id': porder.dest_address_id.id,
570                     'warehouse_id': porder.warehouse_id.id,
571                     'location_id': porder.location_id.id,
572                     'pricelist_id': porder.pricelist_id.id,
573                     'state': 'draft',
574                     'order_line': {},
575                     'notes': '%s' % (porder.notes or '',),
576                     'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
577                 })
578             else:
579                 if porder.date_order < order_infos['date_order']:
580                     order_infos['date_order'] = porder.date_order
581                 if porder.notes:
582                     order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
583                 if porder.origin:
584                     order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
585
586             for order_line in porder.order_line:
587                 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
588                 o_line = order_infos['order_line'].setdefault(line_key, {})
589                 if o_line:
590                     # merge the line with an existing line
591                     o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
592                 else:
593                     # append a new "standalone" line
594                     for field in ('product_qty', 'product_uom'):
595                         field_val = getattr(order_line, field)
596                         if isinstance(field_val, browse_record):
597                             field_val = field_val.id
598                         o_line[field] = field_val
599                     o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
600
601
602
603         allorders = []
604         orders_info = {}
605         for order_key, (order_data, old_ids) in new_orders.iteritems():
606             # skip merges with only one order
607             if len(old_ids) < 2:
608                 allorders += (old_ids or [])
609                 continue
610
611             # cleanup order line data
612             for key, value in order_data['order_line'].iteritems():
613                 del value['uom_factor']
614                 value.update(dict(key))
615             order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
616
617             # create the new order
618             neworder_id = self.create(cr, uid, order_data)
619             orders_info.update({neworder_id: old_ids})
620             allorders.append(neworder_id)
621
622             # make triggers pointing to the old orders point to the new order
623             for old_id in old_ids:
624                 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
625                 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
626         return orders_info
627
628 purchase_order()
629
630 class purchase_order_line(osv.osv):
631     def _amount_line(self, cr, uid, ids, prop, arg, context=None):
632         res = {}
633         cur_obj=self.pool.get('res.currency')
634         tax_obj = self.pool.get('account.tax')
635         for line in self.browse(cr, uid, ids, context=context):
636             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
637             cur = line.order_id.pricelist_id.currency_id
638             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
639         return res
640
641     def _get_uom_id(self, cr, uid, context=None):
642         try:
643             proxy = self.pool.get('ir.model.data')
644             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
645             return result[1]
646         except Exception, ex:
647             return False
648
649     _columns = {
650         'name': fields.char('Description', size=256, required=True),
651         'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
652         'date_planned': fields.date('Scheduled Date', required=True, select=True),
653         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
654         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
655         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
656         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
657         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
658         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
659         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
660         'notes': fields.text('Notes'),
661         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
662         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
663         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
664         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
665                                   help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
666                                        \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
667                                        \n* The \'Done\' state is set automatically when purchase order is set as done. \
668                                        \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
669         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
670         'invoiced': fields.boolean('Invoiced', readonly=True),
671         'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
672         'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
673
674     }
675     _defaults = {
676         'product_uom' : _get_uom_id,
677         'product_qty': lambda *a: 1.0,
678         'state': lambda *args: 'draft',
679         'invoiced': lambda *a: 0,
680     }
681     _table = 'purchase_order_line'
682     _name = 'purchase.order.line'
683     _description = 'Purchase Order Line'
684
685     def copy_data(self, cr, uid, id, default=None, context=None):
686         if not default:
687             default = {}
688         default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
689         return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
690
691     def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
692             partner_id, date_order=False, fiscal_position=False, date_planned=False,
693             name=False, price_unit=False, notes=False, context={}):
694         if not pricelist:
695             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.'))
696         if not  partner_id:
697             raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
698         if not product:
699             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
700                 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
701         res = {}
702         prod= self.pool.get('product.product').browse(cr, uid, product)
703         product_uom_pool = self.pool.get('product.uom')
704         lang=False
705         if partner_id:
706             lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
707         context={'lang':lang}
708         context['partner_id'] = partner_id
709
710         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
711         prod_uom_po = prod.uom_po_id.id
712         if not uom:
713             uom = prod_uom_po
714         if not date_order:
715             date_order = time.strftime('%Y-%m-%d')
716         qty = qty or 1.0
717         seller_delay = 0
718         if uom:
719             uom1_cat = prod.uom_id.category_id.id
720             uom2_cat = product_uom_pool.browse(cr, uid, uom).category_id.id
721             if uom1_cat != uom2_cat:
722                 uom = False
723
724         prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id], context=context)[0][1]
725         res = {}
726         for s in prod.seller_ids:
727             if s.name.id == partner_id:
728                 seller_delay = s.delay
729                 if s.product_uom:
730                     temp_qty = product_uom_pool._compute_qty(cr, uid, s.product_uom.id, s.min_qty, to_uom_id=prod.uom_id.id)
731                     uom = s.product_uom.id #prod_uom_po
732                 temp_qty = s.min_qty # supplier _qty assigned to temp
733                 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
734                     qty = temp_qty
735                     res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier has a minimal quantity set to %s, you should not purchase less.') % qty}})
736         qty_in_product_uom = product_uom_pool._compute_qty(cr, uid, uom, qty, to_uom_id=prod.uom_id.id)
737         price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
738                     product, qty_in_product_uom or 1.0, partner_id, {
739                         'uom': uom,
740                         'date': date_order,
741                         })[pricelist]
742         dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
743
744
745         res.update({'value': {'price_unit': price, 'name': prod_name,
746             'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
747             'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
748             'product_qty': qty,
749             'product_uom': prod.uom_id.id}})
750         domain = {}
751
752         taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
753         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
754         res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
755         res2 = self.pool.get('product.uom').read(cr, uid, [prod.uom_id.id], ['category_id'])
756         res3 = prod.uom_id.category_id.id
757         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
758         if res2[0]['category_id'][0] != res3:
759             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'))
760
761         res['domain'] = domain
762         return res
763
764     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
765             partner_id, date_order=False, fiscal_position=False, date_planned=False,
766             name=False, price_unit=False, notes=False, context={}):
767         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
768                 partner_id, date_order=date_order, fiscal_position=fiscal_position, date_planned=date_planned,
769             name=name, price_unit=price_unit, notes=notes, context=context)
770         if 'product_uom' in res['value']:
771             if uom and (uom != res['value']['product_uom']) and res['value']['product_uom']:
772                 seller_uom_name = self.pool.get('product.uom').read(cr, uid, [res['value']['product_uom']], ['name'])[0]['name']
773                 res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier only sells this product by %s') % seller_uom_name }})
774             del res['value']['product_uom']
775         if not uom:
776             res['value']['price_unit'] = 0.0
777         return res
778
779     def action_confirm(self, cr, uid, ids, context=None):
780         self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
781         return True
782
783 purchase_order_line()
784
785 class procurement_order(osv.osv):
786     _inherit = 'procurement.order'
787     _columns = {
788         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
789     }
790
791     def action_po_assign(self, cr, uid, ids, context=None):
792         """ This is action which call from workflow to assign purchase order to procurements
793         @return: True
794         """
795         res = self.make_po(cr, uid, ids, context=context)
796         res = res.values()
797         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
798
799     def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
800         """Create the purchase order from the procurement, using
801            the provided field values, after adding the given purchase
802            order line in the purchase order.
803
804            :params procurement: the procurement object generating the purchase order
805            :params dict po_vals: field values for the new purchase order (the
806                                  ``order_line`` field will be overwritten with one
807                                  single line, as passed in ``line_vals``).
808            :params dict line_vals: field values of the single purchase order line that
809                                    the purchase order will contain.
810            :return: id of the newly created purchase order
811            :rtype: int
812         """
813         po_vals.update({'order_line': [(0,0,line_vals)]})
814         return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
815
816     def make_po(self, cr, uid, ids, context=None):
817         """ Make purchase order from procurement
818         @return: New created Purchase Orders procurement wise
819         """
820         res = {}
821         if context is None:
822             context = {}
823         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
824         partner_obj = self.pool.get('res.partner')
825         uom_obj = self.pool.get('product.uom')
826         pricelist_obj = self.pool.get('product.pricelist')
827         prod_obj = self.pool.get('product.product')
828         acc_pos_obj = self.pool.get('account.fiscal.position')
829         seq_obj = self.pool.get('ir.sequence')
830         warehouse_obj = self.pool.get('stock.warehouse')
831         for procurement in self.browse(cr, uid, ids, context=context):
832             res_id = procurement.move_id.id
833             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
834             seller_qty = procurement.product_id.seller_qty
835             seller_delay = int(procurement.product_id.seller_delay)
836             partner_id = partner.id
837             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
838             pricelist_id = partner.property_product_pricelist_purchase.id
839             warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
840             uom_id = procurement.product_id.uom_po_id.id
841
842             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
843             if seller_qty:
844                 qty = max(qty,seller_qty)
845
846             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
847
848             order_date = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
849             schedule_date = (order_date - relativedelta(days=company.po_lead))
850             order_dates = schedule_date - relativedelta(days=seller_delay)
851
852             #Passing partner_id to context for purchase order line integrity of Line name
853             context.update({'lang': partner.lang, 'partner_id': partner_id})
854
855             product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
856             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
857             taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
858
859             line_vals = {
860                 'name': product.partner_ref,
861                 'product_qty': qty,
862                 'product_id': procurement.product_id.id,
863                 'product_uom': uom_id,
864                 'price_unit': price or 0.0,
865                 'date_planned': schedule_date.strftime('%Y-%m-%d %H:%M:%S'),
866                 'move_dest_id': res_id,
867                 'notes': product.description_purchase,
868                 'taxes_id': [(6,0,taxes)],
869             }
870             name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
871             po_vals = {
872                 'name': name,
873                 'origin': procurement.origin,
874                 'partner_id': partner_id,
875                 'partner_address_id': address_id,
876                 'location_id': procurement.location_id.id,
877                 'warehouse_id': warehouse_id and warehouse_id[0] or False,
878                 'pricelist_id': pricelist_id,
879                 'date_order': order_dates.strftime('%Y-%m-%d %H:%M:%S'),
880                 'company_id': procurement.company_id.id,
881                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
882             }
883             res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=context)
884             self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]})
885         return res
886
887 procurement_order()
888
889 class stock_invoice_onshipping(osv.osv_memory):
890     _inherit = "stock.invoice.onshipping"
891
892     def create_invoice(self, cr, uid, ids, context=None):
893         if context is None:
894             context = {}
895         res = super(stock_invoice_onshipping,self).create_invoice(cr, uid, ids, context=context)
896         purchase_obj = self.pool.get('purchase.order')
897         picking_obj = self.pool.get('stock.picking')
898         for pick_id in res:
899             pick = picking_obj.browse(cr, uid, pick_id, context=context)
900             if pick.purchase_id:
901                 purchase_obj.write(cr, uid, [pick.purchase_id.id], {
902                     'invoice_ids': [(4, res[pick_id])]}, context=context)
903         return res
904
905 stock_invoice_onshipping()
906
907 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: