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