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