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