[IMP]multi_company,purchase:Remove a demo data and sql constraint
[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, method=True, string='Received', type='float'),
189         'invoiced': fields.function(_invoiced, method=True, string='Invoiced & Paid', type='boolean', help="It indicates that an invoice has been paid"),
190         'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
191         'invoice_method': fields.selection([('manual','Manual'),('order','From Order'),('picking','From Picking')], 'Invoicing Control', required=True,
192             help="From Order: a draft invoice will be pre-generated based on the purchase order. The accountant " \
193                 "will just have to validate this invoice for control.\n" \
194                 "From Picking: a draft invoice will be pre-generated based on validated receptions.\n" \
195                 "Manual: allows you to generate suppliers invoices by chosing in the uninvoiced lines of all manual purchase orders."
196         ),
197         'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, method=True, string='Expected Date', type='date', select=True, help="This is computed as the minimum scheduled date of all purchase order lines' products.",
198             store = {
199                 'purchase.order.line': (_get_order, ['date_planned'], 10),
200             }
201         ),
202         'amount_untaxed': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Untaxed Amount',
203             store={
204                 'purchase.order.line': (_get_order, None, 10),
205             }, multi="sums", help="The amount without tax"),
206         'amount_tax': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Taxes',
207             store={
208                 'purchase.order.line': (_get_order, None, 10),
209             }, multi="sums", help="The tax amount"),
210         'amount_total': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Total',
211             store={
212                 'purchase.order.line': (_get_order, None, 10),
213             }, multi="sums",help="The total amount"),
214         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
215         'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
216         'create_uid':  fields.many2one('res.users', 'Responsible'),
217         'company_id': fields.many2one('res.company','Company',required=True,select=1),
218     }
219     _defaults = {
220         'date_order': 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 !'), _('Cannot delete Purchase Order(s) which are in %s State!')  % _(dict(purchase_order.STATE_SELECTION).get(s['state'])))
245
246         # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
247         # automatically sending subflow.delete upon deletion
248         wf_service = netsvc.LocalService("workflow")
249         for id in unlink_ids:
250             wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
251
252         return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
253
254     def button_dummy(self, cr, uid, ids, context=None):
255         return True
256
257     def onchange_dest_address_id(self, cr, uid, ids, adr_id):
258         if not adr_id:
259             return {}
260         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 can not confirm purchase order without Purchase Order 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                         _('Could not cancel purchase order !'),
410                         _('You must first cancel all picking attached 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                         _('Could not cancel this purchase order !'),
418                         _('You must first cancel all invoices attached 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 action_picking_create(self,cr, uid, ids, *args):
429         picking_id = False
430         for order in self.browse(cr, uid, ids):
431             loc_id = order.partner_id.property_stock_supplier.id
432             istate = 'none'
433             if order.invoice_method=='picking':
434                 istate = '2binvoiced'
435             pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in')
436             picking_id = self.pool.get('stock.picking').create(cr, uid, {
437                 'name': pick_name,
438                 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
439                 'type': 'in',
440                 'address_id': order.dest_address_id.id or order.partner_address_id.id,
441                 'invoice_state': istate,
442                 'purchase_id': order.id,
443                 'company_id': order.company_id.id,
444                 'move_lines' : [],
445             })
446             todo_moves = []
447             for order_line in order.order_line:
448                 if not order_line.product_id:
449                     continue
450                 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
451                     dest = order.location_id.id
452                     move = self.pool.get('stock.move').create(cr, uid, {
453                         'name': order.name + ': ' +(order_line.name or ''),
454                         'product_id': order_line.product_id.id,
455                         'product_qty': order_line.product_qty,
456                         'product_uos_qty': order_line.product_qty,
457                         'product_uom': order_line.product_uom.id,
458                         'product_uos': order_line.product_uom.id,
459                         'date': order_line.date_planned,
460                         'date_expected': order_line.date_planned,
461                         'location_id': loc_id,
462                         'location_dest_id': dest,
463                         'picking_id': picking_id,
464                         'move_dest_id': order_line.move_dest_id.id,
465                         'state': 'draft',
466                         'purchase_line_id': order_line.id,
467                         'company_id': order.company_id.id,
468                         'price_unit': order_line.price_unit
469                     })
470                     if order_line.move_dest_id:
471                         self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
472                     todo_moves.append(move)
473             self.pool.get('stock.move').action_confirm(cr, uid, todo_moves)
474             self.pool.get('stock.move').force_assign(cr, uid, todo_moves)
475             wf_service = netsvc.LocalService("workflow")
476             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
477         return picking_id
478
479     def copy(self, cr, uid, id, default=None, context=None):
480         if not default:
481             default = {}
482         default.update({
483             'state':'draft',
484             'shipped':False,
485             'invoiced':False,
486             'invoice_ids': [],
487             'picking_ids': [],
488             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
489         })
490         return super(purchase_order, self).copy(cr, uid, id, default, context)
491
492
493     def do_merge(self, cr, uid, ids, context=None):
494         """
495         To merge similar type of purchase orders.
496         Orders will only be merged if:
497         * Purchase Orders are in draft
498         * Purchase Orders belong to the same partner
499         * Purchase Orders are have same stock location, same pricelist
500         Lines will only be merged if:
501         * Order lines are exactly the same except for the quantity and unit
502
503          @param self: The object pointer.
504          @param cr: A database cursor
505          @param uid: ID of the user currently logged in
506          @param ids: the ID or list of IDs
507          @param context: A standard dictionary
508
509          @return: new purchase order id
510
511         """
512         wf_service = netsvc.LocalService("workflow")
513         def make_key(br, fields):
514             list_key = []
515             for field in fields:
516                 field_val = getattr(br, field)
517                 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
518                     if not field_val:
519                         field_val = False
520                 if isinstance(field_val, browse_record):
521                     field_val = field_val.id
522                 elif isinstance(field_val, browse_null):
523                     field_val = False
524                 elif isinstance(field_val, list):
525                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
526                 list_key.append((field, field_val))
527             list_key.sort()
528             return tuple(list_key)
529
530     # compute what the new orders should contain
531
532         new_orders = {}
533
534         for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
535             order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
536             new_order = new_orders.setdefault(order_key, ({}, []))
537             new_order[1].append(porder.id)
538             order_infos = new_order[0]
539             if not order_infos:
540                 order_infos.update({
541                     'origin': porder.origin,
542                     'date_order': time.strftime('%Y-%m-%d'),
543                     'partner_id': porder.partner_id.id,
544                     'partner_address_id': porder.partner_address_id.id,
545                     'dest_address_id': porder.dest_address_id.id,
546                     'warehouse_id': porder.warehouse_id.id,
547                     'location_id': porder.location_id.id,
548                     'pricelist_id': porder.pricelist_id.id,
549                     'state': 'draft',
550                     'order_line': {},
551                     'notes': '%s' % (porder.notes or '',),
552                     'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
553                 })
554             else:
555                 if porder.notes:
556                     order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
557                 if porder.origin:
558                     order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
559
560             for order_line in porder.order_line:
561                 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
562                 o_line = order_infos['order_line'].setdefault(line_key, {})
563                 if o_line:
564                     # merge the line with an existing line
565                     o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
566                 else:
567                     # append a new "standalone" line
568                     for field in ('product_qty', 'product_uom'):
569                         field_val = getattr(order_line, field)
570                         if isinstance(field_val, browse_record):
571                             field_val = field_val.id
572                         o_line[field] = field_val
573                     o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
574
575
576
577         allorders = []
578         orders_info = {}
579         for order_key, (order_data, old_ids) in new_orders.iteritems():
580             # skip merges with only one order
581             if len(old_ids) < 2:
582                 allorders += (old_ids or [])
583                 continue
584
585             # cleanup order line data
586             for key, value in order_data['order_line'].iteritems():
587                 del value['uom_factor']
588                 value.update(dict(key))
589             order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
590
591             # create the new order
592             neworder_id = self.create(cr, uid, order_data)
593             orders_info.update({neworder_id: old_ids})
594             allorders.append(neworder_id)
595
596             # make triggers pointing to the old orders point to the new order
597             for old_id in old_ids:
598                 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
599                 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
600         return orders_info
601
602 purchase_order()
603
604 class purchase_order_line(osv.osv):
605     def _amount_line(self, cr, uid, ids, prop, arg, context=None):
606         res = {}
607         cur_obj=self.pool.get('res.currency')
608         tax_obj = self.pool.get('account.tax')
609         for line in self.browse(cr, uid, ids, context=context):
610             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty)
611             cur = line.order_id.pricelist_id.currency_id
612             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
613         return res
614
615     _columns = {
616         'name': fields.char('Description', size=256, required=True),
617         'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
618         'date_planned': fields.date('Scheduled Date', required=True, select=True),
619         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
620         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
621         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
622         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
623         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
624         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
625         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
626         'notes': fields.text('Notes'),
627         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
628         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
629         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
630         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', required=True, readonly=True,
631                                   help=' * The \'Draft\' state is set automatically when purchase order in draft state. \
632                                        \n* The \'Confirmed\' state is set automatically as confirm when purchase order in confirm state. \
633                                        \n* The \'Done\' state is set automatically when purchase order is set as done. \
634                                        \n* The \'Cancelled\' state is set automatically when user cancel purchase order.'),
635         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
636         'invoiced': fields.boolean('Invoiced', readonly=True),
637         'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
638         'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
639
640     }
641     _defaults = {
642         'product_qty': lambda *a: 1.0,
643         'state': lambda *args: 'draft',
644         'invoiced': lambda *a: 0,
645     }
646     _table = 'purchase_order_line'
647     _name = 'purchase.order.line'
648     _description = 'Purchase Order Line'
649
650     def copy_data(self, cr, uid, id, default=None, context=None):
651         if not default:
652             default = {}
653         default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
654         return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
655
656     def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
657             partner_id, date_order=False, fiscal_position=False, date_planned=False,
658             name=False, price_unit=False, notes=False):
659         if not pricelist:
660             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.'))
661         if not  partner_id:
662             raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
663         if not product:
664             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
665                 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
666         res = {}
667         prod= self.pool.get('product.product').browse(cr, uid, product)
668         product_uom_pool = self.pool.get('product.uom')
669         lang=False
670         if partner_id:
671             lang=self.pool.get('res.partner').read(cr, uid, partner_id, ['lang'])['lang']
672         context={'lang':lang}
673         context['partner_id'] = partner_id
674
675         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
676         prod_uom_po = prod.uom_po_id.id
677         if not uom:
678             uom = prod_uom_po
679         if not date_order:
680             date_order = time.strftime('%Y-%m-%d')
681         qty = qty or 1.0
682         seller_delay = 0
683         if uom:
684             uom1_cat = prod.uom_id.category_id.id
685             uom2_cat = product_uom_pool.browse(cr, uid, uom).category_id.id
686             if uom1_cat != uom2_cat:
687                 uom = False
688
689         prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id], context=context)[0][1]
690         res = {}
691         for s in prod.seller_ids:
692             if s.name.id == partner_id:
693                 seller_delay = s.delay
694                 if s.product_uom:
695                     temp_qty = product_uom_pool._compute_qty(cr, uid, s.product_uom.id, s.min_qty, to_uom_id=prod.uom_id.id)
696                     uom = s.product_uom.id #prod_uom_po
697                 temp_qty = s.min_qty # supplier _qty assigned to temp
698                 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
699                     qty = temp_qty
700                     res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier has a minimal quantity set to %s, you cannot purchase less.') % qty}})
701         qty_in_product_uom = product_uom_pool._compute_qty(cr, uid, uom, qty, to_uom_id=prod.uom_id.id)
702         price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
703                     product, qty_in_product_uom or 1.0, partner_id, {
704                         'uom': uom,
705                         'date': date_order,
706                         })[pricelist]
707         dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
708
709
710         res.update({'value': {'price_unit': price, 'name': prod_name,
711             'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
712             'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
713             'product_qty': qty,
714             'product_uom': prod.uom_id.id}})
715         domain = {}
716
717         taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
718         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
719         res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
720         res2 = self.pool.get('product.uom').read(cr, uid, [prod.uom_id.id], ['category_id'])
721         res3 = prod.uom_id.category_id.id
722         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
723         if res2[0]['category_id'][0] != res3:
724             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'))
725
726         res['domain'] = domain
727         return res
728
729     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
730             partner_id, date_order=False, fiscal_position=False, date_planned=False,
731             name=False, price_unit=False, notes=False):
732         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
733                 partner_id, date_order=date_order, fiscal_position=fiscal_position, date_planned=date_planned,
734             name=name, price_unit=price_unit, notes=notes)
735         if 'product_uom' in res['value']:
736             if uom and (uom != res['value']['product_uom']) and res['value']['product_uom']:
737                 seller_uom_name = self.pool.get('product.uom').read(cr, uid, [res['value']['product_uom']], ['name'])[0]['name']
738                 res.update({'warning': {'title': _('Warning'), 'message': _('The selected supplier only sells this product by %s') % seller_uom_name }})
739             del res['value']['product_uom']
740         if not uom:
741             res['value']['price_unit'] = 0.0
742         return res
743
744     def action_confirm(self, cr, uid, ids, context=None):
745         self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
746         return True
747
748 purchase_order_line()
749
750 class procurement_order(osv.osv):
751     _inherit = 'procurement.order'
752     _columns = {
753         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
754     }
755
756     def action_po_assign(self, cr, uid, ids, context=None):
757         """ This is action which call from workflow to assign purchase order to procurements
758         @return: True
759         """
760         res = self.make_po(cr, uid, ids, context=context)
761         res = res.values()
762         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
763
764     def make_po(self, cr, uid, ids, context=None):
765         """ Make purchase order from procurement
766         @return: New created Purchase Orders procurement wise
767         """
768         res = {}
769         if context is None:
770             context = {}
771         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
772         partner_obj = self.pool.get('res.partner')
773         uom_obj = self.pool.get('product.uom')
774         pricelist_obj = self.pool.get('product.pricelist')
775         prod_obj = self.pool.get('product.product')
776         acc_pos_obj = self.pool.get('account.fiscal.position')
777         po_obj = self.pool.get('purchase.order')
778         for procurement in self.browse(cr, uid, ids, context=context):
779             res_id = procurement.move_id.id
780             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
781             seller_qty = procurement.product_id.seller_qty
782             seller_delay = int(procurement.product_id.seller_delay)
783             partner_id = partner.id
784             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
785             pricelist_id = partner.property_product_pricelist_purchase.id
786
787             uom_id = procurement.product_id.uom_po_id.id
788
789             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
790             if seller_qty:
791                 qty = max(qty,seller_qty)
792
793             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
794
795             newdate = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
796             newdate = (newdate - relativedelta(days=company.po_lead)) - relativedelta(days=seller_delay)
797
798             #Passing partner_id to context for purchase order line integrity of Line name
799             context.update({'lang': partner.lang, 'partner_id': partner_id})
800
801             product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
802
803             line = {
804                 'name': product.partner_ref,
805                 'product_qty': qty,
806                 'product_id': procurement.product_id.id,
807                 'product_uom': uom_id,
808                 'price_unit': price,
809                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
810                 'move_dest_id': res_id,
811                 'notes': product.description_purchase,
812             }
813
814             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
815             taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
816             line.update({
817                 'taxes_id': [(6,0,taxes)]
818             })
819             purchase_id = po_obj.create(cr, uid, {
820                 'origin': procurement.origin,
821                 'partner_id': partner_id,
822                 'partner_address_id': address_id,
823                 'location_id': procurement.location_id.id,
824                 'pricelist_id': pricelist_id,
825                 'order_line': [(0,0,line)],
826                 'company_id': procurement.company_id.id,
827                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
828             })
829             res[procurement.id] = purchase_id
830             self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': purchase_id})
831         return res
832
833 procurement_order()
834
835 class stock_invoice_onshipping(osv.osv_memory):
836     _inherit = "stock.invoice.onshipping"
837
838     def create_invoice(self, cr, uid, ids, context=None):
839         if context is None:
840             context = {}
841         res = super(stock_invoice_onshipping,self).create_invoice(cr, uid, ids, context=context)
842         purchase_obj = self.pool.get('purchase.order')
843         picking_obj = self.pool.get('stock.picking')
844         for pick_id in res:
845             pick = picking_obj.browse(cr, uid, pick_id, context=context)
846             if pick.purchase_id:
847                 purchase_obj.write(cr, uid, [pick.purchase_id.id], {
848                     'invoice_ids': [(4, res[pick_id])]}, context=context)
849         return res
850
851 stock_invoice_onshipping()
852
853 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: