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