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