[MERGE] account journal items widget by dle
[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 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
33
34 class purchase_order(osv.osv):
35
36     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
37         res = {}
38         cur_obj=self.pool.get('res.currency')
39         for order in self.browse(cr, uid, ids, context=context):
40             res[order.id] = {
41                 'amount_untaxed': 0.0,
42                 'amount_tax': 0.0,
43                 'amount_total': 0.0,
44             }
45             val = val1 = 0.0
46             cur = order.pricelist_id.currency_id
47             for line in order.order_line:
48                val1 += line.price_subtotal
49                for c in self.pool.get('account.tax').compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, order.partner_id)['taxes']:
50                     val += c.get('amount', 0.0)
51             res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
52             res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
53             res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
54         return res
55
56     def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
57         if not value: return False
58         if type(ids)!=type([]):
59             ids=[ids]
60         for po in self.browse(cr, uid, ids, context=context):
61             if po.order_line:
62                 cr.execute("""update purchase_order_line set
63                         date_planned=%s
64                     where
65                         order_id=%s and
66                         (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
67             cr.execute("""update purchase_order set
68                     minimum_planned_date=%s where id=%s""", (value, po.id))
69         return True
70
71     def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
72         res={}
73         purchase_obj=self.browse(cr, uid, ids, context=context)
74         for purchase in purchase_obj:
75             res[purchase.id] = False
76             if purchase.order_line:
77                 min_date=purchase.order_line[0].date_planned
78                 for line in purchase.order_line:
79                     if line.date_planned < min_date:
80                         min_date=line.date_planned
81                 res[purchase.id]=min_date
82         return res
83
84
85     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
86         res = {}
87         for purchase in self.browse(cursor, user, ids, context=context):
88             tot = 0.0
89             for invoice in purchase.invoice_ids:
90                 if invoice.state not in ('draft','cancel'):
91                     tot += invoice.amount_untaxed
92             if purchase.amount_untaxed:
93                 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
94             else:
95                 res[purchase.id] = 0.0
96         return res
97
98     def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
99         if not ids: return {}
100         res = {}
101         for id in ids:
102             res[id] = [0.0,0.0]
103         cr.execute('''SELECT
104                 p.purchase_id,sum(m.product_qty), m.state
105             FROM
106                 stock_move m
107             LEFT JOIN
108                 stock_picking p on (p.id=m.picking_id)
109             WHERE
110                 p.purchase_id IN %s GROUP BY m.state, p.purchase_id''',(tuple(ids),))
111         for oid,nbr,state in cr.fetchall():
112             if state=='cancel':
113                 continue
114             if state=='done':
115                 res[oid][0] += nbr or 0.0
116                 res[oid][1] += nbr or 0.0
117             else:
118                 res[oid][1] += nbr or 0.0
119         for r in res:
120             if not res[r][1]:
121                 res[r] = 0.0
122             else:
123                 res[r] = 100.0 * res[r][0] / res[r][1]
124         return res
125
126     def _get_order(self, cr, uid, ids, context=None):
127         result = {}
128         for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
129             result[line.order_id.id] = True
130         return result.keys()
131
132     def _invoiced(self, cursor, user, ids, name, arg, context=None):
133         res = {}
134         for purchase in self.browse(cursor, user, ids, context=context):
135             invoiced = False
136             if purchase.invoiced_rate == 100.00:
137                 invoiced = True
138             res[purchase.id] = invoiced
139         return res
140     
141     def _get_journal(self, cr, uid, context=None):
142         if context is None:
143             context = {}
144         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
145         company_id = context.get('company_id', user.company_id.id)
146         journal_obj = self.pool.get('account.journal')
147         res = journal_obj.search(cr, uid, [('type', '=', 'purchase'),
148                                             ('company_id', '=', company_id)],
149                                                 limit=1)
150         return res and res[0] or False  
151
152     STATE_SELECTION = [
153         ('draft', 'Draft PO'),
154         ('sent', 'RFQ Sent'),
155         ('confirmed', 'Waiting Approval'),
156         ('approved', 'Purchase Order'),
157         ('except_picking', 'Shipping Exception'),
158         ('except_invoice', 'Invoice Exception'),
159         ('done', 'Done'),
160         ('cancel', 'Cancelled')
161     ]
162
163     _columns = {
164         '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."),
165         'origin': fields.char('Source Document', size=64,
166             help="Reference of the document that generated this purchase order request; a sale order or an internal procurement request."
167         ),
168         'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, size=64,
169             help="Reference of the sale order or quotation sent by your supplier. It's mainly used to do the matching when you receive the products as this reference is usually written on the delivery order sent by your supplier."),
170         'date_order':fields.date('Order Date', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, select=True, help="Date on which this document has been created."),
171         'date_approve':fields.date('Date Approved', readonly=1, select=True, help="Date on which purchase order has been approved"),
172         'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, change_default=True),
173         'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
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                 "Otherwise, keep empty to deliver to your own company."
177         ),
178         'warehouse_id': fields.many2one('stock.warehouse', 'Destination Warehouse'),
179         'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]} ),
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         'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", readonly=True, required=True),
182         'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="The status of the purchase order or the quotation request. A quotation is a purchase order in a 'Draft' status. Then the order has to be confirmed by the user, the status switch to 'Confirmed'. Then the supplier must confirm the order to change the status to 'Approved'. When the purchase order is paid and received, the status becomes 'Done'. If a cancel action occurs in the invoice or in the reception of goods, the status becomes in exception.", select=True),
183         'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
184         'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
185         'notes': fields.text('Terms and Conditions'),
186         'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id', 'invoice_id', 'Invoices', help="Invoices generated for a purchase order"),
187         'picking_ids': fields.one2many('stock.picking.in', 'purchase_id', 'Picking List', readonly=True, help="This is the list of incoming shipments that have been generated for this purchase order."),
188         'shipped':fields.boolean('Received', readonly=True, select=True, help="It indicates that a picking has been done"),
189         'shipped_rate': fields.function(_shipped_rate, string='Received', type='float'),
190         'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', help="It indicates that an invoice has been paid"),
191         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
192         'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on incoming shipments')], 'Invoicing Control', required=True,
193             readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)]},
194             help="Based on Purchase Order lines: place individual lines in 'Invoice Control > Based on P.O. lines' from where you can selectively create an invoice.\n" \
195                 "Based on generated invoice: create a draft invoice you can validate later.\n" \
196                 "Bases on incoming shipments: let you create an invoice when receptions are validated."
197         ),
198         'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, string='Expected Date', type='date', select=True, help="This is computed as the minimum scheduled date of all purchase order lines' products.",
199             store = {
200                 'purchase.order.line': (_get_order, ['date_planned'], 10),
201             }
202         ),
203         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Untaxed Amount',
204             store={
205                 'purchase.order.line': (_get_order, None, 10),
206             }, multi="sums", help="The amount without tax"),
207         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Taxes',
208             store={
209                 'purchase.order.line': (_get_order, None, 10),
210             }, multi="sums", help="The tax amount"),
211         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Total',
212             store={
213                 'purchase.order.line': (_get_order, None, 10),
214             }, multi="sums",help="The total amount"),
215         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
216         'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
217         'create_uid':  fields.many2one('res.users', 'Responsible'),
218         'company_id': fields.many2one('res.company','Company',required=True,select=1, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
219         'journal_id': fields.many2one('account.journal', 'Journal'),
220     }
221     _defaults = {
222         'date_order': fields.date.context_today,
223         'state': 'draft',
224         'name': lambda obj, cr, uid, context: '/',
225         'shipped': 0,
226         'invoice_method': 'order',
227         'invoiced': 0,
228         '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,
229         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
230         'journal_id': _get_journal,
231     }
232     _sql_constraints = [
233         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
234     ]
235     _name = "purchase.order"
236     _inherit = ['mail.thread', 'ir.needaction_mixin']
237     _description = "Purchase Order"
238     _order = "name desc"
239
240     def create(self, cr, uid, vals, context=None):
241         if vals.get('name','/')=='/':
242             vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'purchase.order') or '/'
243         order =  super(purchase_order, self).create(cr, uid, vals, context=context)
244         if order:
245             self.create_send_note(cr, uid, [order], context=context)
246         return order
247
248     def unlink(self, cr, uid, ids, context=None):
249         purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
250         unlink_ids = []
251         for s in purchase_orders:
252             if s['state'] in ['draft','cancel']:
253                 unlink_ids.append(s['id'])
254             else:
255                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.'))
256
257         # automatically sending subflow.delete upon deletion
258         wf_service = netsvc.LocalService("workflow")
259         for id in unlink_ids:
260             wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
261
262         return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
263
264     def button_dummy(self, cr, uid, ids, context=None):
265         return True
266
267     def onchange_pricelist(self, cr, uid, ids, pricelist_id, context=None):
268         if not pricelist_id:
269             return {}
270         return {'value': {'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id}}
271
272     def onchange_dest_address_id(self, cr, uid, ids, address_id):
273         if not address_id:
274             return {}
275         address = self.pool.get('res.partner')
276         values = {'warehouse_id': False}
277         supplier = address.browse(cr, uid, address_id)
278         if supplier:
279             location_id = supplier.property_stock_customer.id
280             values.update({'location_id': location_id})
281         return {'value':values}
282
283     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
284         if not warehouse_id:
285             return {}
286         warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id)
287         return {'value':{'location_id': warehouse.lot_input_id.id, 'dest_address_id': False}}
288
289     def onchange_partner_id(self, cr, uid, ids, partner_id):
290         partner = self.pool.get('res.partner')
291         if not partner_id:
292             return {'value':{'fiscal_position': False}}
293         supplier_address = partner.address_get(cr, uid, [partner_id], ['default'])
294         supplier = partner.browse(cr, uid, partner_id)
295         pricelist = supplier.property_product_pricelist_purchase.id
296         fiscal_position = supplier.property_account_position and supplier.property_account_position.id or False
297         return {'value':{'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
298
299     def invoice_open(self, cr, uid, ids, context=None):
300         mod_obj = self.pool.get('ir.model.data')
301         act_obj = self.pool.get('ir.actions.act_window')
302
303         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree2')
304         id = result and result[1] or False
305         result = act_obj.read(cr, uid, [id], context=context)[0]
306         inv_ids = []
307         for po in self.browse(cr, uid, ids, context=context):
308             inv_ids+= [invoice.id for invoice in po.invoice_ids]
309         if not inv_ids:
310             raise osv.except_osv(_('Error!'), _('Please create Invoices.'))
311          #choose the view_mode accordingly
312         if len(inv_ids)>1:
313             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
314         else:
315             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
316             result['views'] = [(res and res[1] or False, 'form')]
317             result['res_id'] = inv_ids and inv_ids[0] or False
318         return result
319
320     def view_invoice(self, cr, uid, ids, context=None):
321         '''
322         This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
323         '''
324         mod_obj = self.pool.get('ir.model.data')
325         wizard_obj = self.pool.get('purchase.order.line_invoice')
326         #compute the number of invoices to display
327         inv_ids = []
328         for po in self.browse(cr, uid, ids, context=context):
329             if po.invoice_method == 'manual':
330                 if not po.invoice_ids:
331                     context.update({'active_ids' :  [line.id for line in po.order_line]})
332                     wizard_obj.makeInvoices(cr, uid, [], context=context)
333
334         for po in self.browse(cr, uid, ids, context=context):
335             inv_ids+= [invoice.id for invoice in po.invoice_ids]
336         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
337         res_id = res and res[1] or False
338
339         return {
340             'name': _('Supplier Invoices'),
341             'view_type': 'form',
342             'view_mode': 'form',
343             'view_id': [res_id],
344             'res_model': 'account.invoice',
345             'context': "{'type':'in_invoice', 'journal_type': 'purchase'}",
346             'type': 'ir.actions.act_window',
347             'nodestroy': True,
348             'target': 'current',
349             'res_id': inv_ids and inv_ids[0] or False,
350         }
351
352     def view_picking(self, cr, uid, ids, context=None):
353         '''
354         This function returns an action that display existing pîcking orders of given purchase order ids.
355         '''
356         mod_obj = self.pool.get('ir.model.data')
357         pick_ids = []
358         for po in self.browse(cr, uid, ids, context=context):
359             pick_ids += [picking.id for picking in po.picking_ids]
360
361         action_model, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree4'))
362         action = self.pool.get(action_model).read(cr, uid, action_id, context=context)
363         ctx = eval(action['context'])
364         ctx.update({
365             'search_default_purchase_id': ids[0]
366         })
367         if pick_ids and len(pick_ids) == 1:
368             form_view_ids = [view_id for view_id, view in action['views'] if view == 'form']
369             view_id = form_view_ids and form_view_ids[0] or False
370             action.update({
371                 'views': [],
372                 'view_mode': 'form',
373                 'view_id': view_id,
374                 'res_id': pick_ids[0]
375             })
376
377         action.update({
378             'context': ctx,
379         })
380         return action
381
382     def wkf_approve_order(self, cr, uid, ids, context=None):
383         self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)})
384         return True
385
386     def wkf_send_rfq(self, cr, uid, ids, context=None):
387         '''
388         This function opens a window to compose an email, with the edi purchase template message loaded by default
389         '''
390         ir_model_data = self.pool.get('ir.model.data')
391         try:
392             template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
393         except ValueError:
394             template_id = False
395         try:
396             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
397         except ValueError:
398             compose_form_id = False 
399         ctx = dict(context)
400         ctx.update({
401             'default_model': 'purchase.order',
402             'default_res_id': ids[0],
403             'default_use_template': bool(template_id),
404             'default_template_id': template_id,
405             'default_composition_mode': 'comment',
406         })
407         return {
408             'type': 'ir.actions.act_window',
409             'view_type': 'form',
410             'view_mode': 'form',
411             'res_model': 'mail.compose.message',
412             'views': [(compose_form_id, 'form')],
413             'view_id': compose_form_id,
414             'target': 'new',
415             'context': ctx,
416         }
417
418     def print_quotation(self, cr, uid, ids, context=None):
419         '''
420         This function prints the request for quotation and mark it as sent, so that we can see more easily the next step of the workflow
421         '''
422         assert len(ids) == 1, 'This option should only be used for a single id at a time'
423         wf_service = netsvc.LocalService("workflow")
424         wf_service.trg_validate(uid, 'purchase.order', ids[0], 'send_rfq', cr)
425         datas = {
426                  'model': 'purchase.order',
427                  'ids': ids,
428                  'form': self.read(cr, uid, ids[0], context=context),
429         }
430         return {'type': 'ir.actions.report.xml', 'report_name': 'purchase.quotation', 'datas': datas, 'nodestroy': True}
431
432     #TODO: implement messages system
433     def wkf_confirm_order(self, cr, uid, ids, context=None):
434         todo = []
435         for po in self.browse(cr, uid, ids, context=context):
436             if not po.order_line:
437                 raise osv.except_osv(_('Error!'),_('You cannot confirm a purchase order without any purchase order line.'))
438             for line in po.order_line:
439                 if line.state=='draft':
440                     todo.append(line.id)
441
442         self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
443         for id in ids:
444             self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
445         self.confirm_send_note(cr, uid, ids, context)
446         return True
447
448     def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
449         """Collects require data from purchase order line that is used to create invoice line
450         for that purchase order line
451         :param account_id: Expense account of the product of PO line if any.
452         :param browse_record order_line: Purchase order line browse record
453         :return: Value for fields of invoice lines.
454         :rtype: dict
455         """
456         return {
457             'name': order_line.name,
458             'account_id': account_id,
459             'price_unit': order_line.price_unit or 0.0,
460             'quantity': order_line.product_qty,
461             'product_id': order_line.product_id.id or False,
462             'uos_id': order_line.product_uom.id or False,
463             'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
464             'account_analytic_id': order_line.account_analytic_id.id or False,
465         }
466
467     def action_cancel_draft(self, cr, uid, ids, context=None):
468         if not len(ids):
469             return False
470         self.write(cr, uid, ids, {'state':'draft','shipped':0})
471         wf_service = netsvc.LocalService("workflow")
472         for p_id in ids:
473             # Deleting the existing instance of workflow for PO
474             wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
475             wf_service.trg_create(uid, 'purchase.order', p_id, cr)
476         self.draft_send_note(cr, uid, ids, context=context)
477         return True
478
479     def action_invoice_create(self, cr, uid, ids, context=None):
480         """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
481         :param ids: list of ids of purchase orders.
482         :return: ID of created invoice.
483         :rtype: int
484         """
485         res = False
486
487         journal_obj = self.pool.get('account.journal')
488         inv_obj = self.pool.get('account.invoice')
489         inv_line_obj = self.pool.get('account.invoice.line')
490         fiscal_obj = self.pool.get('account.fiscal.position')
491         property_obj = self.pool.get('ir.property')
492
493         for order in self.browse(cr, uid, ids, context=context):
494             pay_acc_id = order.partner_id.property_account_payable.id
495             journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', order.company_id.id)], limit=1)
496             if not journal_ids:
497                 raise osv.except_osv(_('Error!'),
498                     _('Define purchase journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
499
500             # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
501             inv_lines = []
502             for po_line in order.order_line:
503                 if po_line.product_id:
504                     acc_id = po_line.product_id.product_tmpl_id.property_account_expense.id
505                     if not acc_id:
506                         acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
507                     if not acc_id:
508                         raise osv.except_osv(_('Error!'), _('Define expense account for this company: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
509                 else:
510                     acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category').id
511                 fpos = order.fiscal_position or False
512                 acc_id = fiscal_obj.map_account(cr, uid, fpos, acc_id)
513
514                 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
515                 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
516                 inv_lines.append(inv_line_id)
517
518                 po_line.write({'invoiced':True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
519
520             # get invoice data and create invoice
521             inv_data = {
522                 'name': order.partner_ref or order.name,
523                 'reference': order.partner_ref or order.name,
524                 'account_id': pay_acc_id,
525                 'type': 'in_invoice',
526                 'partner_id': order.partner_id.id,
527                 'currency_id': order.pricelist_id.currency_id.id,
528                 'journal_id': len(journal_ids) and journal_ids[0] or False,
529                 'invoice_line': [(6, 0, inv_lines)],
530                 'origin': order.name,
531                 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
532                 'payment_term': order.partner_id.property_payment_term and order.partner_id.property_payment_term.id or False,
533                 'company_id': order.company_id.id,
534             }
535             inv_id = inv_obj.create(cr, uid, inv_data, context=context)
536
537             # compute the invoice
538             inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
539
540             # Link this new invoice to related purchase order
541             order.write({'invoice_ids': [(4, inv_id)]}, context=context)
542             res = inv_id
543         if res:
544             self.invoice_send_note(cr, uid, ids, res, context)
545         return res
546
547     def invoice_done(self, cr, uid, ids, context=None):
548         self.write(cr, uid, ids, {'state':'approved'}, context=context)
549         self.invoice_done_send_note(cr, uid, ids, context=context)
550         return True
551
552     def has_stockable_product(self, cr, uid, ids, *args):
553         for order in self.browse(cr, uid, ids):
554             for order_line in order.order_line:
555                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
556                     return True
557         return False
558
559     def action_cancel(self, cr, uid, ids, context=None):
560         wf_service = netsvc.LocalService("workflow")
561         for purchase in self.browse(cr, uid, ids, context=context):
562             for pick in purchase.picking_ids:
563                 if pick.state not in ('draft','cancel'):
564                     raise osv.except_osv(
565                         _('Unable to cancel this purchase order.'),
566                         _('First cancel all receptions related to this purchase order.'))
567             for pick in purchase.picking_ids:
568                 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
569             for inv in purchase.invoice_ids:
570                 if inv and inv.state not in ('cancel','draft'):
571                     raise osv.except_osv(
572                         _('Unable to cancel this purchase order.'),
573                         _('You must first cancel all receptions related to this purchase order.'))
574                 if inv:
575                     wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
576         self.write(cr,uid,ids,{'state':'cancel'})
577
578         for (id, name) in self.name_get(cr, uid, ids):
579             wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
580         self.cancel_send_note(cr, uid, ids, context)
581         return True
582
583     def _prepare_order_picking(self, cr, uid, order, context=None):
584         return {
585             'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
586             'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
587             'date': order.date_order,
588             'partner_id': order.dest_address_id.id or order.partner_id.id,
589             'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
590             'type': 'in',
591             'partner_id': order.dest_address_id.id or order.partner_id.id,
592             'purchase_id': order.id,
593             'company_id': order.company_id.id,
594             'move_lines' : [],
595         }
596
597     def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, context=None):
598         return {
599             'name': order.name + ': ' + (order_line.name or ''),
600             'product_id': order_line.product_id.id,
601             'product_qty': order_line.product_qty,
602             'product_uos_qty': order_line.product_qty,
603             'product_uom': order_line.product_uom.id,
604             'product_uos': order_line.product_uom.id,
605             'date': order_line.date_planned,
606             'date_expected': order_line.date_planned,
607             'location_id': order.partner_id.property_stock_supplier.id,
608             'location_dest_id': order.location_id.id,
609             'picking_id': picking_id,
610             'partner_id': order.dest_address_id.id or order.partner_id.id,
611             'move_dest_id': order_line.move_dest_id.id,
612             'state': 'draft',
613             'type':'in',
614             'purchase_line_id': order_line.id,
615             'company_id': order.company_id.id,
616             'price_unit': order_line.price_unit
617         }
618
619     def _create_pickings(self, cr, uid, order, order_lines, picking_id=False, context=None):
620         """Creates pickings and appropriate stock moves for given order lines, then
621         confirms the moves, makes them available, and confirms the picking.
622
623         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
624         a standard outgoing picking will be created to wrap the stock moves, as returned
625         by :meth:`~._prepare_order_picking`.
626
627         Modules that wish to customize the procurements or partition the stock moves over
628         multiple stock pickings may override this method and call ``super()`` with
629         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
630
631         :param browse_record order: purchase order to which the order lines belong
632         :param list(browse_record) order_lines: purchase order line records for which picking
633                                                 and moves should be created.
634         :param int picking_id: optional ID of a stock picking to which the created stock moves
635                                will be added. A new picking will be created if omitted.
636         :return: list of IDs of pickings used/created for the given order lines (usually just one)
637         """
638         if not picking_id:
639             picking_id = self.pool.get('stock.picking').create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
640         todo_moves = []
641         stock_move = self.pool.get('stock.move')
642         wf_service = netsvc.LocalService("workflow")
643         for order_line in order_lines:
644             if not order_line.product_id:
645                 continue
646             if order_line.product_id.type in ('product', 'consu'):
647                 move = stock_move.create(cr, uid, self._prepare_order_line_move(cr, uid, order, order_line, picking_id, context=context))
648                 if order_line.move_dest_id:
649                     order_line.move_dest_id.write({'location_id': order.location_id.id})
650                 todo_moves.append(move)
651         stock_move.action_confirm(cr, uid, todo_moves)
652         stock_move.force_assign(cr, uid, todo_moves)
653         wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
654         return [picking_id]
655
656     def action_picking_create(self, cr, uid, ids, context=None):
657         picking_ids = []
658         for order in self.browse(cr, uid, ids):
659             picking_ids.extend(self._create_pickings(cr, uid, order, order.order_line, None, context=context))
660
661         # Must return one unique picking ID: the one to connect in the subflow of the purchase order.
662         # In case of multiple (split) pickings, we should return the ID of the critical one, i.e. the
663         # one that should trigger the advancement of the purchase workflow.
664         # By default we will consider the first one as most important, but this behavior can be overridden.
665         if picking_ids:
666             self.shipment_send_note(cr, uid, ids, picking_ids[0], context=context)
667         return picking_ids[0] if picking_ids else False
668
669     def picking_done(self, cr, uid, ids, context=None):
670         self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
671         self.shipment_done_send_note(cr, uid, ids, context=context)
672         return True
673
674     def copy(self, cr, uid, id, default=None, context=None):
675         if not default:
676             default = {}
677         default.update({
678             'state':'draft',
679             'shipped':False,
680             'invoiced':False,
681             'invoice_ids': [],
682             'picking_ids': [],
683             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
684         })
685         return super(purchase_order, self).copy(cr, uid, id, default, context)
686
687     def do_merge(self, cr, uid, ids, context=None):
688         """
689         To merge similar type of purchase orders.
690         Orders will only be merged if:
691         * Purchase Orders are in draft
692         * Purchase Orders belong to the same partner
693         * Purchase Orders are have same stock location, same pricelist
694         Lines will only be merged if:
695         * Order lines are exactly the same except for the quantity and unit
696
697          @param self: The object pointer.
698          @param cr: A database cursor
699          @param uid: ID of the user currently logged in
700          @param ids: the ID or list of IDs
701          @param context: A standard dictionary
702
703          @return: new purchase order id
704
705         """
706         #TOFIX: merged order line should be unlink
707         wf_service = netsvc.LocalService("workflow")
708         def make_key(br, fields):
709             list_key = []
710             for field in fields:
711                 field_val = getattr(br, field)
712                 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
713                     if not field_val:
714                         field_val = False
715                 if isinstance(field_val, browse_record):
716                     field_val = field_val.id
717                 elif isinstance(field_val, browse_null):
718                     field_val = False
719                 elif isinstance(field_val, list):
720                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
721                 list_key.append((field, field_val))
722             list_key.sort()
723             return tuple(list_key)
724
725         # Compute what the new orders should contain
726
727         new_orders = {}
728
729         for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
730             order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
731             new_order = new_orders.setdefault(order_key, ({}, []))
732             new_order[1].append(porder.id)
733             order_infos = new_order[0]
734             if not order_infos:
735                 order_infos.update({
736                     'origin': porder.origin,
737                     'date_order': porder.date_order,
738                     'partner_id': porder.partner_id.id,
739                     'dest_address_id': porder.dest_address_id.id,
740                     'warehouse_id': porder.warehouse_id.id,
741                     'location_id': porder.location_id.id,
742                     'pricelist_id': porder.pricelist_id.id,
743                     'state': 'draft',
744                     'order_line': {},
745                     'notes': '%s' % (porder.notes or '',),
746                     'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
747                 })
748             else:
749                 if porder.date_order < order_infos['date_order']:
750                     order_infos['date_order'] = porder.date_order
751                 if porder.notes:
752                     order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
753                 if porder.origin:
754                     order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
755
756             for order_line in porder.order_line:
757                 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'product_id', 'move_dest_id', 'account_analytic_id'))
758                 o_line = order_infos['order_line'].setdefault(line_key, {})
759                 if o_line:
760                     # merge the line with an existing line
761                     o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
762                 else:
763                     # append a new "standalone" line
764                     for field in ('product_qty', 'product_uom'):
765                         field_val = getattr(order_line, field)
766                         if isinstance(field_val, browse_record):
767                             field_val = field_val.id
768                         o_line[field] = field_val
769                     o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
770
771
772
773         allorders = []
774         orders_info = {}
775         for order_key, (order_data, old_ids) in new_orders.iteritems():
776             # skip merges with only one order
777             if len(old_ids) < 2:
778                 allorders += (old_ids or [])
779                 continue
780
781             # cleanup order line data
782             for key, value in order_data['order_line'].iteritems():
783                 del value['uom_factor']
784                 value.update(dict(key))
785             order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
786
787             # create the new order
788             neworder_id = self.create(cr, uid, order_data)
789             orders_info.update({neworder_id: old_ids})
790             allorders.append(neworder_id)
791
792             # make triggers pointing to the old orders point to the new order
793             for old_id in old_ids:
794                 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
795                 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
796         return orders_info
797
798     # --------------------------------------
799     # OpenChatter methods and notifications
800     # --------------------------------------
801
802     def needaction_domain_get(self, cr, uid, ids, context=None):
803         return [('state', '=', 'draft')]
804
805     def create_send_note(self, cr, uid, ids, context=None):
806         return self.message_post(cr, uid, ids, body=_("Request for quotation <b>created</b>."), context=context)
807
808     def confirm_send_note(self, cr, uid, ids, context=None):
809         for obj in self.browse(cr, uid, ids, context=context):
810             self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to a Purchase Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
811
812     def shipment_send_note(self, cr, uid, ids, picking_id, context=None):
813         for order in self.browse(cr, uid, ids, context=context):
814             for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
815                 # convert datetime field to a datetime, using server format, then
816                 # convert it to the user TZ and re-render it with %Z to add the timezone
817                 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
818                 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
819                 self.message_post(cr, uid, [order.id], body=_("Shipment <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
820
821     def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
822         for order in self.browse(cr, uid, ids, context=context):
823             for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
824                 self.message_post(cr, uid, [order.id], body=_("Draft Invoice of %s %s is <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
825
826     def shipment_done_send_note(self, cr, uid, ids, context=None):
827         self.message_post(cr, uid, ids, body=_("""Shipment <b>received</b>."""), context=context)
828
829     def invoice_done_send_note(self, cr, uid, ids, context=None):
830         self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
831
832     def draft_send_note(self, cr, uid, ids, context=None):
833         return self.message_post(cr, uid, ids, body=_("Purchase Order has been set to <b>draft</b>."), context=context)
834
835     def cancel_send_note(self, cr, uid, ids, context=None):
836         for obj in self.browse(cr, uid, ids, context=context):
837             self.message_post(cr, uid, [obj.id], body=_("Purchase Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
838
839 purchase_order()
840
841 class purchase_order_line(osv.osv):
842     def _amount_line(self, cr, uid, ids, prop, arg, context=None):
843         res = {}
844         cur_obj=self.pool.get('res.currency')
845         tax_obj = self.pool.get('account.tax')
846         for line in self.browse(cr, uid, ids, context=context):
847             taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, line.order_id.partner_id)
848             cur = line.order_id.pricelist_id.currency_id
849             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
850         return res
851
852     _columns = {
853         'name': fields.text('Description', required=True),
854         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
855         'date_planned': fields.date('Scheduled Date', required=True, select=True),
856         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
857         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
858         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
859         'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
860         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
861         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
862         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
863         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
864         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
865         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
866         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
867                                   help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
868                                        \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
869                                        \n* The \'Done\' status is set automatically when purchase order is set as done. \
870                                        \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
871         'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
872         'invoiced': fields.boolean('Invoiced', readonly=True),
873         'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
874         'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
875
876     }
877     _defaults = {
878         'product_qty': lambda *a: 1.0,
879         'state': lambda *args: 'draft',
880         'invoiced': lambda *a: 0,
881     }
882     _table = 'purchase_order_line'
883     _name = 'purchase.order.line'
884     _description = 'Purchase Order Line'
885
886     def copy_data(self, cr, uid, id, default=None, context=None):
887         if not default:
888             default = {}
889         default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
890         return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
891
892     def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
893             partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
894             name=False, price_unit=False, context=None):
895         """
896         onchange handler of product_uom.
897         """
898         if not uom_id:
899             return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
900         return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
901             partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
902             name=name, price_unit=price_unit, context=context)
903
904     def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
905         """Return the datetime value to use as Schedule Date (``date_planned``) for
906            PO Lines that correspond to the given product.supplierinfo,
907            when ordered at `date_order_str`.
908
909            :param browse_record | False supplier_info: product.supplierinfo, used to
910                determine delivery delay (if False, default delay = 0)
911            :param str date_order_str: date of order, as a string in
912                DEFAULT_SERVER_DATE_FORMAT
913            :rtype: datetime
914            :return: desired Schedule Date for the PO line
915         """
916         supplier_delay = int(supplier_info.delay) if supplier_info else 0
917         return datetime.strptime(date_order_str, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=supplier_delay)
918
919     def _check_product_uom_group(self, cr, uid, context=None):
920         group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom')
921         res = [user for user in group_uom.users if user.id == uid]
922         return len(res) and True or False
923
924
925     def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
926             partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
927             name=False, price_unit=False, context=None):
928         """
929         onchange handler of product_id.
930         """
931         if context is None:
932             context = {}
933
934         res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
935         if not product_id:
936             return res
937
938         product_product = self.pool.get('product.product')
939         product_uom = self.pool.get('product.uom')
940         res_partner = self.pool.get('res.partner')
941         product_supplierinfo = self.pool.get('product.supplierinfo')
942         product_pricelist = self.pool.get('product.pricelist')
943         account_fiscal_position = self.pool.get('account.fiscal.position')
944         account_tax = self.pool.get('account.tax')
945
946         # - check for the presence of partner_id and pricelist_id
947         #if not partner_id:
948         #    raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
949         #if not pricelist_id:
950         #    raise osv.except_osv(_('No Pricelist !'), _('Select a price list in the purchase order form before choosing a product.'))
951
952         # - determine name and notes based on product in partner lang.
953         context_partner = context.copy()
954         if partner_id:
955             lang = res_partner.browse(cr, uid, partner_id).lang
956             context_partner.update( {'lang': lang, 'partner_id': partner_id} )
957         product = product_product.browse(cr, uid, product_id, context=context_partner)
958         name = product.name
959         if product.description_purchase:
960             name += '\n' + product.description_purchase
961         res['value'].update({'name': name})
962
963         # - set a domain on product_uom
964         res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
965
966         # - check that uom and product uom belong to the same category
967         product_uom_po_id = product.uom_po_id.id
968         if not uom_id:
969             uom_id = product_uom_po_id
970
971         if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
972             if self._check_product_uom_group(cr, uid, context=context):
973                 res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
974             uom_id = product_uom_po_id
975
976         res['value'].update({'product_uom': uom_id})
977
978         # - determine product_qty and date_planned based on seller info
979         if not date_order:
980             date_order = fields.date.context_today(self,cr,uid,context=context)
981
982         qty = qty or 1.0
983         supplierinfo = False
984         for supplier in product.seller_ids:
985             if partner_id and (supplier.name.id == partner_id):
986                 supplierinfo = supplier
987                 if supplierinfo.product_uom.id != uom_id:
988                     res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
989                 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
990                 if qty < min_qty: # If the supplier quantity is greater than entered from user, set minimal.
991                     res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier has a minimal quantity set to %s %s, you should not purchase less.') % (supplierinfo.min_qty, supplierinfo.product_uom.name)}
992                     qty = min_qty
993
994         dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
995
996         res['value'].update({'date_planned': date_planned or dt, 'product_qty': qty})
997
998         # - determine price_unit and taxes_id
999         if pricelist_id:
1000             price = product_pricelist.price_get(cr, uid, [pricelist_id],
1001                     product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order})[pricelist_id]
1002         else:
1003             price = product.standard_price
1004
1005         taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
1006         fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
1007         taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
1008         res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
1009
1010         return res
1011
1012     product_id_change = onchange_product_id
1013     product_uom_change = onchange_product_uom
1014
1015     def action_confirm(self, cr, uid, ids, context=None):
1016         self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
1017         return True
1018
1019 purchase_order_line()
1020
1021 class procurement_order(osv.osv):
1022     _inherit = 'procurement.order'
1023     _columns = {
1024         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
1025     }
1026
1027     def action_po_assign(self, cr, uid, ids, context=None):
1028         """ This is action which call from workflow to assign purchase order to procurements
1029         @return: True
1030         """
1031         res = self.make_po(cr, uid, ids, context=context)
1032         res = res.values()
1033         return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
1034
1035     def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
1036         """Create the purchase order from the procurement, using
1037            the provided field values, after adding the given purchase
1038            order line in the purchase order.
1039
1040            :params procurement: the procurement object generating the purchase order
1041            :params dict po_vals: field values for the new purchase order (the
1042                                  ``order_line`` field will be overwritten with one
1043                                  single line, as passed in ``line_vals``).
1044            :params dict line_vals: field values of the single purchase order line that
1045                                    the purchase order will contain.
1046            :return: id of the newly created purchase order
1047            :rtype: int
1048         """
1049         po_vals.update({'order_line': [(0,0,line_vals)]})
1050         return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
1051
1052     def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
1053         """Return the datetime value to use as Schedule Date (``date_planned``) for the
1054            Purchase Order Lines created to satisfy the given procurement.
1055
1056            :param browse_record procurement: the procurement for which a PO will be created.
1057            :param browse_report company: the company to which the new PO will belong to.
1058            :rtype: datetime
1059            :return: the desired Schedule Date for the PO lines
1060         """
1061         procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
1062         schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
1063         return schedule_date
1064
1065     def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
1066         """Return the datetime value to use as Order Date (``date_order``) for the
1067            Purchase Order created to satisfy the given procurement.
1068
1069            :param browse_record procurement: the procurement for which a PO will be created.
1070            :param browse_report company: the company to which the new PO will belong to.
1071            :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
1072            :rtype: datetime
1073            :return: the desired Order Date for the PO
1074         """
1075         seller_delay = int(procurement.product_id.seller_delay)
1076         return schedule_date - relativedelta(days=seller_delay)
1077
1078     def make_po(self, cr, uid, ids, context=None):
1079         """ Make purchase order from procurement
1080         @return: New created Purchase Orders procurement wise
1081         """
1082         res = {}
1083         if context is None:
1084             context = {}
1085         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
1086         partner_obj = self.pool.get('res.partner')
1087         uom_obj = self.pool.get('product.uom')
1088         pricelist_obj = self.pool.get('product.pricelist')
1089         prod_obj = self.pool.get('product.product')
1090         acc_pos_obj = self.pool.get('account.fiscal.position')
1091         seq_obj = self.pool.get('ir.sequence')
1092         warehouse_obj = self.pool.get('stock.warehouse')
1093         for procurement in self.browse(cr, uid, ids, context=context):
1094             res_id = procurement.move_id.id
1095             partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
1096             seller_qty = procurement.product_id.seller_qty
1097             partner_id = partner.id
1098             address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1099             pricelist_id = partner.property_product_pricelist_purchase.id
1100             warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
1101             uom_id = procurement.product_id.uom_po_id.id
1102
1103             qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1104             if seller_qty:
1105                 qty = max(qty,seller_qty)
1106
1107             price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
1108
1109             schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
1110             purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
1111
1112             #Passing partner_id to context for purchase order line integrity of Line name
1113             new_context = context.copy()
1114             new_context.update({'lang': partner.lang, 'partner_id': partner_id})
1115
1116             product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
1117             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1118             taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
1119
1120             name = product.partner_ref
1121             if product.description_purchase:
1122                 name += '\n'+ product.description_purchase
1123             line_vals = {
1124                 'name': name,
1125                 'product_qty': qty,
1126                 'product_id': procurement.product_id.id,
1127                 'product_uom': uom_id,
1128                 'price_unit': price or 0.0,
1129                 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1130                 'move_dest_id': res_id,
1131                 'taxes_id': [(6,0,taxes)],
1132             }
1133             name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
1134             po_vals = {
1135                 'name': name,
1136                 'origin': procurement.origin,
1137                 'partner_id': partner_id,
1138                 'location_id': procurement.location_id.id,
1139                 'warehouse_id': warehouse_id and warehouse_id[0] or False,
1140                 'pricelist_id': pricelist_id,
1141                 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
1142                 'company_id': procurement.company_id.id,
1143                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1144             }
1145             res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=new_context)
1146             self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]})
1147         self.purchase_order_create_note(cr, uid, ids, context=context)
1148         return res
1149     
1150     def _product_virtual_get(self, cr, uid, order_point):
1151         procurement = order_point.procurement_id
1152         if procurement and procurement.state != 'exception' and procurement.purchase_id and procurement.purchase_id.state in ('draft', 'confirmed'):
1153             return None
1154         return super(procurement_order, self)._product_virtual_get(cr, uid, order_point)
1155
1156     def purchase_order_create_note(self, cr, uid, ids, context=None):
1157         for procurement in self.browse(cr, uid, ids, context=context):
1158             body = _("Draft Purchase Order created")
1159             self.message_post(cr, uid, [procurement.id], body=body, context=context)
1160
1161 procurement_order()
1162
1163 class mail_mail(osv.osv):
1164     _name = 'mail.mail'
1165     _inherit = 'mail.mail'
1166
1167     def _postprocess_sent_message(self, cr, uid, mail, context=None):
1168         if mail.model == 'purchase.order':
1169             wf_service = netsvc.LocalService("workflow")
1170             wf_service.trg_validate(uid, 'purchase.order', mail.res_id, 'send_rfq', cr)
1171         return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
1172
1173 mail_mail()
1174
1175 class product_template(osv.osv):
1176     _name = 'product.template'
1177     _inherit = 'product.template'
1178     _columns = {
1179         'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
1180     }
1181     _defaults = {
1182         'purchase_ok': 1,
1183     }
1184
1185 product_template()
1186
1187 class mail_compose_message(osv.osv):
1188     _inherit = 'mail.compose.message'
1189     def send_mail(self, cr, uid, ids, context=None):
1190         context = context or {}
1191         if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
1192             wf_service = netsvc.LocalService("workflow")
1193             wf_service.trg_validate(uid, 'purchase.order', context['default_res_id'], 'send_rfq', cr)
1194         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1195
1196 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: