[MERGE] merge with lp:openobject-addons
[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 state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the processing of a document linked to the sale order. \nThe 'Waiting Schedule' state 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         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
720         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
721         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
722         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
723         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
724         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
725         'product_uos': fields.many2one('product.uom', 'Product UoS'),
726         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
727         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
728         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
729                 help='* The \'Draft\' state is set when the related sales order in draft state. \
730                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
731                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
732                     \n* The \'Done\' state is set when the sales order line has been picked. \
733                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
734         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
735         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
736         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
737     }
738     _order = 'sequence, id'
739     _defaults = {
740         'product_uom' : _get_uom_id,
741         'discount': 0.0,
742         'product_uom_qty': 1,
743         'product_uos_qty': 1,
744         'sequence': 10,
745         'invoiced': 0,
746         'state': 'draft',
747         'price_unit': 0.0,
748     }
749
750     def _get_line_qty(self, cr, uid, line, context=None):
751         if (line.order_id.invoice_quantity=='order'):
752             if line.product_uos:
753                 return line.product_uos_qty or 0.0
754         return line.product_uom_qty
755
756     def _get_line_uom(self, cr, uid, line, context=None):
757         if (line.order_id.invoice_quantity=='order'):
758             if line.product_uos:
759                 return line.product_uos.id
760         return line.product_uom.id
761
762     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
763         """Prepare the dict of values to create the new invoice line for a
764            sale order line. This method may be overridden to implement custom
765            invoice generation (making sure to call super() to establish
766            a clean extension chain).
767
768            :param browse_record line: sale.order.line record to invoice
769            :param int account_id: optional ID of a G/L account to force
770                (this is used for returning products including service)
771            :return: dict of values to create() the invoice line
772         """
773         res = {}
774         if not line.invoiced:
775             if not account_id:
776                 if line.product_id:
777                     account_id = line.product_id.product_tmpl_id.property_account_income.id
778                     if not account_id:
779                         account_id = line.product_id.categ_id.property_account_income_categ.id
780                     if not account_id:
781                         raise osv.except_osv(_('Error!'),
782                                 _('Please define income account for this product: "%s" (id:%d).') % \
783                                     (line.product_id.name, line.product_id.id,))
784                 else:
785                     prop = self.pool.get('ir.property').get(cr, uid,
786                             'property_account_income_categ', 'product.category',
787                             context=context)
788                     account_id = prop and prop.id or False
789             uosqty = self._get_line_qty(cr, uid, line, context=context)
790             uos_id = self._get_line_uom(cr, uid, line, context=context)
791             pu = 0.0
792             if uosqty:
793                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
794                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
795             fpos = line.order_id.fiscal_position or False
796             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
797             if not account_id:
798                 raise osv.except_osv(_('Error!'),
799                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
800             res = {
801                 'name': line.name,
802                 'sequence': line.sequence,
803                 'origin': line.order_id.name,
804                 'account_id': account_id,
805                 'price_unit': pu,
806                 'quantity': uosqty,
807                 'discount': line.discount,
808                 'uos_id': uos_id,
809                 'product_id': line.product_id.id or False,
810                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
811                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
812             }
813
814         return res
815
816     def invoice_line_create(self, cr, uid, ids, context=None):
817         if context is None:
818             context = {}
819
820         create_ids = []
821         sales = set()
822         for line in self.browse(cr, uid, ids, context=context):
823             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
824             if vals:
825                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
826                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
827                 self.write(cr, uid, [line.id], {'invoiced': True})
828                 sales.add(line.order_id.id)
829                 create_ids.append(inv_id)
830         # Trigger workflow events
831         wf_service = netsvc.LocalService("workflow")
832         for sale_id in sales:
833             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
834         return create_ids
835
836     def button_cancel(self, cr, uid, ids, context=None):
837         for line in self.browse(cr, uid, ids, context=context):
838             if line.invoiced:
839                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
840         return self.write(cr, uid, ids, {'state': 'cancel'})
841
842     def button_confirm(self, cr, uid, ids, context=None):
843         return self.write(cr, uid, ids, {'state': 'confirmed'})
844
845     def button_done(self, cr, uid, ids, context=None):
846         wf_service = netsvc.LocalService("workflow")
847         res = self.write(cr, uid, ids, {'state': 'done'})
848         for line in self.browse(cr, uid, ids, context=context):
849             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
850         return res
851
852     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
853         product_obj = self.pool.get('product.product')
854         if not product_id:
855             return {'value': {'product_uom': product_uos,
856                 'product_uom_qty': product_uos_qty}, 'domain': {}}
857
858         product = product_obj.browse(cr, uid, product_id)
859         value = {
860             'product_uom': product.uom_id.id,
861         }
862         # FIXME must depend on uos/uom of the product and not only of the coeff.
863         try:
864             value.update({
865                 'product_uom_qty': product_uos_qty / product.uos_coeff,
866                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
867             })
868         except ZeroDivisionError:
869             pass
870         return {'value': value}
871
872     def copy_data(self, cr, uid, id, default=None, context=None):
873         if not default:
874             default = {}
875         default.update({'state': 'draft',  'invoiced': False, 'invoice_lines': []})
876         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
877
878     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
879             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
880             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
881         context = context or {}
882         lang = lang or context.get('lang',False)
883         if not  partner_id:
884             raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
885         warning = {}
886         product_uom_obj = self.pool.get('product.uom')
887         partner_obj = self.pool.get('res.partner')
888         product_obj = self.pool.get('product.product')
889         context = {'lang': lang, 'partner_id': partner_id}
890         if partner_id:
891             lang = partner_obj.browse(cr, uid, partner_id).lang
892         context_partner = {'lang': lang, 'partner_id': partner_id}
893
894         if not product:
895             return {'value': {'th_weight': 0,
896                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
897                    'product_uos': []}}
898         if not date_order:
899             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
900
901         result = {}
902         warning_msgs = {}
903         product_obj = product_obj.browse(cr, uid, product, context=context)
904
905         uom2 = False
906         if uom:
907             uom2 = product_uom_obj.browse(cr, uid, uom)
908             if product_obj.uom_id.category_id.id != uom2.category_id.id:
909                 uom = False
910         if uos:
911             if product_obj.uos_id:
912                 uos2 = product_uom_obj.browse(cr, uid, uos)
913                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
914                     uos = False
915             else:
916                 uos = False
917         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
918         if update_tax: #The quantity only have changed
919             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
920
921         if not flag:
922             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
923             if product_obj.description_sale:
924                 result['name'] += '\n'+product_obj.description_sale
925         domain = {}
926         if (not uom) and (not uos):
927             result['product_uom'] = product_obj.uom_id.id
928             if product_obj.uos_id:
929                 result['product_uos'] = product_obj.uos_id.id
930                 result['product_uos_qty'] = qty * product_obj.uos_coeff
931                 uos_category_id = product_obj.uos_id.category_id.id
932             else:
933                 result['product_uos'] = False
934                 result['product_uos_qty'] = qty
935                 uos_category_id = False
936             result['th_weight'] = qty * product_obj.weight
937             domain = {'product_uom':
938                         [('category_id', '=', product_obj.uom_id.category_id.id)],
939                         'product_uos':
940                         [('category_id', '=', uos_category_id)]}
941         elif uos and not uom: # only happens if uom is False
942             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
943             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
944             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
945         elif uom: # whether uos is set or not
946             default_uom = product_obj.uom_id and product_obj.uom_id.id
947             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
948             if product_obj.uos_id:
949                 result['product_uos'] = product_obj.uos_id.id
950                 result['product_uos_qty'] = qty * product_obj.uos_coeff
951             else:
952                 result['product_uom'] = default_uom
953                 result['product_uos'] = False
954                 result['product_uos_qty'] = qty
955             result['th_weight'] = q * product_obj.weight        # Round the quantity up
956
957         if not uom2:
958             uom2 = product_obj.uom_id
959         # get unit price
960
961         if not pricelist:
962             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
963                     'Please set one before choosing a product.')
964             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
965         else:
966             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
967                     product, qty or 1.0, partner_id, {
968                         'uom': uom or result.get('product_uom'),
969                         'date': date_order,
970                         })[pricelist]
971             if price is False:
972                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
973                         "You have to change either the product, the quantity or the pricelist.")
974
975                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
976             else:
977                 result.update({'price_unit': price})
978         if warning_msgs:
979             warning = {
980                        'title': _('Configuration Error!'),
981                        'message' : warning_msgs
982                     }
983         return {'value': result, 'domain': domain, 'warning': warning}
984
985     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
986             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
987             lang=False, update_tax=True, date_order=False, context=None):
988         context = context or {}
989         lang = lang or ('lang' in context and context['lang'])
990         if not uom:
991             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
992         return self.product_id_change(cursor, user, ids, pricelist, product,
993                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
994                 partner_id=partner_id, lang=lang, update_tax=update_tax,
995                 date_order=date_order, context=context)
996
997     def unlink(self, cr, uid, ids, context=None):
998         if context is None:
999             context = {}
1000         """Allows to delete sales order lines in draft,cancel states"""
1001         for rec in self.browse(cr, uid, ids, context=context):
1002             if rec.state not in ['draft', 'cancel']:
1003                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1004         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1005
1006 sale_order_line()
1007
1008 class mail_compose_message(osv.osv):
1009     _inherit = 'mail.compose.message'
1010     def send_mail(self, cr, uid, ids, context=None):
1011         context = context or {}
1012         if context.get('mark_so_as_sent', False) and context.get('default_res_id', False):
1013             wf_service = netsvc.LocalService("workflow")
1014             wf_service.trg_validate(uid, 'sale.order', context.get('default_res_id', False), 'quotation_sent', cr)
1015         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1016
1017 mail_compose_message()
1018
1019 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: