[MERGE] sale: Add _prepare_invoice(), courtesy of Alexis Delattre
[odoo/odoo.git] / addons / sale / sale.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
24 import time
25 import pooler
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
29 import decimal_precision as dp
30 import netsvc
31
32 class sale_shop(osv.osv):
33     _name = "sale.shop"
34     _description = "Sales Shop"
35     _columns = {
36         'name': fields.char('Shop Name', size=64, required=True),
37         'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40         'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
41         'company_id': fields.many2one('res.company', 'Company', required=False),
42     }
43     _defaults = {
44         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
45     }
46
47 sale_shop()
48
49 class sale_order(osv.osv):
50     _name = "sale.order"
51     _description = "Sales Order"
52
53     def copy(self, cr, uid, id, default=None, context=None):
54         if not default:
55             default = {}
56         default.update({
57             'state': 'draft',
58             'shipped': False,
59             'invoice_ids': [],
60             'picking_ids': [],
61             'date_confirm': False,
62             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
63         })
64         return super(sale_order, self).copy(cr, uid, id, default, context=context)
65
66     def _amount_line_tax(self, cr, uid, line, context=None):
67         val = 0.0
68         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.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)['taxes']:
69             val += c.get('amount', 0.0)
70         return val
71
72     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
73         cur_obj = self.pool.get('res.currency')
74         res = {}
75         for order in self.browse(cr, uid, ids, context=context):
76             res[order.id] = {
77                 'amount_untaxed': 0.0,
78                 'amount_tax': 0.0,
79                 'amount_total': 0.0,
80             }
81             val = val1 = 0.0
82             cur = order.pricelist_id.currency_id
83             for line in order.order_line:
84                 val1 += line.price_subtotal
85                 val += self._amount_line_tax(cr, uid, line, context=context)
86             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
87             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
88             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
89         return res
90
91     # This is False
92     def _picked_rate(self, cr, uid, ids, name, arg, context=None):
93         if not ids:
94             return {}
95         res = {}
96         tmp = {}
97         for id in ids:
98             tmp[id] = {'picked': 0.0, 'total': 0.0}
99         cr.execute('''SELECT
100                 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
101             FROM
102                 stock_move m
103             LEFT JOIN
104                 stock_picking p on (p.id=m.picking_id)
105             LEFT JOIN
106                 procurement_order mp on (mp.move_id=m.id)
107             WHERE
108                 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
109
110         for item in cr.dictfetchall():
111             if item['move_state'] == 'cancel':
112                 continue
113
114             if item['picking_type'] == 'in':#this is a returned picking
115                 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
116                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
117                     tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
118             else:
119                 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
120                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
121                     tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
122
123         for order in self.browse(cr, uid, ids, context=context):
124             if order.shipped:
125                 res[order.id] = 100.0
126             else:
127                 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
128         return res
129
130     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
131         res = {}
132         for sale in self.browse(cursor, user, ids, context=context):
133             if sale.invoiced:
134                 res[sale.id] = 100.0
135                 continue
136             tot = 0.0
137             for invoice in sale.invoice_ids:
138                 if invoice.state not in ('draft', 'cancel'):
139                     tot += invoice.amount_untaxed
140             if tot:
141                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
142             else:
143                 res[sale.id] = 0.0
144         return res
145
146     def _invoiced(self, cursor, user, ids, name, arg, context=None):
147         res = {}
148         for sale in self.browse(cursor, user, ids, context=context):
149             res[sale.id] = True
150             for invoice in sale.invoice_ids:
151                 if invoice.state != 'paid':
152                     res[sale.id] = False
153                     break
154             if not sale.invoice_ids:
155                 res[sale.id] = False
156         return res
157
158     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
159         if not len(args):
160             return []
161         clause = ''
162         sale_clause = ''
163         no_invoiced = False
164         for arg in args:
165             if arg[1] == '=':
166                 if arg[2]:
167                     clause += 'AND inv.state = \'paid\''
168                 else:
169                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
170                     sale_clause = ',  sale_order AS sale '
171                     no_invoiced = True
172
173         cursor.execute('SELECT rel.order_id ' \
174                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
175                 'WHERE rel.invoice_id = inv.id ' + clause)
176         res = cursor.fetchall()
177         if no_invoiced:
178             cursor.execute('SELECT sale.id ' \
179                     'FROM sale_order AS sale ' \
180                     'WHERE sale.id NOT IN ' \
181                         '(SELECT rel.order_id ' \
182                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
183             res.extend(cursor.fetchall())
184         if not res:
185             return [('id', '=', 0)]
186         return [('id', 'in', [x[0] for x in res])]
187
188     def _get_order(self, cr, uid, ids, context=None):
189         result = {}
190         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
191             result[line.order_id.id] = True
192         return result.keys()
193
194     _columns = {
195         'name': fields.char('Order Reference', size=64, required=True,
196             readonly=True, states={'draft': [('readonly', False)]}, select=True),
197         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
198         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
199         'client_order_ref': fields.char('Customer Reference', size=64),
200         'state': fields.selection([
201             ('draft', 'Quotation'),
202             ('waiting_date', 'Waiting Schedule'),
203             ('manual', 'To Invoice'),
204             ('progress', 'In Progress'),
205             ('shipping_except', 'Shipping Exception'),
206             ('invoice_except', 'Invoice Exception'),
207             ('done', 'Done'),
208             ('cancel', 'Cancelled')
209             ], 'Order State', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
210         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
211         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
212         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
213         'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
214         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
215         'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sales order."),
216         'partner_order_id': fields.many2one('res.partner.address', 'Ordering Contact', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="The name and address of the contact who requested the order or quotation."),
217         'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
218
219         'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
220         'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
221             'Picking Policy', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
222         'order_policy': fields.selection([
223             ('prepaid', 'Pay before delivery'),
224             ('manual', 'Deliver & invoice on demand'),
225             ('picking', 'Invoice based on deliveries'),
226             ('postpaid', 'Invoice on order after delivery'),
227         ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
228                     help="""The Invoice Policy is used to synchronise invoice and delivery operations.
229   - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
230   - The 'Deliver & Invoice on demand' will create the picking order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice based on the sale order or the sale order lines.
231   - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
232   - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
233         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
234         'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
235
236         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
237         'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
238         'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Picking', readonly=True, help="This is a list of picking that has been generated for this sales order."),
239         'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
240         'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
241         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
242         'invoiced': fields.function(_invoiced, string='Paid',
243             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
244         'note': fields.text('Notes'),
245
246         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
247             store = {
248                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
249                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
250             },
251             multi='sums', help="The amount without tax."),
252         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
253             store = {
254                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
255                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
256             },
257             multi='sums', help="The tax amount."),
258         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
259             store = {
260                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
261                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
262             },
263             multi='sums', help="The total amount."),
264
265         'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True, readonly=True, states={'draft': [('readonly', False)]}),
266         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
267         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
268         'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
269     }
270     _defaults = {
271         'picking_policy': 'direct',
272         'date_order': fields.date.context_today,
273         'order_policy': 'manual',
274         'state': 'draft',
275         'user_id': lambda obj, cr, uid, context: uid,
276         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
277         'invoice_quantity': 'order',
278         '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'],
279         'partner_order_id': lambda self, cr, uid, context: context.get('partner_id', False) and  self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['contact'])['contact'],
280         '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'],
281     }
282     _sql_constraints = [
283         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
284     ]
285     _order = 'name desc'
286
287     # Form filling
288     def unlink(self, cr, uid, ids, context=None):
289         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
290         unlink_ids = []
291         for s in sale_orders:
292             if s['state'] in ['draft', 'cancel']:
293                 unlink_ids.append(s['id'])
294             else:
295                 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sale order, you must cancel it before ! To cancel a sale order, you must first cancel related picking or delivery orders.'))
296
297         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
298
299     def onchange_shop_id(self, cr, uid, ids, shop_id):
300         v = {}
301         if shop_id:
302             shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
303             v['project_id'] = shop.project_id.id
304             # Que faire si le client a une pricelist a lui ?
305             if shop.pricelist_id.id:
306                 v['pricelist_id'] = shop.pricelist_id.id
307         return {'value': v}
308
309     def action_cancel_draft(self, cr, uid, ids, *args):
310         if not len(ids):
311             return False
312         cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
313         line_ids = map(lambda x: x[0], cr.fetchall())
314         self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
315         self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
316         wf_service = netsvc.LocalService("workflow")
317         for inv_id in ids:
318             # Deleting the existing instance of workflow for SO
319             wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
320             wf_service.trg_create(uid, 'sale.order', inv_id, cr)
321         for (id,name) in self.name_get(cr, uid, ids):
322             message = _("The sales order '%s' has been set in draft state.") %(name,)
323             self.log(cr, uid, id, message)
324         return True
325
326     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
327         if (not pricelist_id) or (not order_lines):
328             return {}
329         warning = {
330             'title': _('Pricelist Warning!'),
331             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
332         }
333         return {'warning': warning}
334
335     def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
336         if not order_id:
337             return {}
338         val = {}
339         if not invoice_id:
340             val['partner_invoice_id'] = order_id
341         if not shipping_id:
342             val['partner_shipping_id'] = order_id
343         return {'value': val}
344
345     def onchange_partner_id(self, cr, uid, ids, part):
346         if not part:
347             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
348
349         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
350         part = self.pool.get('res.partner').browse(cr, uid, part)
351         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
352         payment_term = part.property_payment_term and part.property_payment_term.id or False
353         fiscal_position = part.property_account_position and part.property_account_position.id or False
354         dedicated_salesman = part.user_id and part.user_id.id or uid
355         val = {
356             'partner_invoice_id': addr['invoice'],
357             'partner_order_id': addr['contact'],
358             'partner_shipping_id': addr['delivery'],
359             'payment_term': payment_term,
360             'fiscal_position': fiscal_position,
361             'user_id': dedicated_salesman,
362         }
363         if pricelist:
364             val['pricelist_id'] = pricelist
365         return {'value': val}
366
367     def shipping_policy_change(self, cr, uid, ids, policy, context=None):
368         if not policy:
369             return {}
370         inv_qty = 'order'
371         if policy == 'prepaid':
372             inv_qty = 'order'
373         elif policy == 'picking':
374             inv_qty = 'procurement'
375         return {'value': {'invoice_quantity': inv_qty}}
376
377     def write(self, cr, uid, ids, vals, context=None):
378         if vals.get('order_policy', False):
379             if vals['order_policy'] == 'prepaid':
380                 vals.update({'invoice_quantity': 'order'})
381             elif vals['order_policy'] == 'picking':
382                 vals.update({'invoice_quantity': 'procurement'})
383         return super(sale_order, self).write(cr, uid, ids, vals, context=context)
384
385     def create(self, cr, uid, vals, context=None):
386         if vals.get('order_policy', False):
387             if vals['order_policy'] == 'prepaid':
388                 vals.update({'invoice_quantity': 'order'})
389             if vals['order_policy'] == 'picking':
390                 vals.update({'invoice_quantity': 'procurement'})
391         return super(sale_order, self).create(cr, uid, vals, context=context)
392
393     def button_dummy(self, cr, uid, ids, context=None):
394         return True
395
396     def _prepare_invoice(self, cr, uid, order, lines, context=None):
397         """ Builds the dict containing the values for the invoice
398             @param order: order object
399             @param line: list of invoice line IDs that must be attached to the invoice
400             @return: dict that will be used to create the invoice object
401         """
402         journal_ids = self.pool.get('account.journal').search(cr, uid,
403             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
404             limit=1)
405         if not journal_ids:
406             raise osv.except_osv(_('Error !'),
407                 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
408         return {
409             'name': order.client_order_ref or '',
410             'origin': order.name,
411             'type': 'out_invoice',
412             'reference': order.client_order_ref or order.name,
413             'account_id': order.partner_id.property_account_receivable.id,
414             'partner_id': order.partner_id.id,
415             'journal_id': journal_ids[0],
416             'address_invoice_id': order.partner_invoice_id.id,
417             'address_contact_id': order.partner_order_id.id,
418             'invoice_line': [(6, 0, lines)],
419             'currency_id': order.pricelist_id.currency_id.id,
420             'comment': order.note,
421             'payment_term': order.payment_term and order.payment_term.id or False,
422             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
423             'date_invoice': context.get('date_invoice', False),
424             'company_id': order.company_id.id,
425             'user_id': order.user_id and order.user_id.id or False
426         }
427
428     def _make_invoice(self, cr, uid, order, lines, context=None):
429         inv_obj = self.pool.get('account.invoice')
430         obj_invoice_line = self.pool.get('account.invoice.line')
431         if context is None:
432             context = {}
433         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
434         from_line_invoice_ids = []
435         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
436             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
437                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
438                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
439         for preinv in order.invoice_ids:
440             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
441                 for preline in preinv.invoice_line:
442                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
443                     lines.append(inv_line_id)
444         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
445         inv_id = inv_obj.create(cr, uid, inv, context=context)
446         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
447         if data.get('value', False):
448             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
449         inv_obj.button_compute(cr, uid, [inv_id])
450         return inv_id
451
452     def manual_invoice(self, cr, uid, ids, context=None):
453         mod_obj = self.pool.get('ir.model.data')
454         wf_service = netsvc.LocalService("workflow")
455         inv_ids = set()
456         inv_ids1 = set()
457         for id in ids:
458             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
459                 inv_ids.add(record.id)
460         # inv_ids would have old invoices if any
461         for id in ids:
462             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
463             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
464                 inv_ids1.add(record.id)
465         inv_ids = list(inv_ids1.difference(inv_ids))
466
467         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
468         res_id = res and res[1] or False,
469
470         return {
471             'name': _('Customer Invoices'),
472             'view_type': 'form',
473             'view_mode': 'form',
474             'view_id': [res_id],
475             'res_model': 'account.invoice',
476             'context': "{'type':'out_invoice'}",
477             'type': 'ir.actions.act_window',
478             'nodestroy': True,
479             'target': 'current',
480             'res_id': inv_ids and inv_ids[0] or False,
481         }
482
483     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
484         res = False
485         invoices = {}
486         invoice_ids = []
487         picking_obj = self.pool.get('stock.picking')
488         invoice = self.pool.get('account.invoice')
489         obj_sale_order_line = self.pool.get('sale.order.line')
490         partner_currency = {}
491         if context is None:
492             context = {}
493         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
494         # last day of the last month as invoice date
495         if date_inv:
496             context['date_inv'] = date_inv
497         for o in self.browse(cr, uid, ids, context=context):
498             currency_id = o.pricelist_id.currency_id.id
499             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
500                 raise osv.except_osv(
501                     _('Error !'),
502                     _('You cannot group sales having different currencies for the same partner.'))
503
504             partner_currency[o.partner_id.id] = currency_id
505             lines = []
506             for line in o.order_line:
507                 if line.invoiced:
508                     continue
509                 elif (line.state in states):
510                     lines.append(line.id)
511             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
512             if created_lines:
513                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
514         if not invoices:
515             for o in self.browse(cr, uid, ids, context=context):
516                 for i in o.invoice_ids:
517                     if i.state == 'draft':
518                         return i.id
519         for val in invoices.values():
520             if grouped:
521                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
522                 invoice_ref = ''
523                 for o, l in val:
524                     invoice_ref += o.name + '|'
525                     self.write(cr, uid, [o.id], {'state': 'progress'})
526                     if o.order_policy == 'picking':
527                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
528                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
529                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
530             else:
531                 for order, il in val:
532                     res = self._make_invoice(cr, uid, order, il, context=context)
533                     invoice_ids.append(res)
534                     self.write(cr, uid, [order.id], {'state': 'progress'})
535                     if order.order_policy == 'picking':
536                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
537                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
538         return res
539
540     def action_invoice_cancel(self, cr, uid, ids, context=None):
541         if context is None:
542             context = {}
543         for sale in self.browse(cr, uid, ids, context=context):
544             for line in sale.order_line:
545                 #
546                 # Check if the line is invoiced (has asociated invoice
547                 # lines from non-cancelled invoices).
548                 #
549                 invoiced = False
550                 for iline in line.invoice_lines:
551                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
552                         invoiced = True
553                         break
554                 # Update the line (only when needed)
555                 if line.invoiced != invoiced:
556                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
557         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
558         return True
559
560     def action_invoice_end(self, cr, uid, ids, context=None):
561         for order in self.browse(cr, uid, ids, context=context):
562             #
563             # Update the sale order lines state (and invoiced flag).
564             #
565             for line in order.order_line:
566                 vals = {}
567                 #
568                 # Check if the line is invoiced (has asociated invoice
569                 # lines from non-cancelled invoices).
570                 #
571                 invoiced = False
572                 for iline in line.invoice_lines:
573                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
574                         invoiced = True
575                         break
576                 if line.invoiced != invoiced:
577                     vals['invoiced'] = invoiced
578                 # If the line was in exception state, now it gets confirmed.
579                 if line.state == 'exception':
580                     vals['state'] = 'confirmed'
581                 # Update the line (only when needed).
582                 if vals:
583                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
584             #
585             # Update the sales order state.
586             #
587             if order.state == 'invoice_except':
588                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
589         return True
590
591     def action_cancel(self, cr, uid, ids, context=None):
592         wf_service = netsvc.LocalService("workflow")
593         if context is None:
594             context = {}
595         sale_order_line_obj = self.pool.get('sale.order.line')
596         proc_obj = self.pool.get('procurement.order')
597         for sale in self.browse(cr, uid, ids, context=context):
598             for pick in sale.picking_ids:
599                 if pick.state not in ('draft', 'cancel'):
600                     raise osv.except_osv(
601                         _('Could not cancel sales order !'),
602                         _('You must first cancel all picking attached to this sales order.'))
603                 if pick.state == 'cancel':
604                     for mov in pick.move_lines:
605                         proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
606                         if proc_ids:
607                             for proc in proc_ids:
608                                 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
609             for r in self.read(cr, uid, ids, ['picking_ids']):
610                 for pick in r['picking_ids']:
611                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
612             for inv in sale.invoice_ids:
613                 if inv.state not in ('draft', 'cancel'):
614                     raise osv.except_osv(
615                         _('Could not cancel this sales order !'),
616                         _('You must first cancel all invoices attached to this sales order.'))
617             for r in self.read(cr, uid, ids, ['invoice_ids']):
618                 for inv in r['invoice_ids']:
619                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
620             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
621                     {'state': 'cancel'})
622             message = _("The sales order '%s' has been cancelled.") % (sale.name,)
623             self.log(cr, uid, sale.id, message)
624         self.write(cr, uid, ids, {'state': 'cancel'})
625         return True
626
627     def action_wait(self, cr, uid, ids, context=None):
628         for o in self.browse(cr, uid, ids):
629             if not o.order_line:
630                 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
631             if (o.order_policy == 'manual'):
632                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
633             else:
634                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
635             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
636             message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
637             self.log(cr, uid, o.id, message)
638         return True
639
640     def procurement_lines_get(self, cr, uid, ids, *args):
641         res = []
642         for order in self.browse(cr, uid, ids, context={}):
643             for line in order.order_line:
644                 if line.procurement_id:
645                     res.append(line.procurement_id.id)
646         return res
647
648     # if mode == 'finished':
649     #   returns True if all lines are done, False otherwise
650     # if mode == 'canceled':
651     #   returns True if there is at least one canceled line, False otherwise
652     def test_state(self, cr, uid, ids, mode, *args):
653         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
654         finished = True
655         canceled = False
656         notcanceled = False
657         write_done_ids = []
658         write_cancel_ids = []
659         for order in self.browse(cr, uid, ids, context={}):
660             for line in order.order_line:
661                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
662                     if line.state != 'done':
663                         write_done_ids.append(line.id)
664                 else:
665                     finished = False
666                 if line.procurement_id:
667                     if (line.procurement_id.state == 'cancel'):
668                         canceled = True
669                         if line.state != 'exception':
670                             write_cancel_ids.append(line.id)
671                     else:
672                         notcanceled = True
673         if write_done_ids:
674             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
675         if write_cancel_ids:
676             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
677
678         if mode == 'finished':
679             return finished
680         elif mode == 'canceled':
681             return canceled
682             if notcanceled:
683                 return False
684             return canceled
685
686     def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
687         return {
688             'name': line.name,
689             'origin': order.name,
690             'date_planned': date_planned,
691             'product_id': line.product_id.id,
692             'product_qty': line.product_uom_qty,
693             'product_uom': line.product_uom.id,
694             'product_uos_qty': (line.product_uos and line.product_uos_qty)\
695                     or line.product_uom_qty,
696             'product_uos': (line.product_uos and line.product_uos.id)\
697                     or line.product_uom.id,
698             'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
699             'procure_method': line.type,
700             'move_id': move_id,
701             'company_id': order.company_id.id,
702             'note': line.notes
703         }
704
705     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
706         location_id = order.shop_id.warehouse_id.lot_stock_id.id
707         output_id = order.shop_id.warehouse_id.lot_output_id.id
708         return {
709             'name': line.name[:250],
710             'picking_id': picking_id,
711             'product_id': line.product_id.id,
712             'date': date_planned,
713             'date_expected': date_planned,
714             'product_qty': line.product_uom_qty,
715             'product_uom': line.product_uom.id,
716             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
717             'product_uos': (line.product_uos and line.product_uos.id)\
718                     or line.product_uom.id,
719             'product_packaging': line.product_packaging.id,
720             'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
721             'location_id': location_id,
722             'location_dest_id': output_id,
723             'sale_line_id': line.id,
724             'tracking_id': False,
725             'state': 'draft',
726             #'state': 'waiting',
727             'note': line.notes,
728             'company_id': order.company_id.id,
729             'price_unit': line.product_id.standard_price or 0.0
730         }
731
732     def _prepare_order_picking(self, cr, uid, order, context=None):
733         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
734         return {
735             'name': pick_name,
736             'origin': order.name,
737             'date': order.date_order,
738             'type': 'out',
739             'state': 'auto',
740             'move_type': order.picking_policy,
741             'sale_id': order.id,
742             'address_id': order.partner_shipping_id.id,
743             'note': order.note,
744             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
745             'company_id': order.company_id.id,
746         }
747
748     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
749         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
750         """
751         Define ship_recreate for process after shipping exception
752         param order: sale order to which the order lines belong
753         param line: sale order line records to procure
754         param move_id: the ID of stock move
755         param proc_id: the ID of procurement
756         """
757         move_obj = self.pool.get('stock.move')
758         if order.state == 'shipping_except':
759             for pick in order.picking_ids:
760                 for move in pick.move_lines:
761                     if move.state == 'cancel':
762                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
763                         if mov_ids:
764                             for mov in move_obj.browse(cr, uid, mov_ids):
765                                 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
766                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
767                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
768         return True
769
770     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
771         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)    
772         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
773         return date_planned
774
775     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
776         """Create the required procurements to supply sale order lines, also connecting
777         the procurements to appropriate stock moves in order to bring the goods to the
778         sale order's requested location.
779
780         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
781         a standard outgoing picking will be created to wrap the stock moves, as returned
782         by :meth:`~._prepare_order_picking`.
783
784         Modules that wish to customize the procurements or partition the stock moves over
785         multiple stock pickings may override this method and call ``super()`` with
786         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
787
788         :param browse_record order: sale order to which the order lines belong
789         :param list(browse_record) order_lines: sale order line records to procure
790         :param int picking_id: optional ID of a stock picking to which the created stock moves
791                                will be added. A new picking will be created if ommitted.
792         :return: True
793         """
794         move_obj = self.pool.get('stock.move')
795         picking_obj = self.pool.get('stock.picking')
796         procurement_obj = self.pool.get('procurement.order')
797         proc_ids = []
798
799         for line in order_lines:
800             if line.state == 'done':
801                 continue
802
803             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
804
805             if line.product_id:
806                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
807                     if not picking_id:
808                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
809                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
810                 else:
811                     # a service has no stock move
812                     move_id = False
813
814                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
815                 proc_ids.append(proc_id)
816                 line.write({'procurement_id': proc_id})
817                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
818
819         wf_service = netsvc.LocalService("workflow")
820         if picking_id:
821             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
822
823         for proc_id in proc_ids:
824             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
825
826         val = {}
827         if order.state == 'shipping_except':
828             val['state'] = 'progress'
829             val['shipped'] = False
830
831             if (order.order_policy == 'manual'):
832                 for line in order.order_line:
833                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
834                         val['state'] = 'manual'
835                         break
836         order.write(val)
837         return True
838
839     def action_ship_create(self, cr, uid, ids, context=None):
840         for order in self.browse(cr, uid, ids, context=context):
841             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
842         return True
843
844     def action_ship_end(self, cr, uid, ids, context=None):
845         for order in self.browse(cr, uid, ids, context=context):
846             val = {'shipped': True}
847             if order.state == 'shipping_except':
848                 val['state'] = 'progress'
849                 if (order.order_policy == 'manual'):
850                     for line in order.order_line:
851                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
852                             val['state'] = 'manual'
853                             break
854             for line in order.order_line:
855                 towrite = []
856                 if line.state == 'exception':
857                     towrite.append(line.id)
858                 if towrite:
859                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
860             self.write(cr, uid, [order.id], val)
861         return True
862
863     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
864         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
865         for inv in invs:
866             part = inv['partner_id'] and inv['partner_id'][0]
867             pr = inv['amount_untaxed'] or 0.0
868             partnertype = 'customer'
869             eventtype = 'sale'
870             event = {
871                 'name': 'Order: '+name,
872                 'som': False,
873                 'description': 'Order '+str(inv['id']),
874                 'document': '',
875                 'partner_id': part,
876                 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
877                 'user_id': uid,
878                 'partner_type': partnertype,
879                 'probability': 1.0,
880                 'planned_revenue': pr,
881                 'planned_cost': 0.0,
882                 'type': eventtype
883             }
884             self.pool.get('res.partner.event').create(cr, uid, event)
885
886     def has_stockable_products(self, cr, uid, ids, *args):
887         for order in self.browse(cr, uid, ids):
888             for order_line in order.order_line:
889                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
890                     return True
891         return False
892 sale_order()
893
894 # TODO add a field price_unit_uos
895 # - update it on change product and unit price
896 # - use it in report if there is a uos
897 class sale_order_line(osv.osv):
898
899     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
900         tax_obj = self.pool.get('account.tax')
901         cur_obj = self.pool.get('res.currency')
902         res = {}
903         if context is None:
904             context = {}
905         for line in self.browse(cr, uid, ids, context=context):
906             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
907             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)
908             cur = line.order_id.pricelist_id.currency_id
909             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
910         return res
911
912     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
913         res = {}
914         for line in self.browse(cr, uid, ids, context=context):
915             try:
916                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
917             except:
918                 res[line.id] = 1
919         return res
920
921     def _get_uom_id(self, cr, uid, *args):
922         try:
923             proxy = self.pool.get('ir.model.data')
924             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
925             return result[1]
926         except Exception, ex:
927             return False
928
929     _name = 'sale.order.line'
930     _description = 'Sales Order Line'
931     _columns = {
932         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
933         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
934         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
935         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
936         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
937         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
938         'invoiced': fields.boolean('Invoiced', readonly=True),
939         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
940         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
941         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
942         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
943         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
944             help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
945         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
946         'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
947         'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
948         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
949         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
950         'product_uos': fields.many2one('product.uom', 'Product UoS'),
951         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
952         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
953         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
954         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
955         'notes': fields.text('Notes'),
956         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
957         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
958                 help='* The \'Draft\' state is set when the related sales order in draft state. \
959                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
960                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
961                     \n* The \'Done\' state is set when the sales order line has been picked. \
962                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
963         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
964         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
965         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
966     }
967     _order = 'sequence, id'
968     _defaults = {
969         'product_uom' : _get_uom_id,
970         'discount': 0.0,
971         'delay': 0.0,
972         'product_uom_qty': 1,
973         'product_uos_qty': 1,
974         'sequence': 10,
975         'invoiced': 0,
976         'state': 'draft',
977         'type': 'make_to_stock',
978         'product_packaging': False,
979         'price_unit': 0.0,
980     }
981
982     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
983         """ Builds the invoice line dict from a sale order line
984             @param line: sale order line object
985             @param account_id: the id of the account to force eventually (the method is used for picking return including service)
986             @return: dict that will be used to create the invoice line
987         """
988
989         def _get_line_qty(line):
990             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
991                 if line.product_uos:
992                     return line.product_uos_qty or 0.0
993                 return line.product_uom_qty
994             else:
995                 return self.pool.get('procurement.order').quantity_get(cr, uid,
996                         line.procurement_id.id, context=context)
997
998         def _get_line_uom(line):
999             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1000                 if line.product_uos:
1001                     return line.product_uos.id
1002                 return line.product_uom.id
1003             else:
1004                 return self.pool.get('procurement.order').uom_get(cr, uid,
1005                         line.procurement_id.id, context=context)
1006
1007         if not line.invoiced:
1008             if not account_id:
1009                 if line.product_id:
1010                     account_id = line.product_id.product_tmpl_id.property_account_income.id
1011                     if not account_id:
1012                         account_id = line.product_id.categ_id.property_account_income_categ.id
1013                     if not account_id:
1014                         raise osv.except_osv(_('Error !'),
1015                                 _('There is no income account defined for this product: "%s" (id:%d)') % \
1016                                     (line.product_id.name, line.product_id.id,))
1017                 else:
1018                     prop = self.pool.get('ir.property').get(cr, uid,
1019                             'property_account_income_categ', 'product.category',
1020                             context=context)
1021                     account_id = prop and prop.id or False
1022             uosqty = _get_line_qty(line)
1023             uos_id = _get_line_uom(line)
1024             pu = 0.0
1025             if uosqty:
1026                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1027                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1028             fpos = line.order_id.fiscal_position or False
1029             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1030             if not account_id:
1031                 raise osv.except_osv(_('Error !'),
1032                             _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1033             return {
1034                 'name': line.name,
1035                 'origin': line.order_id.name,
1036                 'account_id': account_id,
1037                 'price_unit': pu,
1038                 'quantity': uosqty,
1039                 'discount': line.discount,
1040                 'uos_id': uos_id,
1041                 'product_id': line.product_id.id or False,
1042                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1043                 'note': line.notes,
1044                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1045             }
1046
1047         return False
1048
1049     def invoice_line_create(self, cr, uid, ids, context=None):
1050         if context is None:
1051             context = {}
1052
1053         create_ids = []
1054         sales = set()
1055         for line in self.browse(cr, uid, ids, context=context):
1056             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1057             if vals:
1058                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1059                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1060                 self.write(cr, uid, [line.id], {'invoiced': True})
1061                 sales.add(line.order_id.id)
1062                 create_ids.append(inv_id)
1063         # Trigger workflow events
1064         wf_service = netsvc.LocalService("workflow")
1065         for sale_id in sales:
1066             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1067         return create_ids
1068
1069     def button_cancel(self, cr, uid, ids, context=None):
1070         for line in self.browse(cr, uid, ids, context=context):
1071             if line.invoiced:
1072                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1073             for move_line in line.move_ids:
1074                 if move_line.state != 'cancel':
1075                     raise osv.except_osv(
1076                             _('Could not cancel sales order line!'),
1077                             _('You must first cancel stock moves attached to this sales order line.'))
1078         return self.write(cr, uid, ids, {'state': 'cancel'})
1079
1080     def button_confirm(self, cr, uid, ids, context=None):
1081         return self.write(cr, uid, ids, {'state': 'confirmed'})
1082
1083     def button_done(self, cr, uid, ids, context=None):
1084         wf_service = netsvc.LocalService("workflow")
1085         res = self.write(cr, uid, ids, {'state': 'done'})
1086         for line in self.browse(cr, uid, ids, context=context):
1087             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1088         return res
1089
1090     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1091         product_obj = self.pool.get('product.product')
1092         if not product_id:
1093             return {'value': {'product_uom': product_uos,
1094                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1095
1096         product = product_obj.browse(cr, uid, product_id)
1097         value = {
1098             'product_uom': product.uom_id.id,
1099         }
1100         # FIXME must depend on uos/uom of the product and not only of the coeff.
1101         try:
1102             value.update({
1103                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1104                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1105             })
1106         except ZeroDivisionError:
1107             pass
1108         return {'value': value}
1109
1110     def copy_data(self, cr, uid, id, default=None, context=None):
1111         if not default:
1112             default = {}
1113         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1114         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1115
1116     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1117                                    partner_id=False, packaging=False, flag=False, context=None):
1118         if not product:
1119             return {'value': {'product_packaging': False}}
1120         product_obj = self.pool.get('product.product')
1121         product_uom_obj = self.pool.get('product.uom')
1122         pack_obj = self.pool.get('product.packaging')
1123         warning = {}
1124         result = {}
1125         warning_msgs = ''
1126         if flag:
1127             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1128                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1129                     packaging=packaging, flag=False, context=context)
1130             warning_msgs = res.get('warning') and res['warning']['message']
1131
1132         products = product_obj.browse(cr, uid, product, context=context)
1133         if not products.packaging:
1134             packaging = result['product_packaging'] = False
1135         elif not packaging and products.packaging and not flag:
1136             packaging = products.packaging[0].id
1137             result['product_packaging'] = packaging
1138
1139         if packaging:
1140             default_uom = products.uom_id and products.uom_id.id
1141             pack = pack_obj.browse(cr, uid, packaging, context=context)
1142             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1143 #            qty = qty - qty % q + q
1144             if qty and (q and not (qty % q) == 0):
1145                 ean = pack.ean or _('(n/a)')
1146                 qty_pack = pack.qty
1147                 type_ul = pack.ul
1148                 if not warning_msgs:
1149                     warn_msg = _("You selected a quantity of %d Units.\n"
1150                                 "But it's not compatible with the selected packaging.\n"
1151                                 "Here is a proposition of quantities according to the packaging:\n"
1152                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1153                                     (qty, ean, qty_pack, type_ul.name)
1154                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1155                 warning = {
1156                        'title': _('Configuration Error !'),
1157                        'message': warning_msgs
1158                 }
1159             result['product_uom_qty'] = qty
1160
1161         return {'value': result, 'warning': warning}
1162
1163     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1164             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1165             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1166         context = context or {}
1167         lang = lang or context.get('lang',False)
1168         if not  partner_id:
1169             raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.'))
1170         warning = {}
1171         product_uom_obj = self.pool.get('product.uom')
1172         partner_obj = self.pool.get('res.partner')
1173         product_obj = self.pool.get('product.product')
1174         context = {'lang': lang, 'partner_id': partner_id}
1175         if partner_id:
1176             lang = partner_obj.browse(cr, uid, partner_id).lang
1177         context_partner = {'lang': lang, 'partner_id': partner_id}
1178
1179         if not product:
1180             return {'value': {'th_weight': 0, 'product_packaging': False,
1181                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1182                    'product_uos': []}}
1183         if not date_order:
1184             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1185
1186         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1187         result = res.get('value', {})
1188         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1189         product_obj = product_obj.browse(cr, uid, product, context=context)
1190
1191         uom2 = False
1192         if uom:
1193             uom2 = product_uom_obj.browse(cr, uid, uom)
1194             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1195                 uom = False
1196         if uos:
1197             if product_obj.uos_id:
1198                 uos2 = product_uom_obj.browse(cr, uid, uos)
1199                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1200                     uos = False
1201             else:
1202                 uos = False
1203         if product_obj.description_sale:
1204             result['notes'] = product_obj.description_sale
1205         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1206         if update_tax: #The quantity only have changed
1207             result['delay'] = (product_obj.sale_delay or 0.0)
1208             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1209             result.update({'type': product_obj.procure_method})
1210
1211         if not flag:
1212             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1213         domain = {}
1214         if (not uom) and (not uos):
1215             result['product_uom'] = product_obj.uom_id.id
1216             if product_obj.uos_id:
1217                 result['product_uos'] = product_obj.uos_id.id
1218                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1219                 uos_category_id = product_obj.uos_id.category_id.id
1220             else:
1221                 result['product_uos'] = False
1222                 result['product_uos_qty'] = qty
1223                 uos_category_id = False
1224             result['th_weight'] = qty * product_obj.weight
1225             domain = {'product_uom':
1226                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1227                         'product_uos':
1228                         [('category_id', '=', uos_category_id)]}
1229
1230         elif uos and not uom: # only happens if uom is False
1231             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1232             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1233             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1234         elif uom: # whether uos is set or not
1235             default_uom = product_obj.uom_id and product_obj.uom_id.id
1236             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1237             if product_obj.uos_id:
1238                 result['product_uos'] = product_obj.uos_id.id
1239                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1240             else:
1241                 result['product_uos'] = False
1242                 result['product_uos_qty'] = qty
1243             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1244
1245         if not uom2:
1246             uom2 = product_obj.uom_id
1247         if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1248           and (product_obj.procure_method=='make_to_stock'):
1249             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1250                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1251                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1252                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1253             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1254         # get unit price
1255
1256         if not pricelist:
1257             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1258                     'Please set one before choosing a product.')
1259             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1260         else:
1261             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1262                     product, qty or 1.0, partner_id, {
1263                         'uom': uom or result.get('product_uom'),
1264                         'date': date_order,
1265                         })[pricelist]
1266             if price is False:
1267                 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1268                         "You have to change either the product, the quantity or the pricelist.")
1269
1270                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1271             else:
1272                 result.update({'price_unit': price})
1273         if warning_msgs:
1274             warning = {
1275                        'title': _('Configuration Error !'),
1276                        'message' : warning_msgs
1277                     }
1278         return {'value': result, 'domain': domain, 'warning': warning}
1279
1280     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1281             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1282             lang=False, update_tax=True, date_order=False, context=None):
1283         context = context or {}
1284         lang = lang or ('lang' in context and context['lang'])
1285         res = self.product_id_change(cursor, user, ids, pricelist, product,
1286                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1287                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1288                 date_order=date_order, context=context)
1289         if 'product_uom' in res['value']:
1290             del res['value']['product_uom']
1291         if not uom:
1292             res['value']['price_unit'] = 0.0
1293         return res
1294
1295     def unlink(self, cr, uid, ids, context=None):
1296         if context is None:
1297             context = {}
1298         """Allows to delete sales order lines in draft,cancel states"""
1299         for rec in self.browse(cr, uid, ids, context=context):
1300             if rec.state not in ['draft', 'cancel']:
1301                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1302         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1303
1304 sale_order_line()
1305
1306 class sale_config_picking_policy(osv.osv_memory):
1307     _name = 'sale.config.picking_policy'
1308     _inherit = 'res.config'
1309
1310     _columns = {
1311         'name': fields.char('Name', size=64),
1312         'sale_orders': fields.boolean('Based on Sales Orders',),
1313         'deli_orders': fields.boolean('Based on Delivery Orders'),
1314         'task_work': fields.boolean('Based on Tasks\' Work'),
1315         'timesheet': fields.boolean('Based on Timesheet'),
1316         'order_policy': fields.selection([
1317             ('manual', 'Invoice Based on Sales Orders'),
1318             ('picking', 'Invoice Based on Deliveries'),
1319         ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1320         'charge_delivery': fields.boolean('Do you charge the delivery?'),
1321         'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1322     }
1323     _defaults = {
1324         'order_policy': 'manual',
1325         'time_unit': lambda self, cr, uid, c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c) and self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c)[0] or False,
1326     }
1327
1328     def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1329         res = {}
1330         if sale:
1331             res.update({'order_policy': 'manual'})
1332         elif deli:
1333             res.update({'order_policy': 'picking'})
1334         return {'value':res}
1335
1336     def execute(self, cr, uid, ids, context=None):
1337         ir_values_obj = self.pool.get('ir.values')
1338         data_obj = self.pool.get('ir.model.data')
1339         menu_obj = self.pool.get('ir.ui.menu')
1340         module_obj = self.pool.get('ir.module.module')
1341         module_upgrade_obj = self.pool.get('base.module.upgrade')
1342         module_name = []
1343
1344         group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1345
1346         wizard = self.browse(cr, uid, ids)[0]
1347
1348         if wizard.sale_orders:
1349             menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1350             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1351
1352         if wizard.deli_orders:
1353             menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1354             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1355
1356         if wizard.task_work:
1357             module_name.append('project_timesheet')
1358             module_name.append('project_mrp')
1359             module_name.append('account_analytic_analysis')
1360
1361         if wizard.timesheet:
1362             module_name.append('account_analytic_analysis')
1363
1364         if wizard.charge_delivery:
1365             module_name.append('delivery')
1366
1367         if len(module_name):
1368             module_ids = []
1369             need_install = False
1370             module_ids = []
1371             for module in module_name:
1372                 data_id = module_obj.name_search(cr, uid , module, [], '=')
1373                 module_ids.append(data_id[0][0])
1374
1375             for module in module_obj.browse(cr, uid, module_ids):
1376                 if module.state == 'uninstalled':
1377                     module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1378                     need_install = True
1379                     cr.commit()
1380             if need_install:
1381                 pooler.restart_pool(cr.dbname, update_module=True)[1]
1382
1383         if wizard.time_unit:
1384             prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1385             product_obj = self.pool.get('product.product')
1386             product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1387
1388         ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1389         if wizard.task_work and wizard.time_unit:
1390             company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1391             self.pool.get('res.company').write(cr, uid, [company_id], {
1392                 'project_time_mode_id': wizard.time_unit.id
1393             }, context=context)
1394
1395 sale_config_picking_policy()
1396
1397 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: