Launchpad automatic translations update.
[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   - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
217         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
218         'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
219         '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."),
220
221         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
222         '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)."),
223         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
224         'invoiced': fields.function(_invoiced, string='Paid',
225             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
226         'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
227             fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
228         'note': fields.text('Terms and conditions'),
229
230         'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
231             store={
232                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
233                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
234             },
235             multi='sums', help="The amount without tax.", track_visibility='always'),
236         'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
237             store={
238                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
239                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
240             },
241             multi='sums', help="The tax amount."),
242         'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
243             store={
244                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
245                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
246             },
247             multi='sums', help="The total amount."),
248
249         '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)]}),
250         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
251         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
252         'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
253     }
254     _defaults = {
255         'date_order': fields.date.context_today,
256         'order_policy': 'manual',
257         'state': 'draft',
258         'user_id': lambda obj, cr, uid, context: uid,
259         'name': lambda obj, cr, uid, context: '/',
260         'invoice_quantity': 'order',
261         'shop_id': _get_default_shop,
262         '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'],
263         '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'],
264     }
265     _sql_constraints = [
266         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
267     ]
268     _order = 'name desc'
269
270     # Form filling
271     def unlink(self, cr, uid, ids, context=None):
272         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
273         unlink_ids = []
274         for s in sale_orders:
275             if s['state'] in ['draft', 'cancel']:
276                 unlink_ids.append(s['id'])
277             else:
278                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before !'))
279
280         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
281
282     def copy_quotation(self, cr, uid, ids, context=None):
283         id = self.copy(cr, uid, ids[0], context=None)
284         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
285         view_id = view_ref and view_ref[1] or False,
286         return {
287             'type': 'ir.actions.act_window',
288             'name': _('Sales Order'),
289             'res_model': 'sale.order',
290             'res_id': id,
291             'view_type': 'form',
292             'view_mode': 'form',
293             'view_id': view_id,
294             'target': 'current',
295             'nodestroy': True,
296         }
297
298     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
299         context = context or {}
300         if not pricelist_id:
301             return {}
302         value = {
303             'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
304         }
305         if not order_lines:
306             return {'value': value}
307         warning = {
308             'title': _('Pricelist Warning!'),
309             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
310         }
311         return {'warning': warning, 'value': value}
312
313     def onchange_partner_id(self, cr, uid, ids, part, context=None):
314         if not part:
315             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
316
317         part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
318         #if the chosen partner is not a company and has a parent company, use the parent to choose the delivery, the 
319         #invoicing addresses and all the fields related to the partner.
320         if part.parent_id and not part.is_company:
321             part = part.parent_id
322         addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
323         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
324         payment_term = part.property_payment_term and part.property_payment_term.id or False
325         fiscal_position = part.property_account_position and part.property_account_position.id or False
326         dedicated_salesman = part.user_id and part.user_id.id or uid
327         val = {
328             'partner_invoice_id': addr['invoice'],
329             'partner_shipping_id': addr['delivery'],
330             'payment_term': payment_term,
331             'fiscal_position': fiscal_position,
332             'user_id': dedicated_salesman,
333         }
334         if pricelist:
335             val['pricelist_id'] = pricelist
336         return {'value': val}
337
338     def create(self, cr, uid, vals, context=None):
339         if vals.get('name','/')=='/':
340             vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
341         return super(sale_order, self).create(cr, uid, vals, context=context)
342
343     def button_dummy(self, cr, uid, ids, context=None):
344         return True
345
346     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
347     #        can be removed after 6.1.
348     def _inv_get(self, cr, uid, order, context=None):
349         return {}
350
351     def _prepare_invoice(self, cr, uid, order, lines, context=None):
352         """Prepare the dict of values to create the new invoice for a
353            sales order. This method may be overridden to implement custom
354            invoice generation (making sure to call super() to establish
355            a clean extension chain).
356
357            :param browse_record order: sale.order record to invoice
358            :param list(int) line: list of invoice line IDs that must be
359                                   attached to the invoice
360            :return: dict of value to create() the invoice
361         """
362         if context is None:
363             context = {}
364         journal_ids = self.pool.get('account.journal').search(cr, uid,
365             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
366             limit=1)
367         if not journal_ids:
368             raise osv.except_osv(_('Error!'),
369                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
370         invoice_vals = {
371             'name': order.client_order_ref or '',
372             'origin': order.name,
373             'type': 'out_invoice',
374             'reference': order.client_order_ref or order.name,
375             'account_id': order.partner_id.property_account_receivable.id,
376             'partner_id': order.partner_invoice_id.id,
377             'journal_id': journal_ids[0],
378             'invoice_line': [(6, 0, lines)],
379             'currency_id': order.pricelist_id.currency_id.id,
380             'comment': order.note,
381             'payment_term': order.payment_term and order.payment_term.id or False,
382             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
383             'date_invoice': context.get('date_invoice', False),
384             'company_id': order.company_id.id,
385             'user_id': order.user_id and order.user_id.id or False
386         }
387
388         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
389         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
390         return invoice_vals
391
392     def _make_invoice(self, cr, uid, order, lines, context=None):
393         inv_obj = self.pool.get('account.invoice')
394         obj_invoice_line = self.pool.get('account.invoice.line')
395         if context is None:
396             context = {}
397         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
398         from_line_invoice_ids = []
399         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
400             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
401                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
402                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
403         for preinv in order.invoice_ids:
404             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
405                 for preline in preinv.invoice_line:
406                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
407                     lines.append(inv_line_id)
408         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
409         inv_id = inv_obj.create(cr, uid, inv, context=context)
410         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
411         if data.get('value', False):
412             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
413         inv_obj.button_compute(cr, uid, [inv_id])
414         return inv_id
415
416     def print_quotation(self, cr, uid, ids, context=None):
417         '''
418         This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
419         '''
420         assert len(ids) == 1, 'This option should only be used for a single id at a time'
421         wf_service = netsvc.LocalService("workflow")
422         wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
423         datas = {
424                  'model': 'sale.order',
425                  'ids': ids,
426                  'form': self.read(cr, uid, ids[0], context=context),
427         }
428         return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
429
430     def manual_invoice(self, cr, uid, ids, context=None):
431         """ create invoices for the given sales orders (ids), and open the form
432             view of one of the newly created invoices
433         """
434         mod_obj = self.pool.get('ir.model.data')
435         wf_service = netsvc.LocalService("workflow")
436
437         # create invoices through the sales orders' workflow
438         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
439         for id in ids:
440             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
441         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
442         # determine newly created invoices
443         new_inv_ids = list(inv_ids1 - inv_ids0)
444
445         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
446         res_id = res and res[1] or False,
447
448         return {
449             'name': _('Customer Invoices'),
450             'view_type': 'form',
451             'view_mode': 'form',
452             'view_id': [res_id],
453             'res_model': 'account.invoice',
454             'context': "{'type':'out_invoice'}",
455             'type': 'ir.actions.act_window',
456             'nodestroy': True,
457             'target': 'current',
458             'res_id': new_inv_ids and new_inv_ids[0] or False,
459         }
460
461     def action_view_invoice(self, cr, uid, ids, context=None):
462         '''
463         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.
464         '''
465         mod_obj = self.pool.get('ir.model.data')
466         act_obj = self.pool.get('ir.actions.act_window')
467
468         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
469         id = result and result[1] or False
470         result = act_obj.read(cr, uid, [id], context=context)[0]
471         #compute the number of invoices to display
472         inv_ids = []
473         for so in self.browse(cr, uid, ids, context=context):
474             inv_ids += [invoice.id for invoice in so.invoice_ids]
475         #choose the view_mode accordingly
476         if len(inv_ids)>1:
477             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
478         else:
479             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
480             result['views'] = [(res and res[1] or False, 'form')]
481             result['res_id'] = inv_ids and inv_ids[0] or False
482         return result
483
484     def test_no_product(self, cr, uid, order, context):
485         for line in order.order_line:
486             if line.product_id and (line.product_id.type<>'service'):
487                 return False
488         return True
489
490     def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
491         if states is None:
492             states = ['confirmed', 'done', 'exception']
493         res = False
494         invoices = {}
495         invoice_ids = []
496         invoice = self.pool.get('account.invoice')
497         obj_sale_order_line = self.pool.get('sale.order.line')
498         partner_currency = {}
499         if context is None:
500             context = {}
501         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
502         # last day of the last month as invoice date
503         if date_invoice:
504             context['date_invoice'] = date_invoice
505         for o in self.browse(cr, uid, ids, context=context):
506             currency_id = o.pricelist_id.currency_id.id
507             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
508                 raise osv.except_osv(
509                     _('Error!'),
510                     _('You cannot group sales having different currencies for the same partner.'))
511
512             partner_currency[o.partner_id.id] = currency_id
513             lines = []
514             for line in o.order_line:
515                 if line.invoiced:
516                     continue
517                 elif (line.state in states):
518                     lines.append(line.id)
519             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
520             if created_lines:
521                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
522         if not invoices:
523             for o in self.browse(cr, uid, ids, context=context):
524                 for i in o.invoice_ids:
525                     if i.state == 'draft':
526                         return i.id
527         for val in invoices.values():
528             if grouped:
529                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
530                 invoice_ref = ''
531                 for o, l in val:
532                     invoice_ref += o.name + '|'
533                     self.write(cr, uid, [o.id], {'state': 'progress'})
534                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
535                 #remove last '|' in invoice_ref
536                 if len(invoice_ref) >= 1: 
537                     invoice_ref = invoice_ref[:-1]
538                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
539             else:
540                 for order, il in val:
541                     res = self._make_invoice(cr, uid, order, il, context=context)
542                     invoice_ids.append(res)
543                     self.write(cr, uid, [order.id], {'state': 'progress'})
544                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
545         return res
546
547     def action_invoice_cancel(self, cr, uid, ids, context=None):
548         self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
549         return True
550
551     def action_invoice_end(self, cr, uid, ids, context=None):
552         for this in self.browse(cr, uid, ids, context=context):
553             for line in this.order_line:
554                 if line.state == 'exception':
555                     line.write({'state': 'confirmed'})
556             if this.state == 'invoice_except':
557                 this.write({'state': 'progress'})
558         return True
559
560     def action_cancel(self, cr, uid, ids, context=None):
561         wf_service = netsvc.LocalService("workflow")
562         if context is None:
563             context = {}
564         sale_order_line_obj = self.pool.get('sale.order.line')
565         for sale in self.browse(cr, uid, ids, context=context):
566             for inv in sale.invoice_ids:
567                 if inv.state not in ('draft', 'cancel'):
568                     raise osv.except_osv(
569                         _('Cannot cancel this sales order!'),
570                         _('First cancel all invoices attached to this sales order.'))
571             for r in self.read(cr, uid, ids, ['invoice_ids']):
572                 for inv in r['invoice_ids']:
573                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
574             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
575                     {'state': 'cancel'})
576         self.write(cr, uid, ids, {'state': 'cancel'})
577         return True
578
579     def action_button_confirm(self, cr, uid, ids, context=None):
580         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
581         wf_service = netsvc.LocalService('workflow')
582         wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
583
584         # redisplay the record as a sales order
585         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
586         view_id = view_ref and view_ref[1] or False,
587         return {
588             'type': 'ir.actions.act_window',
589             'name': _('Sales Order'),
590             'res_model': 'sale.order',
591             'res_id': ids[0],
592             'view_type': 'form',
593             'view_mode': 'form',
594             'view_id': view_id,
595             'target': 'current',
596             'nodestroy': True,
597         }
598
599     def action_wait(self, cr, uid, ids, context=None):
600         context = context or {}
601         for o in self.browse(cr, uid, ids):
602             if not o.order_line:
603                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
604             noprod = self.test_no_product(cr, uid, o, context)
605             if (o.order_policy == 'manual') or noprod:
606                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
607             else:
608                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
609             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
610         return True
611
612     def action_quotation_send(self, cr, uid, ids, context=None):
613         '''
614         This function opens a window to compose an email, with the edi sale template message loaded by default
615         '''
616         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
617         ir_model_data = self.pool.get('ir.model.data')
618         try:
619             template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
620         except ValueError:
621             template_id = False
622         try:
623             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
624         except ValueError:
625             compose_form_id = False 
626         ctx = dict(context)
627         ctx.update({
628             'default_model': 'sale.order',
629             'default_res_id': ids[0],
630             'default_use_template': bool(template_id),
631             'default_template_id': template_id,
632             'default_composition_mode': 'comment',
633             'mark_so_as_sent': True
634         })
635         return {
636             'type': 'ir.actions.act_window',
637             'view_type': 'form',
638             'view_mode': 'form',
639             'res_model': 'mail.compose.message',
640             'views': [(compose_form_id, 'form')],
641             'view_id': compose_form_id,
642             'target': 'new',
643             'context': ctx,
644         }
645
646     def action_done(self, cr, uid, ids, context=None):
647         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
648
649
650
651 # TODO add a field price_unit_uos
652 # - update it on change product and unit price
653 # - use it in report if there is a uos
654 class sale_order_line(osv.osv):
655
656     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
657         tax_obj = self.pool.get('account.tax')
658         cur_obj = self.pool.get('res.currency')
659         res = {}
660         if context is None:
661             context = {}
662         for line in self.browse(cr, uid, ids, context=context):
663             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
664             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
665             cur = line.order_id.pricelist_id.currency_id
666             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
667         return res
668
669     def _get_uom_id(self, cr, uid, *args):
670         try:
671             proxy = self.pool.get('ir.model.data')
672             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
673             return result[1]
674         except Exception, ex:
675             return False
676
677     def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
678         res = dict.fromkeys(ids, False)
679         for this in self.browse(cr, uid, ids, context=context):
680             res[this.id] = this.invoice_lines and \
681                 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines) 
682         return res
683
684     def _order_lines_from_invoice(self, cr, uid, ids, context=None):
685         # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
686         cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
687                                                   sale_order_line sol ON (sol.order_id = rel.order_id)
688                                     WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
689         return [i[0] for i in cr.fetchall()]
690
691     _name = 'sale.order.line'
692     _description = 'Sales Order Line'
693     _columns = {
694         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
695         'name': fields.text('Description', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
696         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
697         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
698         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
699         'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
700             store={
701                 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
702                 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
703         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
704         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
705          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."),
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',help="A partner to whom the particular product needs to be allotted."),
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\' status is set when the related sales order in draft status. \
717                     \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
718                     \n* The \'Exception\' status is set when the related sales order is set as exception. \
719                     \n* The \'Done\' status is set when the sales order line has been picked. \
720                     \n* The \'Cancelled\' status 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 = 'order_id desc, 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         'state': 'draft',
733         'type': 'make_to_stock',
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            sales 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.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                 'sequence': line.sequence,
790                 'origin': line.order_id.name,
791                 'account_id': account_id,
792                 'price_unit': pu,
793                 'quantity': uosqty,
794                 'discount': line.discount,
795                 'uos_id': uos_id,
796                 'product_id': line.product_id.id or False,
797                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
798                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
799             }
800
801         return res
802
803     def invoice_line_create(self, cr, uid, ids, context=None):
804         if context is None:
805             context = {}
806
807         create_ids = []
808         sales = set()
809         for line in self.browse(cr, uid, ids, context=context):
810             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
811             if vals:
812                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
813                 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
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 sales 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',  '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_partner)
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
907         if not flag:
908             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
909             if product_obj.description_sale:
910                 result['name'] += '\n'+product_obj.description_sale
911         domain = {}
912         if (not uom) and (not uos):
913             result['product_uom'] = product_obj.uom_id.id
914             if product_obj.uos_id:
915                 result['product_uos'] = product_obj.uos_id.id
916                 result['product_uos_qty'] = qty * product_obj.uos_coeff
917                 uos_category_id = product_obj.uos_id.category_id.id
918             else:
919                 result['product_uos'] = False
920                 result['product_uos_qty'] = qty
921                 uos_category_id = False
922             result['th_weight'] = qty * product_obj.weight
923             domain = {'product_uom':
924                         [('category_id', '=', product_obj.uom_id.category_id.id)],
925                         'product_uos':
926                         [('category_id', '=', uos_category_id)]}
927         elif uos and not uom: # only happens if uom is False
928             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
929             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
930             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
931         elif uom: # whether uos is set or not
932             default_uom = product_obj.uom_id and product_obj.uom_id.id
933             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
934             if product_obj.uos_id:
935                 result['product_uos'] = product_obj.uos_id.id
936                 result['product_uos_qty'] = qty * product_obj.uos_coeff
937             else:
938                 result['product_uos'] = False
939                 result['product_uos_qty'] = qty
940             result['th_weight'] = q * product_obj.weight        # Round the quantity up
941
942         if not uom2:
943             uom2 = product_obj.uom_id
944         # get unit price
945
946         if not pricelist:
947             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
948                     'Please set one before choosing a product.')
949             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
950         else:
951             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
952                     product, qty or 1.0, partner_id, {
953                         'uom': uom or result.get('product_uom'),
954                         'date': date_order,
955                         })[pricelist]
956             if price is False:
957                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
958                         "You have to change either the product, the quantity or the pricelist.")
959
960                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
961             else:
962                 result.update({'price_unit': price})
963         if warning_msgs:
964             warning = {
965                        'title': _('Configuration Error!'),
966                        'message' : warning_msgs
967                     }
968         return {'value': result, 'domain': domain, 'warning': warning}
969
970     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
971             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
972             lang=False, update_tax=True, date_order=False, context=None):
973         context = context or {}
974         lang = lang or ('lang' in context and context['lang'])
975         if not uom:
976             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
977         return self.product_id_change(cursor, user, ids, pricelist, product,
978                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
979                 partner_id=partner_id, lang=lang, update_tax=update_tax,
980                 date_order=date_order, context=context)
981
982     def unlink(self, cr, uid, ids, context=None):
983         if context is None:
984             context = {}
985         """Allows to delete sales order lines in draft,cancel states"""
986         for rec in self.browse(cr, uid, ids, context=context):
987             if rec.state not in ['draft', 'cancel']:
988                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
989         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
990
991
992 class mail_compose_message(osv.Model):
993     _inherit = 'mail.compose.message'
994
995     def send_mail(self, cr, uid, ids, context=None):
996         context = context or {}
997         if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
998             context = dict(context, mail_post_autofollow=True)
999             wf_service = netsvc.LocalService("workflow")
1000             wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
1001         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1002
1003 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: