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