[IMP]sale:updated code for on change of product_id
[odoo/odoo.git] / addons / sale / sale.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 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
24 import time
25 import pooler
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import decimal_precision as dp
30 import netsvc
31
32 class sale_shop(osv.osv):
33     _name = "sale.shop"
34     _description = "Sales Shop"
35     _columns = {
36         'name': fields.char('Shop Name', size=64, required=True),
37         'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39         'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
40         'company_id': fields.many2one('res.company', 'Company', required=False),
41     }
42     _defaults = {
43         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
44     }
45
46 sale_shop()
47
48 class sale_order(osv.osv):
49     _name = "sale.order"
50     _inherit = ['mail.thread', 'ir.needaction_mixin']
51     _description = "Sales Order"
52
53     def onchange_shop_id(self, cr, uid, ids, shop_id, context=None):
54         v = {}
55         if shop_id:
56             shop = self.pool.get('sale.shop').browse(cr, uid, shop_id, context=context)
57             if shop.project_id.id:
58                 v['project_id'] = shop.project_id.id
59             if shop.pricelist_id.id:
60                 v['pricelist_id'] = shop.pricelist_id.id
61         return {'value': v}
62
63     def copy(self, cr, uid, id, default=None, context=None):
64         if not default:
65             default = {}
66         default.update({
67             'state': 'draft',
68             'invoice_ids': [],
69             'date_confirm': False,
70             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
71         })
72         return super(sale_order, self).copy(cr, uid, id, default, context=context)
73
74     def _amount_line_tax(self, cr, uid, line, context=None):
75         val = 0.0
76         for c in self.pool.get('account.tax').compute_all(cr, uid, line.tax_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.product_uom_qty, line.product_id, line.order_id.partner_id)['taxes']:
77             val += c.get('amount', 0.0)
78         return val
79
80     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
81         cur_obj = self.pool.get('res.currency')
82         res = {}
83         for order in self.browse(cr, uid, ids, context=context):
84             res[order.id] = {
85                 'amount_untaxed': 0.0,
86                 'amount_tax': 0.0,
87                 'amount_total': 0.0,
88             }
89             val = val1 = 0.0
90             cur = order.pricelist_id.currency_id
91             for line in order.order_line:
92                 val1 += line.price_subtotal
93                 val += self._amount_line_tax(cr, uid, line, context=context)
94             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
95             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
96             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
97         return res
98
99
100     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
101         res = {}
102         for sale in self.browse(cursor, user, ids, context=context):
103             if sale.invoiced:
104                 res[sale.id] = 100.0
105                 continue
106             tot = 0.0
107             for invoice in sale.invoice_ids:
108                 if invoice.state not in ('draft', 'cancel'):
109                     tot += invoice.amount_untaxed
110             if tot:
111                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
112             else:
113                 res[sale.id] = 0.0
114         return res
115
116     def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
117         res = {}
118         for sale in self.browse(cursor, user, ids, context=context):
119             res[sale.id] = False
120             if sale.invoice_ids:
121                 res[sale.id] = True
122         return res
123
124     def _invoiced(self, cursor, user, ids, name, arg, context=None):
125         res = {}
126         for sale in self.browse(cursor, user, ids, context=context):
127             res[sale.id] = True
128             invoice_existence = False
129             for invoice in sale.invoice_ids:
130                 if invoice.state!='cancel':
131                     invoice_existence = True
132                     if invoice.state != 'paid':
133                         res[sale.id] = False
134                         break
135             if not invoice_existence or sale.state == 'manual':
136                 res[sale.id] = False
137         return res
138
139     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
140         if not len(args):
141             return []
142         clause = ''
143         sale_clause = ''
144         no_invoiced = False
145         for arg in args:
146             if arg[1] == '=':
147                 if arg[2]:
148                     clause += 'AND inv.state = \'paid\''
149                 else:
150                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
151                     sale_clause = ',  sale_order AS sale '
152                     no_invoiced = True
153
154         cursor.execute('SELECT rel.order_id ' \
155                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
156                 'WHERE rel.invoice_id = inv.id ' + clause)
157         res = cursor.fetchall()
158         if no_invoiced:
159             cursor.execute('SELECT sale.id ' \
160                     'FROM sale_order AS sale ' \
161                     'WHERE sale.id NOT IN ' \
162                         '(SELECT rel.order_id ' \
163                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
164             res.extend(cursor.fetchall())
165         if not res:
166             return [('id', '=', 0)]
167         return [('id', 'in', [x[0] for x in res])]
168
169     def _get_order(self, cr, uid, ids, context=None):
170         result = {}
171         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
172             result[line.order_id.id] = True
173         return result.keys()
174
175     _columns = {
176         'name': fields.char('Order Reference', size=64, required=True,
177             readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
178         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
179         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
180         'client_order_ref': fields.char('Customer Reference', size=64),
181         'state': fields.selection([
182             ('draft', 'Draft Quotation'),
183             ('sent', 'Quotation Sent'),
184             ('cancel', 'Cancelled'),
185             ('waiting_date', 'Waiting Schedule'),
186             ('progress', 'Sale Order'),
187             ('manual', 'Sale to Invoice'),
188             ('invoice_except', 'Invoice Exception'),
189             ('done', 'Done'),
190             ], 'Status', readonly=True, help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sale order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
191         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
193         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
194         'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
195         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
196         'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."),
197         'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."),
198         'order_policy': fields.selection([
199                 ('manual', 'On Demand'),
200             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
201             help="""This field controls how invoice and delivery operations are synchronized.
202   - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
203         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
204         'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", readonly=True, required=True),
205         'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
206
207         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
208         'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
209         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
210         'invoiced': fields.function(_invoiced, string='Paid',
211             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
212         'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
213             fnct_search=_invoiced_search, type='boolean', help="It indicates that sale order has at least one invoice."),
214         'note': fields.text('Terms and conditions'),
215
216         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Untaxed Amount',
217             store = {
218                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
219                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
220             },
221             multi='sums', help="The amount without tax."),
222         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Taxes',
223             store = {
224                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
225                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
226             },
227             multi='sums', help="The tax amount."),
228         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Total',
229             store = {
230                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
231                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
232             },
233             multi='sums', help="The total amount."),
234
235         'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
236         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
237         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
238         'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
239     }
240     _defaults = {
241         'date_order': fields.date.context_today,
242         'order_policy': 'manual',
243         'state': 'draft',
244         'user_id': lambda obj, cr, uid, context: uid,
245         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
246         'invoice_quantity': 'order',
247         'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'],
248         'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
249     }
250     _sql_constraints = [
251         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
252     ]
253     _order = 'name desc'
254
255     # Form filling
256     def unlink(self, cr, uid, ids, context=None):
257         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
258         unlink_ids = []
259         for s in sale_orders:
260             if s['state'] in ['draft', 'cancel']:
261                 unlink_ids.append(s['id'])
262             else:
263                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sale order, you must cancel it before !'))
264
265         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
266
267     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
268         if not pricelist_id:
269             return {}
270         value = {
271             'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
272         }
273         if not order_lines:
274             return {'value': value}
275         warning = {
276             'title': _('Pricelist Warning!'),
277             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
278         }
279         return {'warning': warning, 'value': value}
280
281     def onchange_partner_id(self, cr, uid, ids, part):
282         if not part:
283             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
284
285         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
286         part = self.pool.get('res.partner').browse(cr, uid, part)
287         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
288         payment_term = part.property_payment_term and part.property_payment_term.id or False
289         fiscal_position = part.property_account_position and part.property_account_position.id or False
290         dedicated_salesman = part.user_id and part.user_id.id or uid
291         val = {
292             'partner_invoice_id': addr['invoice'],
293             'partner_shipping_id': addr['delivery'],
294             'payment_term': payment_term,
295             'fiscal_position': fiscal_position,
296             'user_id': dedicated_salesman,
297         }
298         if pricelist:
299             val['pricelist_id'] = pricelist
300         return {'value': val}
301
302     def create(self, cr, uid, vals, context=None):
303         order =  super(sale_order, self).create(cr, uid, vals, context=context)
304         if order:
305             self.create_send_note(cr, uid, [order], context=context)
306         return order
307
308     def button_dummy(self, cr, uid, ids, context=None):
309         return True
310
311     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
312     #        can be removed after 6.1.
313     def _inv_get(self, cr, uid, order, context=None):
314         return {}
315
316     def _prepare_invoice(self, cr, uid, order, lines, context=None):
317         """Prepare the dict of values to create the new invoice for a
318            sale order. This method may be overridden to implement custom
319            invoice generation (making sure to call super() to establish
320            a clean extension chain).
321
322            :param browse_record order: sale.order record to invoice
323            :param list(int) line: list of invoice line IDs that must be
324                                   attached to the invoice
325            :return: dict of value to create() the invoice
326         """
327         if context is None:
328             context = {}
329         journal_ids = self.pool.get('account.journal').search(cr, uid,
330             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
331             limit=1)
332         if not journal_ids:
333             raise osv.except_osv(_('Error!'),
334                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
335         invoice_vals = {
336             'name': order.client_order_ref or '',
337             'origin': order.name,
338             'type': 'out_invoice',
339             'reference': order.client_order_ref or order.name,
340             'account_id': order.partner_id.property_account_receivable.id,
341             'partner_id': order.partner_id.id,
342             'journal_id': journal_ids[0],
343             'invoice_line': [(6, 0, lines)],
344             'currency_id': order.pricelist_id.currency_id.id,
345             'comment': order.note,
346             'payment_term': order.payment_term and order.payment_term.id or False,
347             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
348             'date_invoice': context.get('date_invoice', False),
349             'company_id': order.company_id.id,
350             'user_id': order.user_id and order.user_id.id or False
351         }
352
353         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
354         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
355         return invoice_vals
356
357     def _make_invoice(self, cr, uid, order, lines, context=None):
358         inv_obj = self.pool.get('account.invoice')
359         obj_invoice_line = self.pool.get('account.invoice.line')
360         if context is None:
361             context = {}
362         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
363         from_line_invoice_ids = []
364         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
365             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
366                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
367                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
368         for preinv in order.invoice_ids:
369             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
370                 for preline in preinv.invoice_line:
371                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
372                     lines.append(inv_line_id)
373         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
374         inv_id = inv_obj.create(cr, uid, inv, context=context)
375         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
376         if data.get('value', False):
377             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
378         inv_obj.button_compute(cr, uid, [inv_id])
379         return inv_id
380
381     def print_quotation(self, cr, uid, ids, context=None):
382         '''
383         This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
384         '''
385         assert len(ids) == 1, 'This option should only be used for a single id at a time'
386         wf_service = netsvc.LocalService("workflow")
387         wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
388         datas = {
389                  'model': 'sale.order',
390                  'ids': ids,
391                  'form': self.read(cr, uid, ids[0], context=context),
392         }
393         return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
394
395     def manual_invoice(self, cr, uid, ids, context=None):
396         """ create invoices for the given sale orders (ids), and open the form
397             view of one of the newly created invoices
398         """
399         mod_obj = self.pool.get('ir.model.data')
400         wf_service = netsvc.LocalService("workflow")
401
402         # create invoices through the sale orders' workflow
403         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
404         for id in ids:
405             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
406         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
407         # determine newly created invoices
408         new_inv_ids = list(inv_ids1 - inv_ids0)
409
410         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
411         res_id = res and res[1] or False,
412
413         return {
414             'name': _('Customer Invoices'),
415             'view_type': 'form',
416             'view_mode': 'form',
417             'view_id': [res_id],
418             'res_model': 'account.invoice',
419             'context': "{'type':'out_invoice'}",
420             'type': 'ir.actions.act_window',
421             'nodestroy': True,
422             'target': 'current',
423             'res_id': new_inv_ids and new_inv_ids[0] or False,
424         }
425
426     def action_view_invoice(self, cr, uid, ids, context=None):
427         '''
428         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.
429         '''
430         mod_obj = self.pool.get('ir.model.data')
431         act_obj = self.pool.get('ir.actions.act_window')
432
433         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
434         id = result and result[1] or False
435         result = act_obj.read(cr, uid, [id], context=context)[0]
436         #compute the number of invoices to display
437         inv_ids = []
438         for so in self.browse(cr, uid, ids, context=context):
439             inv_ids += [invoice.id for invoice in so.invoice_ids]
440         #choose the view_mode accordingly
441         if len(inv_ids)>1:
442             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
443         else:
444             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
445             result['views'] = [(res and res[1] or False, 'form')]
446             result['res_id'] = inv_ids and inv_ids[0] or False
447         return result
448
449     def test_no_product(self, cr, uid, order, context):
450         for line in order.order_line:
451             if line.product_id and (line.product_id.type<>'service'):
452                 return False
453         return True
454
455     def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_inv = False, context=None):
456         if states is None:
457             states = ['confirmed', 'done', 'exception']
458         res = False
459         invoices = {}
460         invoice_ids = []
461         invoice = self.pool.get('account.invoice')
462         obj_sale_order_line = self.pool.get('sale.order.line')
463         partner_currency = {}
464         if context is None:
465             context = {}
466         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
467         # last day of the last month as invoice date
468         if date_inv:
469             context['date_inv'] = date_inv
470         for o in self.browse(cr, uid, ids, context=context):
471             currency_id = o.pricelist_id.currency_id.id
472             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
473                 raise osv.except_osv(
474                     _('Error!'),
475                     _('You cannot group sales having different currencies for the same partner.'))
476
477             partner_currency[o.partner_id.id] = currency_id
478             lines = []
479             for line in o.order_line:
480                 if line.invoiced:
481                     continue
482                 elif (line.state in states):
483                     lines.append(line.id)
484             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
485             if created_lines:
486                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
487         if not invoices:
488             for o in self.browse(cr, uid, ids, context=context):
489                 for i in o.invoice_ids:
490                     if i.state == 'draft':
491                         return i.id
492         for val in invoices.values():
493             if grouped:
494                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
495                 invoice_ref = ''
496                 for o, l in val:
497                     invoice_ref += o.name + '|'
498                     self.write(cr, uid, [o.id], {'state': 'progress'})
499                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
500                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
501             else:
502                 for order, il in val:
503                     res = self._make_invoice(cr, uid, order, il, context=context)
504                     invoice_ids.append(res)
505                     self.write(cr, uid, [order.id], {'state': 'progress'})
506                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
507         if res:
508             self.invoice_send_note(cr, uid, ids, res, context)
509         return res
510
511     def action_invoice_cancel(self, cr, uid, ids, context=None):
512         if context is None:
513             context = {}
514         for sale in self.browse(cr, uid, ids, context=context):
515             for line in sale.order_line:
516                 #
517                 # Check if the line is invoiced (has asociated invoice
518                 # lines from non-cancelled invoices).
519                 #
520                 invoiced = False
521                 for iline in line.invoice_lines:
522                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
523                         invoiced = True
524                         break
525                 # Update the line (only when needed)
526                 if line.invoiced != invoiced:
527                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
528         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
529         return True
530
531     def action_invoice_end(self, cr, uid, ids, context=None):
532         for order in self.browse(cr, uid, ids, context=context):
533             #
534             # Update the sale order lines state (and invoiced flag).
535             #
536             for line in order.order_line:
537                 vals = {}
538                 #
539                 # Check if the line is invoiced (has asociated invoice
540                 # lines from non-cancelled invoices).
541                 #
542                 invoiced = False
543                 for iline in line.invoice_lines:
544                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
545                         invoiced = True
546                         break
547                 if line.invoiced != invoiced:
548                     vals['invoiced'] = invoiced
549                 # If the line was in exception state, now it gets confirmed.
550                 if line.state == 'exception':
551                     vals['state'] = 'confirmed'
552                 # Update the line (only when needed).
553                 if vals:
554                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
555             #
556             # Update the sales order state.
557             #
558             if order.state == 'invoice_except':
559                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
560             self.invoice_paid_send_note(cr, uid, [order.id], context=context)
561         return True
562
563     def action_cancel(self, cr, uid, ids, context=None):
564         wf_service = netsvc.LocalService("workflow")
565         if context is None:
566             context = {}
567         sale_order_line_obj = self.pool.get('sale.order.line')
568         for sale in self.browse(cr, uid, ids, context=context):
569             for inv in sale.invoice_ids:
570                 if inv.state not in ('draft', 'cancel'):
571                     raise osv.except_osv(
572                         _('Cannot cancel this sales order!'),
573                         _('First cancel all invoices attached to this sales order.'))
574             for r in self.read(cr, uid, ids, ['invoice_ids']):
575                 for inv in r['invoice_ids']:
576                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
577             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
578                     {'state': 'cancel'})
579             self.cancel_send_note(cr, uid, [sale.id], context=None)
580         self.write(cr, uid, ids, {'state': 'cancel'})
581         return True
582
583     def action_button_confirm(self, cr, uid, ids, context=None):
584         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
585         wf_service = netsvc.LocalService('workflow')
586         wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
587
588         # redisplay the record as a sale order
589         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
590         view_id = view_ref and view_ref[1] or False,
591         return {
592             'type': 'ir.actions.act_window',
593             'name': _('Sales Order'),
594             'res_model': 'sale.order',
595             'res_id': ids[0],
596             'view_type': 'form',
597             'view_mode': 'form',
598             'view_id': view_id,
599             'target': 'current',
600             'nodestroy': True,
601         }
602
603     def action_wait(self, cr, uid, ids, context=None):
604         for o in self.browse(cr, uid, ids):
605             if not o.order_line:
606                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
607             noprod = self.test_no_product(cr, uid, o, context)
608             if (o.order_policy == 'manual') or noprod:
609                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
610             else:
611                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
612             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
613             self.confirm_send_note(cr, uid, ids, context)
614         return True
615
616     def action_quotation_send(self, cr, uid, ids, context=None):
617         '''
618         This function opens a window to compose an email, with the edi sale template message loaded by default
619         '''
620         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
621         mod_obj = self.pool.get('ir.model.data')
622         template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
623         template_id = template and template[1] or False
624         res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
625         res_id = res and res[1] or False
626         ctx = dict(context)
627         ctx.update({
628             'default_model': 'sale.order',
629             'default_res_id': ids[0],
630             'default_use_template': True,
631             'default_template_id': template_id,
632             'mark_so_as_sent': True
633         })
634         return {
635             'view_type': 'form',
636             'view_mode': 'form',
637             'res_model': 'mail.compose.message',
638             'views': [(res_id, 'form')],
639             'view_id': res_id,
640             'type': 'ir.actions.act_window',
641             'target': 'new',
642             'context': ctx,
643             'nodestroy': True,
644         }
645
646     def action_done(self, cr, uid, ids, context=None):
647         self.done_send_note(cr, uid, ids, context=context)
648         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
649
650     # ------------------------------------------------
651     # OpenChatter methods and notifications
652     # ------------------------------------------------
653
654     def needaction_domain_get(self, cr, uid, ids, context=None):
655         return [('state', '=', 'draft'), ('user_id','=',uid)]
656
657     def create_send_note(self, cr, uid, ids, context=None):
658         for obj in self.browse(cr, uid, ids, context=context):
659             self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
660
661     def confirm_send_note(self, cr, uid, ids, context=None):
662         for obj in self.browse(cr, uid, ids, context=context):
663             self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
664
665     def cancel_send_note(self, cr, uid, ids, context=None):
666         for obj in self.browse(cr, uid, ids, context=context):
667             self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
668
669     def done_send_note(self, cr, uid, ids, context=None):
670         for obj in self.browse(cr, uid, ids, context=context):
671             self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> set to <b>Done</b>") % (obj.partner_id.name), context=context)
672
673     def invoice_paid_send_note(self, cr, uid, ids, context=None):
674         self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
675
676     def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
677         for order in self.browse(cr, uid, ids, context=context):
678             for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
679                 self.message_post(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
680
681 sale_order()
682
683 # TODO add a field price_unit_uos
684 # - update it on change product and unit price
685 # - use it in report if there is a uos
686 class sale_order_line(osv.osv):
687
688     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
689         tax_obj = self.pool.get('account.tax')
690         cur_obj = self.pool.get('res.currency')
691         res = {}
692         if context is None:
693             context = {}
694         for line in self.browse(cr, uid, ids, context=context):
695             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
696             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
697             cur = line.order_id.pricelist_id.currency_id
698             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
699         return res
700
701     def _get_uom_id(self, cr, uid, *args):
702         try:
703             proxy = self.pool.get('ir.model.data')
704             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
705             return result[1]
706         except Exception, ex:
707             return False
708
709     _name = 'sale.order.line'
710     _description = 'Sales Order Line'
711     _columns = {
712         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
713         'name': fields.text('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
714         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
715         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
716         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
717         'invoiced': fields.boolean('Invoiced', readonly=True),
718         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
719         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
720          help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
721         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
722         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
723         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
724         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
725         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
726         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
727         'product_uos': fields.many2one('product.uom', 'Product UoS'),
728         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
729         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
730         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
731                 help='* The \'Draft\' status is set when the related sales order in draft status. \
732                     \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
733                     \n* The \'Exception\' status is set when the related sales order is set as exception. \
734                     \n* The \'Done\' status is set when the sales order line has been picked. \
735                     \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
736         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
737         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
738         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
739     }
740     _order = 'sequence, id'
741     _defaults = {
742         'product_uom' : _get_uom_id,
743         'discount': 0.0,
744         'product_uom_qty': 1,
745         'product_uos_qty': 1,
746         'sequence': 10,
747         'invoiced': 0,
748         'state': 'draft',
749         'type': 'make_to_stock',
750         'price_unit': 0.0,
751     }
752
753     def _get_line_qty(self, cr, uid, line, context=None):
754         if (line.order_id.invoice_quantity=='order'):
755             if line.product_uos:
756                 return line.product_uos_qty or 0.0
757         return line.product_uom_qty
758
759     def _get_line_uom(self, cr, uid, line, context=None):
760         if (line.order_id.invoice_quantity=='order'):
761             if line.product_uos:
762                 return line.product_uos.id
763         return line.product_uom.id
764
765     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
766         """Prepare the dict of values to create the new invoice line for a
767            sale order line. This method may be overridden to implement custom
768            invoice generation (making sure to call super() to establish
769            a clean extension chain).
770
771            :param browse_record line: sale.order.line record to invoice
772            :param int account_id: optional ID of a G/L account to force
773                (this is used for returning products including service)
774            :return: dict of values to create() the invoice line
775         """
776         res = {}
777         if not line.invoiced:
778             if not account_id:
779                 if line.product_id:
780                     account_id = line.product_id.product_tmpl_id.property_account_income.id
781                     if not account_id:
782                         account_id = line.product_id.categ_id.property_account_income_categ.id
783                     if not account_id:
784                         raise osv.except_osv(_('Error!'),
785                                 _('Please define income account for this product: "%s" (id:%d).') % \
786                                     (line.product_id.name, line.product_id.id,))
787                 else:
788                     prop = self.pool.get('ir.property').get(cr, uid,
789                             'property_account_income_categ', 'product.category',
790                             context=context)
791                     account_id = prop and prop.id or False
792             uosqty = self._get_line_qty(cr, uid, line, context=context)
793             uos_id = self._get_line_uom(cr, uid, line, context=context)
794             pu = 0.0
795             if uosqty:
796                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
797                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
798             fpos = line.order_id.fiscal_position or False
799             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
800             if not account_id:
801                 raise osv.except_osv(_('Error!'),
802                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
803             res = {
804                 'name': line.name,
805                 'sequence': line.sequence,
806                 'origin': line.order_id.name,
807                 'account_id': account_id,
808                 'price_unit': pu,
809                 'quantity': uosqty,
810                 'discount': line.discount,
811                 'uos_id': uos_id,
812                 'product_id': line.product_id.id or False,
813                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
814                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
815             }
816
817         return res
818
819     def invoice_line_create(self, cr, uid, ids, context=None):
820         if context is None:
821             context = {}
822
823         create_ids = []
824         sales = set()
825         for line in self.browse(cr, uid, ids, context=context):
826             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
827             if vals:
828                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
829                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
830                 self.write(cr, uid, [line.id], {'invoiced': True})
831                 sales.add(line.order_id.id)
832                 create_ids.append(inv_id)
833         # Trigger workflow events
834         wf_service = netsvc.LocalService("workflow")
835         for sale_id in sales:
836             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
837         return create_ids
838
839     def button_cancel(self, cr, uid, ids, context=None):
840         for line in self.browse(cr, uid, ids, context=context):
841             if line.invoiced:
842                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
843         return self.write(cr, uid, ids, {'state': 'cancel'})
844
845     def button_confirm(self, cr, uid, ids, context=None):
846         return self.write(cr, uid, ids, {'state': 'confirmed'})
847
848     def button_done(self, cr, uid, ids, context=None):
849         wf_service = netsvc.LocalService("workflow")
850         res = self.write(cr, uid, ids, {'state': 'done'})
851         for line in self.browse(cr, uid, ids, context=context):
852             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
853         return res
854
855     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
856         product_obj = self.pool.get('product.product')
857         if not product_id:
858             return {'value': {'product_uom': product_uos,
859                 'product_uom_qty': product_uos_qty}, 'domain': {}}
860
861         product = product_obj.browse(cr, uid, product_id)
862         value = {
863             'product_uom': product.uom_id.id,
864         }
865         # FIXME must depend on uos/uom of the product and not only of the coeff.
866         try:
867             value.update({
868                 'product_uom_qty': product_uos_qty / product.uos_coeff,
869                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
870             })
871         except ZeroDivisionError:
872             pass
873         return {'value': value}
874
875     def copy_data(self, cr, uid, id, default=None, context=None):
876         if not default:
877             default = {}
878         default.update({'state': 'draft',  'invoiced': False, 'invoice_lines': []})
879         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
880
881     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
882             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
883             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
884         context = context or {}
885         lang = lang or context.get('lang',False)
886         if not  partner_id:
887             raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
888         warning = {}
889         product_uom_obj = self.pool.get('product.uom')
890         partner_obj = self.pool.get('res.partner')
891         product_obj = self.pool.get('product.product')
892         context = {'lang': lang, 'partner_id': partner_id}
893         if partner_id:
894             lang = partner_obj.browse(cr, uid, partner_id).lang
895         context_partner = {'lang': lang, 'partner_id': partner_id}
896
897         if not product:
898             return {'value': {'th_weight': 0,
899                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
900                    'product_uos': []}}
901         if not date_order:
902             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
903
904         result = {}
905         warning_msgs = {}
906         product_obj = product_obj.browse(cr, uid, product, context=context)
907
908         uom2 = False
909         if uom:
910             uom2 = product_uom_obj.browse(cr, uid, uom)
911             if product_obj.uom_id.category_id.id != uom2.category_id.id:
912                 uom = False
913         if uos:
914             if product_obj.uos_id:
915                 uos2 = product_uom_obj.browse(cr, uid, uos)
916                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
917                     uos = False
918             else:
919                 uos = False
920         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
921         if update_tax: #The quantity only have changed
922             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
923
924         if not flag:
925             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
926             if product_obj.description_sale:
927                 result['name'] += '\n'+product_obj.description_sale
928         domain = {}
929         if (not uom) and (not uos):
930             result['product_uom'] = product_obj.uom_id.id
931             if product_obj.uos_id:
932                 result['product_uos'] = product_obj.uos_id.id
933                 result['product_uos_qty'] = qty * product_obj.uos_coeff
934                 uos_category_id = product_obj.uos_id.category_id.id
935             else:
936                 result['product_uos'] = False
937                 result['product_uos_qty'] = qty
938                 uos_category_id = False
939             result['th_weight'] = qty * product_obj.weight
940             domain = {'product_uom':
941                         [('category_id', '=', product_obj.uom_id.category_id.id)],
942                         'product_uos':
943                         [('category_id', '=', uos_category_id)]}
944         elif uos and not uom: # only happens if uom is False
945             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
946             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
947             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
948         elif uom: # whether uos is set or not
949             default_uom = product_obj.uom_id and product_obj.uom_id.id
950             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
951             result['product_uom'] = default_uom
952             if product_obj.uos_id:
953                 result['product_uos'] = product_obj.uos_id.id
954                 result['product_uos_qty'] = qty * product_obj.uos_coeff
955             else:
956                 result['product_uos'] = False
957                 result['product_uos_qty'] = qty
958             result['th_weight'] = q * product_obj.weight        # Round the quantity up
959
960         if not uom2:
961             uom2 = product_obj.uom_id
962         # get unit price
963
964         if not pricelist:
965             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
966                     'Please set one before choosing a product.')
967             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
968         else:
969             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
970                     product, qty or 1.0, partner_id, {
971                         'uom': uom or result.get('product_uom'),
972                         'date': date_order,
973                         })[pricelist]
974             if price is False:
975                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
976                         "You have to change either the product, the quantity or the pricelist.")
977
978                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
979             else:
980                 result.update({'price_unit': price})
981         if warning_msgs:
982             warning = {
983                        'title': _('Configuration Error!'),
984                        'message' : warning_msgs
985                     }
986         return {'value': result, 'domain': domain, 'warning': warning}
987
988     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
989             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
990             lang=False, update_tax=True, date_order=False, context=None):
991         context = context or {}
992         lang = lang or ('lang' in context and context['lang'])
993         if not uom:
994             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
995         return self.product_id_change(cursor, user, ids, pricelist, product,
996                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
997                 partner_id=partner_id, lang=lang, update_tax=update_tax,
998                 date_order=date_order, context=context)
999
1000     def unlink(self, cr, uid, ids, context=None):
1001         if context is None:
1002             context = {}
1003         """Allows to delete sales order lines in draft,cancel states"""
1004         for rec in self.browse(cr, uid, ids, context=context):
1005             if rec.state not in ['draft', 'cancel']:
1006                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1007         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1008
1009 sale_order_line()
1010
1011 class mail_compose_message(osv.osv):
1012     _inherit = 'mail.compose.message'
1013     def send_mail(self, cr, uid, ids, context=None):
1014         context = context or {}
1015         if context.get('mark_so_as_sent', False) and context.get('default_res_id', False):
1016             wf_service = netsvc.LocalService("workflow")
1017             wf_service.trg_validate(uid, 'sale.order', context.get('default_res_id', False), 'quotation_sent', cr)
1018         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1019
1020 mail_compose_message()
1021
1022 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: