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