[FIX]sale_line_to_invoice: while creating invoice from sale order line the tax is...
[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         addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
318         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
319         payment_term = part.property_payment_term and part.property_payment_term.id or False
320         fiscal_position = part.property_account_position and part.property_account_position.id or False
321         dedicated_salesman = part.user_id and part.user_id.id or uid
322         val = {
323             'partner_invoice_id': addr['invoice'],
324             'partner_shipping_id': addr['delivery'],
325             'payment_term': payment_term,
326             'fiscal_position': fiscal_position,
327             'user_id': dedicated_salesman,
328         }
329         if pricelist:
330             val['pricelist_id'] = pricelist
331         return {'value': val}
332
333     def create(self, cr, uid, vals, context=None):
334         if vals.get('name','/')=='/':
335             vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
336         return super(sale_order, self).create(cr, uid, vals, context=context)
337
338     def button_dummy(self, cr, uid, ids, context=None):
339         return True
340
341     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
342     #        can be removed after 6.1.
343     def _inv_get(self, cr, uid, order, context=None):
344         return {}
345
346     def _prepare_invoice(self, cr, uid, order, lines, context=None):
347         """Prepare the dict of values to create the new invoice for a
348            sales order. This method may be overridden to implement custom
349            invoice generation (making sure to call super() to establish
350            a clean extension chain).
351
352            :param browse_record order: sale.order record to invoice
353            :param list(int) line: list of invoice line IDs that must be
354                                   attached to the invoice
355            :return: dict of value to create() the invoice
356         """
357         if context is None:
358             context = {}
359         journal_ids = self.pool.get('account.journal').search(cr, uid,
360             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
361             limit=1)
362         if not journal_ids:
363             raise osv.except_osv(_('Error!'),
364                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
365         invoice_vals = {
366             'name': order.client_order_ref or '',
367             'origin': order.name,
368             'type': 'out_invoice',
369             'reference': order.client_order_ref or order.name,
370             'account_id': order.partner_id.property_account_receivable.id,
371             'partner_id': order.partner_invoice_id.id,
372             'journal_id': journal_ids[0],
373             'invoice_line': [(6, 0, lines)],
374             'currency_id': order.pricelist_id.currency_id.id,
375             'comment': order.note,
376             'payment_term': order.payment_term and order.payment_term.id or False,
377             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
378             'date_invoice': context.get('date_invoice', False),
379             'company_id': order.company_id.id,
380             'user_id': order.user_id and order.user_id.id or False
381         }
382
383         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
384         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
385         return invoice_vals
386
387     def _make_invoice(self, cr, uid, order, lines, context=None):
388         inv_obj = self.pool.get('account.invoice')
389         obj_invoice_line = self.pool.get('account.invoice.line')
390         if context is None:
391             context = {}
392         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
393         from_line_invoice_ids = []
394         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
395             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
396                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
397                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
398         for preinv in order.invoice_ids:
399             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
400                 for preline in preinv.invoice_line:
401                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
402                     lines.append(inv_line_id)
403         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
404         inv_id = inv_obj.create(cr, uid, inv, context=context)
405         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
406         if data.get('value', False):
407             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
408         inv_obj.button_compute(cr, uid, [inv_id])
409         return inv_id
410
411     def print_quotation(self, cr, uid, ids, context=None):
412         '''
413         This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
414         '''
415         assert len(ids) == 1, 'This option should only be used for a single id at a time'
416         wf_service = netsvc.LocalService("workflow")
417         wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
418         datas = {
419                  'model': 'sale.order',
420                  'ids': ids,
421                  'form': self.read(cr, uid, ids[0], context=context),
422         }
423         return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
424
425     def manual_invoice(self, cr, uid, ids, context=None):
426         """ create invoices for the given sales orders (ids), and open the form
427             view of one of the newly created invoices
428         """
429         mod_obj = self.pool.get('ir.model.data')
430         wf_service = netsvc.LocalService("workflow")
431
432         # create invoices through the sales orders' workflow
433         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
434         for id in ids:
435             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
436         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
437         # determine newly created invoices
438         new_inv_ids = list(inv_ids1 - inv_ids0)
439
440         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
441         res_id = res and res[1] or False,
442
443         return {
444             'name': _('Customer Invoices'),
445             'view_type': 'form',
446             'view_mode': 'form',
447             'view_id': [res_id],
448             'res_model': 'account.invoice',
449             'context': "{'type':'out_invoice'}",
450             'type': 'ir.actions.act_window',
451             'nodestroy': True,
452             'target': 'current',
453             'res_id': new_inv_ids and new_inv_ids[0] or False,
454         }
455
456     def action_view_invoice(self, cr, uid, ids, context=None):
457         '''
458         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.
459         '''
460         mod_obj = self.pool.get('ir.model.data')
461         act_obj = self.pool.get('ir.actions.act_window')
462
463         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
464         id = result and result[1] or False
465         result = act_obj.read(cr, uid, [id], context=context)[0]
466         #compute the number of invoices to display
467         inv_ids = []
468         for so in self.browse(cr, uid, ids, context=context):
469             inv_ids += [invoice.id for invoice in so.invoice_ids]
470         #choose the view_mode accordingly
471         if len(inv_ids)>1:
472             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
473         else:
474             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
475             result['views'] = [(res and res[1] or False, 'form')]
476             result['res_id'] = inv_ids and inv_ids[0] or False
477         return result
478
479     def test_no_product(self, cr, uid, order, context):
480         for line in order.order_line:
481             if line.product_id and (line.product_id.type<>'service'):
482                 return False
483         return True
484
485     def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
486         if states is None:
487             states = ['confirmed', 'done', 'exception']
488         res = False
489         invoices = {}
490         invoice_ids = []
491         invoice = self.pool.get('account.invoice')
492         obj_sale_order_line = self.pool.get('sale.order.line')
493         partner_currency = {}
494         if context is None:
495             context = {}
496         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
497         # last day of the last month as invoice date
498         if date_invoice:
499             context['date_invoice'] = date_invoice
500         for o in self.browse(cr, uid, ids, context=context):
501             currency_id = o.pricelist_id.currency_id.id
502             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
503                 raise osv.except_osv(
504                     _('Error!'),
505                     _('You cannot group sales having different currencies for the same partner.'))
506
507             partner_currency[o.partner_id.id] = currency_id
508             lines = []
509             for line in o.order_line:
510                 if line.invoiced:
511                     continue
512                 elif (line.state in states):
513                     lines.append(line.id)
514             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
515             if created_lines:
516                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
517         if not invoices:
518             for o in self.browse(cr, uid, ids, context=context):
519                 for i in o.invoice_ids:
520                     if i.state == 'draft':
521                         return i.id
522         for val in invoices.values():
523             if grouped:
524                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
525                 invoice_ref = ''
526                 for o, l in val:
527                     invoice_ref += o.name + '|'
528                     self.write(cr, uid, [o.id], {'state': 'progress'})
529                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
530                 #remove last '|' in invoice_ref
531                 if len(invoice_ref) >= 1: 
532                     invoice_ref = invoice_ref[:-1]
533                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
534             else:
535                 for order, il in val:
536                     res = self._make_invoice(cr, uid, order, il, context=context)
537                     invoice_ids.append(res)
538                     self.write(cr, uid, [order.id], {'state': 'progress'})
539                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
540         return res
541
542     def action_invoice_cancel(self, cr, uid, ids, context=None):
543         self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
544         return True
545
546     def action_invoice_end(self, cr, uid, ids, context=None):
547         for this in self.browse(cr, uid, ids, context=context):
548             for line in this.order_line:
549                 if line.state == 'exception':
550                     line.write({'state': 'confirmed'})
551             if this.state == 'invoice_except':
552                 this.write({'state': 'progress'})
553         return True
554
555     def action_cancel(self, cr, uid, ids, context=None):
556         wf_service = netsvc.LocalService("workflow")
557         if context is None:
558             context = {}
559         sale_order_line_obj = self.pool.get('sale.order.line')
560         for sale in self.browse(cr, uid, ids, context=context):
561             for inv in sale.invoice_ids:
562                 if inv.state not in ('draft', 'cancel'):
563                     raise osv.except_osv(
564                         _('Cannot cancel this sales order!'),
565                         _('First cancel all invoices attached to this sales order.'))
566             for r in self.read(cr, uid, ids, ['invoice_ids']):
567                 for inv in r['invoice_ids']:
568                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
569             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
570                     {'state': 'cancel'})
571         self.write(cr, uid, ids, {'state': 'cancel'})
572         return True
573
574     def action_button_confirm(self, cr, uid, ids, context=None):
575         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
576         wf_service = netsvc.LocalService('workflow')
577         wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
578
579         # redisplay the record as a sales order
580         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
581         view_id = view_ref and view_ref[1] or False,
582         return {
583             'type': 'ir.actions.act_window',
584             'name': _('Sales Order'),
585             'res_model': 'sale.order',
586             'res_id': ids[0],
587             'view_type': 'form',
588             'view_mode': 'form',
589             'view_id': view_id,
590             'target': 'current',
591             'nodestroy': True,
592         }
593
594     def action_wait(self, cr, uid, ids, context=None):
595         context = context or {}
596         for o in self.browse(cr, uid, ids):
597             if not o.order_line:
598                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
599             noprod = self.test_no_product(cr, uid, o, context)
600             if (o.order_policy == 'manual') or noprod:
601                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
602             else:
603                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
604             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
605         return True
606
607     def action_quotation_send(self, cr, uid, ids, context=None):
608         '''
609         This function opens a window to compose an email, with the edi sale template message loaded by default
610         '''
611         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
612         ir_model_data = self.pool.get('ir.model.data')
613         try:
614             template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
615         except ValueError:
616             template_id = False
617         try:
618             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
619         except ValueError:
620             compose_form_id = False 
621         ctx = dict(context)
622         ctx.update({
623             'default_model': 'sale.order',
624             'default_res_id': ids[0],
625             'default_use_template': bool(template_id),
626             'default_template_id': template_id,
627             'default_composition_mode': 'comment',
628             'mark_so_as_sent': True
629         })
630         return {
631             'type': 'ir.actions.act_window',
632             'view_type': 'form',
633             'view_mode': 'form',
634             'res_model': 'mail.compose.message',
635             'views': [(compose_form_id, 'form')],
636             'view_id': compose_form_id,
637             'target': 'new',
638             'context': ctx,
639         }
640
641     def action_done(self, cr, uid, ids, context=None):
642         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
643
644
645
646 # TODO add a field price_unit_uos
647 # - update it on change product and unit price
648 # - use it in report if there is a uos
649 class sale_order_line(osv.osv):
650
651     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
652         tax_obj = self.pool.get('account.tax')
653         cur_obj = self.pool.get('res.currency')
654         res = {}
655         if context is None:
656             context = {}
657         for line in self.browse(cr, uid, ids, context=context):
658             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
659             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
660             cur = line.order_id.pricelist_id.currency_id
661             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
662         return res
663
664     def _get_uom_id(self, cr, uid, *args):
665         try:
666             proxy = self.pool.get('ir.model.data')
667             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
668             return result[1]
669         except Exception, ex:
670             return False
671
672     def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
673         res = dict.fromkeys(ids, False)
674         for this in self.browse(cr, uid, ids, context=context):
675             res[this.id] = this.invoice_lines and \
676                 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines) 
677         return res
678
679     def _order_lines_from_invoice(self, cr, uid, ids, context=None):
680         # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
681         cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
682                                                   sale_order_line sol ON (sol.order_id = rel.order_id)
683                                     WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
684         return [i[0] for i in cr.fetchall()]
685
686     _name = 'sale.order.line'
687     _description = 'Sales Order Line'
688     _columns = {
689         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
690         'name': fields.text('Description', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
691         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
692         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
693         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
694         'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
695             store={
696                 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
697                 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
698         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
699         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
700          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."),
701         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
702         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
703         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
704         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
705         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
706         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
707         'product_uos': fields.many2one('product.uom', 'Product UoS'),
708         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
709         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
710         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
711                 help='* The \'Draft\' status is set when the related sales order in draft status. \
712                     \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
713                     \n* The \'Exception\' status is set when the related sales order is set as exception. \
714                     \n* The \'Done\' status is set when the sales order line has been picked. \
715                     \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
716         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
717         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
718         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
719     }
720     _order = 'order_id desc, sequence, id'
721     _defaults = {
722         'product_uom' : _get_uom_id,
723         'discount': 0.0,
724         'product_uom_qty': 1,
725         'product_uos_qty': 1,
726         'sequence': 10,
727         'state': 'draft',
728         'type': 'make_to_stock',
729         'price_unit': 0.0,
730     }
731
732     def _get_line_qty(self, cr, uid, line, context=None):
733         if (line.order_id.invoice_quantity=='order'):
734             if line.product_uos:
735                 return line.product_uos_qty or 0.0
736         return line.product_uom_qty
737
738     def _get_line_uom(self, cr, uid, line, context=None):
739         if (line.order_id.invoice_quantity=='order'):
740             if line.product_uos:
741                 return line.product_uos.id
742         return line.product_uom.id
743
744     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
745         """Prepare the dict of values to create the new invoice line for a
746            sales order line. This method may be overridden to implement custom
747            invoice generation (making sure to call super() to establish
748            a clean extension chain).
749
750            :param browse_record line: sale.order.line record to invoice
751            :param int account_id: optional ID of a G/L account to force
752                (this is used for returning products including service)
753            :return: dict of values to create() the invoice line
754         """
755         res = {}
756         if not line.invoiced:
757             if not account_id:
758                 if line.product_id:
759                     account_id = line.product_id.property_account_income.id
760                     if not account_id:
761                         account_id = line.product_id.categ_id.property_account_income_categ.id
762                     if not account_id:
763                         raise osv.except_osv(_('Error!'),
764                                 _('Please define income account for this product: "%s" (id:%d).') % \
765                                     (line.product_id.name, line.product_id.id,))
766                 else:
767                     prop = self.pool.get('ir.property').get(cr, uid,
768                             'property_account_income_categ', 'product.category',
769                             context=context)
770                     account_id = prop and prop.id or False
771             uosqty = self._get_line_qty(cr, uid, line, context=context)
772             uos_id = self._get_line_uom(cr, uid, line, context=context)
773             pu = 0.0
774             if uosqty:
775                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
776                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
777             fpos = line.order_id.fiscal_position or False
778             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
779             if not account_id:
780                 raise osv.except_osv(_('Error!'),
781                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
782             res = {
783                 'name': line.name,
784                 'sequence': line.sequence,
785                 'origin': line.order_id.name,
786                 'account_id': account_id,
787                 'price_unit': pu,
788                 'quantity': uosqty,
789                 'discount': line.discount,
790                 'uos_id': uos_id,
791                 'product_id': line.product_id.id or False,
792                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
793                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
794             }
795
796         return res
797
798     def invoice_line_create(self, cr, uid, ids, context=None):
799         if context is None:
800             context = {}
801
802         create_ids = []
803         sales = set()
804         for line in self.browse(cr, uid, ids, context=context):
805             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
806             if vals:
807                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
808                 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
809                 sales.add(line.order_id.id)
810                 create_ids.append(inv_id)
811         # Trigger workflow events
812         wf_service = netsvc.LocalService("workflow")
813         for sale_id in sales:
814             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
815         return create_ids
816
817     def button_cancel(self, cr, uid, ids, context=None):
818         for line in self.browse(cr, uid, ids, context=context):
819             if line.invoiced:
820                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
821         return self.write(cr, uid, ids, {'state': 'cancel'})
822
823     def button_confirm(self, cr, uid, ids, context=None):
824         return self.write(cr, uid, ids, {'state': 'confirmed'})
825
826     def button_done(self, cr, uid, ids, context=None):
827         wf_service = netsvc.LocalService("workflow")
828         res = self.write(cr, uid, ids, {'state': 'done'})
829         for line in self.browse(cr, uid, ids, context=context):
830             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
831         return res
832
833     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
834         product_obj = self.pool.get('product.product')
835         if not product_id:
836             return {'value': {'product_uom': product_uos,
837                 'product_uom_qty': product_uos_qty}, 'domain': {}}
838
839         product = product_obj.browse(cr, uid, product_id)
840         value = {
841             'product_uom': product.uom_id.id,
842         }
843         # FIXME must depend on uos/uom of the product and not only of the coeff.
844         try:
845             value.update({
846                 'product_uom_qty': product_uos_qty / product.uos_coeff,
847                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
848             })
849         except ZeroDivisionError:
850             pass
851         return {'value': value}
852
853     def copy_data(self, cr, uid, id, default=None, context=None):
854         if not default:
855             default = {}
856         default.update({'state': 'draft',  'invoice_lines': []})
857         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
858
859     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
860             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
861             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
862         context = context or {}
863         lang = lang or context.get('lang',False)
864         if not  partner_id:
865             raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
866         warning = {}
867         product_uom_obj = self.pool.get('product.uom')
868         partner_obj = self.pool.get('res.partner')
869         product_obj = self.pool.get('product.product')
870         context = {'lang': lang, 'partner_id': partner_id}
871         if partner_id:
872             lang = partner_obj.browse(cr, uid, partner_id).lang
873         context_partner = {'lang': lang, 'partner_id': partner_id}
874
875         if not product:
876             return {'value': {'th_weight': 0,
877                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
878                    'product_uos': []}}
879         if not date_order:
880             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
881
882         result = {}
883         warning_msgs = ''
884         product_obj = product_obj.browse(cr, uid, product, context=context_partner)
885
886         uom2 = False
887         if uom:
888             uom2 = product_uom_obj.browse(cr, uid, uom)
889             if product_obj.uom_id.category_id.id != uom2.category_id.id:
890                 uom = False
891         if uos:
892             if product_obj.uos_id:
893                 uos2 = product_uom_obj.browse(cr, uid, uos)
894                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
895                     uos = False
896             else:
897                 uos = False
898         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
899         if update_tax: #The quantity only have changed
900             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
901
902         if not flag:
903             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
904             if product_obj.description_sale:
905                 result['name'] += '\n'+product_obj.description_sale
906         domain = {}
907         if (not uom) and (not uos):
908             result['product_uom'] = product_obj.uom_id.id
909             if product_obj.uos_id:
910                 result['product_uos'] = product_obj.uos_id.id
911                 result['product_uos_qty'] = qty * product_obj.uos_coeff
912                 uos_category_id = product_obj.uos_id.category_id.id
913             else:
914                 result['product_uos'] = False
915                 result['product_uos_qty'] = qty
916                 uos_category_id = False
917             result['th_weight'] = qty * product_obj.weight
918             domain = {'product_uom':
919                         [('category_id', '=', product_obj.uom_id.category_id.id)],
920                         'product_uos':
921                         [('category_id', '=', uos_category_id)]}
922         elif uos and not uom: # only happens if uom is False
923             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
924             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
925             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
926         elif uom: # whether uos is set or not
927             default_uom = product_obj.uom_id and product_obj.uom_id.id
928             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
929             if product_obj.uos_id:
930                 result['product_uos'] = product_obj.uos_id.id
931                 result['product_uos_qty'] = qty * product_obj.uos_coeff
932             else:
933                 result['product_uos'] = False
934                 result['product_uos_qty'] = qty
935             result['th_weight'] = q * product_obj.weight        # Round the quantity up
936
937         if not uom2:
938             uom2 = product_obj.uom_id
939         # get unit price
940
941         if not pricelist:
942             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
943                     'Please set one before choosing a product.')
944             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
945         else:
946             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
947                     product, qty or 1.0, partner_id, {
948                         'uom': uom or result.get('product_uom'),
949                         'date': date_order,
950                         })[pricelist]
951             if price is False:
952                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
953                         "You have to change either the product, the quantity or the pricelist.")
954
955                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
956             else:
957                 result.update({'price_unit': price})
958         if warning_msgs:
959             warning = {
960                        'title': _('Configuration Error!'),
961                        'message' : warning_msgs
962                     }
963         return {'value': result, 'domain': domain, 'warning': warning}
964
965     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
966             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
967             lang=False, update_tax=True, date_order=False, context=None):
968         context = context or {}
969         lang = lang or ('lang' in context and context['lang'])
970         if not uom:
971             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
972         return self.product_id_change(cursor, user, ids, pricelist, product,
973                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
974                 partner_id=partner_id, lang=lang, update_tax=update_tax,
975                 date_order=date_order, context=context)
976
977     def unlink(self, cr, uid, ids, context=None):
978         if context is None:
979             context = {}
980         """Allows to delete sales order lines in draft,cancel states"""
981         for rec in self.browse(cr, uid, ids, context=context):
982             if rec.state not in ['draft', 'cancel']:
983                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
984         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
985
986
987 class mail_compose_message(osv.Model):
988     _inherit = 'mail.compose.message'
989
990     def send_mail(self, cr, uid, ids, context=None):
991         context = context or {}
992         if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
993             context = dict(context, mail_post_autofollow=True)
994             wf_service = netsvc.LocalService("workflow")
995             wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
996         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
997
998
999 class account_invoice(osv.Model):
1000     _inherit = 'account.invoice'
1001
1002     def unlink(self, cr, uid, ids, context=None):
1003         """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1004         invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1005         #if we can't cancel all invoices, do nothing
1006         if len(invoice_ids) == len(ids):
1007             #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1008             #it will trigger the workflow to put the sale order in an 'invoice exception' state
1009             wf_service = netsvc.LocalService("workflow")
1010             for id in ids:
1011                 wf_service.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1012         return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1013
1014 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: