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