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