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