Cancelling sale order should affect Procurements,production orders
[odoo/odoo.git] / addons / sale / sale.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import time
24 import netsvc
25 from osv import fields, osv
26 from mx import DateTime
27 from tools import config
28 from tools.translate import _
29
30 class sale_shop(osv.osv):
31     _name = "sale.shop"
32     _description = "Sale Shop"
33     _columns = {
34         'name': fields.char('Shop Name', size=64, required=True),
35         'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
36         'payment_account_id': fields.many2many('account.account', 'sale_shop_account', 'shop_id', 'account_id', 'Payment Accounts'),
37         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
38         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39         'project_id': fields.many2one('account.analytic.account', 'Analytic Account'),
40     }
41 sale_shop()
42
43
44 def _incoterm_get(self, cr, uid, context={}):
45     cr.execute('select code, code||\', \'||name from stock_incoterms where active')
46     return cr.fetchall()
47
48
49 class sale_order(osv.osv):
50     _name = "sale.order"
51     _description = "Sale Order"
52
53     def copy(self, cr, uid, id, default=None, context={}):
54         if not default:
55             default = {}
56         default.update({
57             'state': 'draft',
58             'shipped': False,
59             'invoice_ids': [],
60             'picking_ids': [],
61             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
62         })
63         return super(sale_order, self).copy(cr, uid, id, default, context)
64
65     def _amount_line_tax(self, cr, uid, line, context={}):
66         val = 0.0
67         for c in self.pool.get('account.tax').compute(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):
68             val += c['amount']
69         return val
70
71     def _amount_all(self, cr, uid, ids, field_name, arg, context):
72         res = {}
73         cur_obj = self.pool.get('res.currency')
74         for order in self.browse(cr, uid, ids):
75             res[order.id] = {
76                 'amount_untaxed': 0.0,
77                 'amount_tax': 0.0,
78                 'amount_total': 0.0,
79             }
80             val = val1 = 0.0
81             cur = order.pricelist_id.currency_id
82             for line in order.order_line:
83                 val1 += line.price_subtotal
84                 val += self._amount_line_tax(cr, uid, line, context)
85             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
86             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
87             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
88         return res
89
90     def _picked_rate(self, cr, uid, ids, name, arg, context=None):
91         if not ids:
92             return {}
93         res = {}
94         for id in ids:
95             res[id] = [0.0, 0.0]
96         cr.execute('''SELECT
97                 p.sale_id,sum(m.product_qty), mp.state as mp_state, m.state as state, p.type as tp
98             FROM
99                 stock_move m
100             LEFT JOIN
101                 stock_picking p on (p.id=m.picking_id)
102             LEFT JOIN
103                 mrp_procurement mp on (mp.move_id=m.id)
104             WHERE
105                 p.sale_id in %s
106             GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
107
108         for oid, nbr, state, move_state, type_pick in cr.fetchall():
109             if state == 'cancel':
110                 continue
111             res[oid][1] += nbr or 0.0
112             if state == 'done' or move_state == 'done':
113                 res[oid][0] += nbr or 0.0
114             
115             if type_pick == 'in':#which  clearly means that this is a returned picking
116                 res[oid][1] -= 2*nbr or 0.0 # Deducting the return picking qty
117                 if state == 'done' or move_state == 'done':
118                     nbr += nbr
119                 res[oid][0] -= nbr or 0.0
120                 
121         for r in res:
122             if not res[r][1]:
123                 res[r] = 0.0
124             else:
125                 res[r] = 100.0 * res[r][0] / res[r][1]
126         for order in self.browse(cr, uid, ids, context):
127             if order.shipped:
128                 res[order.id] = 100.0
129         return res
130
131     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
132         res = {}
133         for sale in self.browse(cursor, user, ids, context=context):
134             if sale.invoiced:
135                 res[sale.id] = 100.0
136                 continue
137             tot = 0.0
138             for invoice in sale.invoice_ids:
139                 if invoice.state not in ('draft', 'cancel'):
140                     tot += invoice.amount_untaxed
141
142             if tot:
143                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
144             else:
145                 res[sale.id] = 0.0
146         return res
147
148     def _invoiced(self, cursor, user, ids, name, arg, context=None):
149         res = {}
150         for sale in self.browse(cursor, user, ids, context=context):
151             res[sale.id] = True
152             for invoice in sale.invoice_ids:
153                 if invoice.state != 'paid':
154                     res[sale.id] = False
155                     break
156             if not sale.invoice_ids:
157                 res[sale.id] = False
158         return res
159
160     def _invoiced_search(self, cursor, user, obj, name, args, context):
161         if not len(args):
162             return []
163         clause = ''
164         no_invoiced = False
165         for arg in args:
166             if arg[1] == '=':
167                 if arg[2]:
168                     clause += 'AND inv.state = \'paid\''
169                 else:
170                     clause += 'AND inv.state <> \'paid\''
171                     no_invoiced = True
172         cursor.execute('SELECT rel.order_id ' \
173                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
174                 'WHERE rel.invoice_id = inv.id ' + clause)
175         res = cursor.fetchall()
176         if no_invoiced:
177             cursor.execute('SELECT sale.id ' \
178                     'FROM sale_order AS sale ' \
179                     'WHERE sale.id NOT IN ' \
180                         '(SELECT rel.order_id ' \
181                         'FROM sale_order_invoice_rel AS rel)')
182             res.extend(cursor.fetchall())
183         if not res:
184             return [('id', '=', 0)]
185         return [('id', 'in', [x[0] for x in res])]
186
187     def _get_order(self, cr, uid, ids, context={}):
188         result = {}
189         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
190             result[line.order_id.id] = True
191         return result.keys()
192
193     _columns = {
194         'name': fields.char('Order Reference', size=64, required=True, select=True),
195         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
196         'origin': fields.char('Origin', size=64),
197         'client_order_ref': fields.char('Customer Ref', size=64),
198
199         'state': fields.selection([
200             ('draft', 'Quotation'),
201             ('waiting_date', 'Waiting Schedule'),
202             ('manual', 'Manual In Progress'),
203             ('progress', 'In Progress'),
204             ('shipping_except', 'Shipping Exception'),
205             ('invoice_except', 'Invoice Exception'),
206             ('done', 'Done'),
207             ('cancel', 'Cancelled')
208             ], 'Order State', readonly=True, help="Gives the state of the quotation or sale order. The exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the packing list process (Shipping Exception). The 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the date 'Date Ordered'.", select=True),
209         'date_order': fields.date('Date Ordered', required=True, readonly=True, states={'draft': [('readonly', False)]}),
210
211         'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
212         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
213         'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
214         '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 that requested the order or quotation."),
215         'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
216
217         'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
218         'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
219             'Packing Policy', required=True, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
220         'order_policy': fields.selection([
221             ('prepaid', 'Payment Before Delivery'),
222             ('manual', 'Shipping & Manual Invoice'),
223             ('postpaid', 'Invoice on Order After Delivery'),
224             ('picking', 'Invoice from the Packing'),
225         ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
226                     help="""The Shipping Policy is used to synchronise invoice and delivery operations.
227   - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
228   - The 'Shipping & Manual Invoice' will create the packing order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice.
229   - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all packing lists have been finished.
230   - The 'Invoice from the packing' choice is used to create an invoice during the packing process."""),
231         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}),
232         'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
233
234         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
235         'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', help="This is the list of invoices that have been generated for this sale order. The same sale order may have been invoiced in several times (by line for example)."),
236         'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Packing', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
237         'shipped': fields.boolean('Picked', readonly=True),
238         'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
239         'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
240         'invoiced': fields.function(_invoiced, method=True, string='Paid',
241             fnct_search=_invoiced_search, type='boolean'),
242         'note': fields.text('Notes'),
243
244         'amount_untaxed': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Untaxed Amount',
245             store = {
246                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
247                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
248             },
249             multi='sums'),
250         'amount_tax': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Taxes',
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'),
256         'amount_total': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Total',
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'),
262
263         '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 invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True),
264         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
265         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position')
266     }
267     _defaults = {
268         'picking_policy': lambda *a: 'direct',
269         'date_order': lambda *a: time.strftime('%Y-%m-%d'),
270         'order_policy': lambda *a: 'manual',
271         'state': lambda *a: 'draft',
272         'user_id': lambda obj, cr, uid, context: uid,
273         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
274         'invoice_quantity': lambda *a: 'order',
275         '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'],
276         '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'],
277         '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'],
278 #        'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist.id,
279     }
280     _order = 'name desc'
281
282     # Form filling
283     def unlink(self, cr, uid, ids, context=None):
284         sale_orders = self.read(cr, uid, ids, ['state'])
285         unlink_ids = []
286         for s in sale_orders:
287             if s['state'] in ['draft', 'cancel']:
288                 unlink_ids.append(s['id'])
289             else:
290                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
291         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
292
293     def onchange_shop_id(self, cr, uid, ids, shop_id):
294         v = {}
295         if shop_id:
296             shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
297             v['project_id'] = shop.project_id.id
298             # Que faire si le client a une pricelist a lui ?
299             if shop.pricelist_id.id:
300                 v['pricelist_id'] = shop.pricelist_id.id
301             #v['payment_default_id']=shop.payment_default_id.id
302         return {'value': v}
303
304     def action_cancel_draft(self, cr, uid, ids, *args):
305         if not len(ids):
306             return False
307         cr.execute('select id from sale_order_line where order_id in %s', (tuple(ids),))
308         line_ids = map(lambda x: x[0], cr.fetchall())
309         self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
310         self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
311         wf_service = netsvc.LocalService("workflow")
312         for inv_id in ids:
313             # Deleting the existing instance of workflow for SO
314             wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
315             wf_service.trg_create(uid, 'sale.order', inv_id, cr)
316         return True
317
318     def onchange_partner_id(self, cr, uid, ids, part):
319         if not part:
320             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
321
322         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
323         part = self.pool.get('res.partner').browse(cr, uid, part)
324         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
325         payment_term = part.property_payment_term and part.property_payment_term.id or False
326         fiscal_position = part.property_account_position and part.property_account_position.id or False
327         dedicated_salesman = part.user_id and part.user_id.id or uid
328
329         val = {
330             'partner_invoice_id': addr['invoice'],
331             'partner_order_id': addr['contact'],
332             'partner_shipping_id': addr['delivery'],
333             'payment_term': payment_term,
334             'fiscal_position': fiscal_position,
335             'user_id': dedicated_salesman,
336         }
337
338         if pricelist:
339             val['pricelist_id'] = pricelist
340
341         return {'value': val}
342
343     def shipping_policy_change(self, cr, uid, ids, policy, context={}):
344         if not policy:
345             return {}
346         inv_qty = 'order'
347         if policy == 'prepaid':
348             inv_qty = 'order'
349         elif policy == 'picking':
350             inv_qty = 'procurement'
351         return {'value': {'invoice_quantity': inv_qty}}
352
353     def write(self, cr, uid, ids, vals, context=None):
354         if 'order_policy' in vals:
355             if vals['order_policy'] == 'prepaid':
356                 vals.update({'invoice_quantity': 'order'})
357             elif vals['order_policy'] == 'picking':
358                 vals.update({'invoice_quantity': 'procurement'})
359         return super(sale_order, self).write(cr, uid, ids, vals, context=context)
360
361     def create(self, cr, uid, vals, context={}):
362         if 'order_policy' in vals:
363             if vals['order_policy'] == 'prepaid':
364                 vals.update({'invoice_quantity': 'order'})
365             if vals['order_policy'] == 'picking':
366                 vals.update({'invoice_quantity': 'procurement'})
367         return super(sale_order, self).create(cr, uid, vals, context=context)
368
369     def button_dummy(self, cr, uid, ids, context={}):
370         return True
371
372 #FIXME: the method should return the list of invoices created (invoice_ids)
373 # and not the id of the last invoice created (res). The problem is that we
374 # cannot change it directly since the method is called by the sale order
375 # workflow and I suppose it expects a single id...
376     def _inv_get(self, cr, uid, order, context={}):
377         return {}
378
379     def _make_invoice(self, cr, uid, order, lines, context={}):
380         a = order.partner_id.property_account_receivable.id
381         if order.payment_term:
382             pay_term = order.payment_term.id
383         else:
384             pay_term = False
385         for preinv in order.invoice_ids:
386             if preinv.state not in ('cancel',):
387                 for preline in preinv.invoice_line:
388                     inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
389                     lines.append(inv_line_id)
390         inv = {
391             'name': order.client_order_ref or order.name,
392             'origin': order.name,
393             'type': 'out_invoice',
394             'reference': "P%dSO%d" % (order.partner_id.id, order.id),
395             'account_id': a,
396             'partner_id': order.partner_id.id,
397             'address_invoice_id': order.partner_invoice_id.id,
398             'address_contact_id': order.partner_order_id.id,
399             'invoice_line': [(6, 0, lines)],
400             'currency_id': order.pricelist_id.currency_id.id,
401             'comment': order.note,
402             'payment_term': pay_term,
403             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id
404         }
405         inv_obj = self.pool.get('account.invoice')
406         inv.update(self._inv_get(cr, uid, order))
407         inv_id = inv_obj.create(cr, uid, inv)
408         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
409         if data.get('value', False):
410             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
411         inv_obj.button_compute(cr, uid, [inv_id])
412         return inv_id
413
414     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception']):
415         res = False
416         invoices = {}
417         invoice_ids = []
418
419         for o in self.browse(cr, uid, ids):
420             lines = []
421             for line in o.order_line:
422                 if (line.state in states) and not line.invoiced:
423                     lines.append(line.id)
424             created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
425             if created_lines:
426                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
427
428         if not invoices:
429             for o in self.browse(cr, uid, ids):
430                 for i in o.invoice_ids:
431                     if i.state == 'draft':
432                         return i.id
433         picking_obj = self.pool.get('stock.picking')
434         for val in invoices.values():
435             if grouped:
436                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []))
437                 for o, l in val:
438                     self.write(cr, uid, [o.id], {'state': 'progress'})
439                     if o.order_policy == 'picking':
440                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
441                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
442             else:
443                 for order, il in val:
444                     res = self._make_invoice(cr, uid, order, il)
445                     invoice_ids.append(res)
446                     self.write(cr, uid, [order.id], {'state': 'progress'})
447                     if order.order_policy == 'picking':
448                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
449                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
450         return res
451
452     def action_invoice_cancel(self, cr, uid, ids, context=None):
453         for sale in self.browse(cr, uid, ids):
454             for line in sale.order_line:
455                 #
456                 # Check if the line is invoiced (has asociated invoice
457                 # lines from non-cancelled invoices).
458                 #
459                 invoiced = False
460                 for iline in line.invoice_lines:
461                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
462                         invoiced = True
463                         break
464                 # Update the line (only when needed)
465                 if line.invoiced != invoiced:
466                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
467         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
468         return True
469     
470     def action_invoice_end(self, cr, uid, ids, context=None):
471         for order in self.browse(cr, uid, ids, context=context):
472             #
473             # Update the sale order lines state (and invoiced flag).
474             #
475             for line in order.order_line:
476                 vals = {}
477                 #
478                 # Check if the line is invoiced (has asociated invoice
479                 # lines from non-cancelled invoices).
480                 #
481                 invoiced = False
482                 for iline in line.invoice_lines:
483                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
484                         invoiced = True
485                         break
486                 if line.invoiced != invoiced:
487                     vals['invoiced'] = invoiced
488                 # If the line was in exception state, now it gets confirmed.
489                 if line.state == 'exception':
490                     vals['state'] = 'confirmed'
491                 # Update the line (only when needed).
492                 if vals:
493                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
494             #
495             # Update the sale order state.
496             #
497             if order.state == 'invoice_except':
498                 self.write(cr, uid, [order.id], {'state' : 'progress'}, context=context)
499             
500         return True
501     
502     def action_cancel(self, cr, uid, ids, context={}):
503         ok = True
504         wf_service = netsvc.LocalService("workflow")
505         sale_order_line_obj = self.pool.get('sale.order.line')
506         for sale in self.browse(cr, uid, ids):
507             for pick in sale.picking_ids:
508                 if pick.state not in ('draft', 'cancel'):
509                     raise osv.except_osv(
510                         _('Could not cancel sale order !'),
511                         _('You must first cancel all packing attached to this sale order.'))
512                 if pick.state == 'cancel':
513                     for mov in pick.move_lines:
514                         proc_ids = self.pool.get('mrp.procurement').search(cr, uid, [('move_id', '=', mov.id)])
515                         if proc_ids:
516                             for proc in proc_ids:
517                                 wf_service.trg_validate(uid, 'mrp.procurement', proc, 'button_check', cr)
518             for r in self.read(cr, uid, ids, ['picking_ids']):
519                 for pick in r['picking_ids']:
520                     wf_service = netsvc.LocalService("workflow")
521                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
522             for inv in sale.invoice_ids:
523                 if inv.state not in ('draft', 'cancel'):
524                     raise osv.except_osv(
525                         _('Could not cancel this sale order !'),
526                         _('You must first cancel all invoices attached to this sale order.'))
527             for r in self.read(cr, uid, ids, ['invoice_ids']):
528                 for inv in r['invoice_ids']:
529                     wf_service = netsvc.LocalService("workflow")
530                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
531             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
532                     {'state': 'cancel'})
533         self.write(cr, uid, ids, {'state': 'cancel'})
534         return True
535
536     def action_wait(self, cr, uid, ids, *args):
537         event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
538         event_obj = self.pool.get('res.partner.event')
539         for o in self.browse(cr, uid, ids):
540             if event_p:
541                 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
542                         'partner_id': o.partner_id.id,\
543                         'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
544                         'user_id': (o.user_id and o.user_id.id) or uid,\
545                         'partner_type': 'customer', 'probability': 1.0,\
546                         'planned_revenue': o.amount_untaxed})
547             if (o.order_policy == 'manual'):
548                 self.write(cr, uid, [o.id], {'state': 'manual'})
549             else:
550                 self.write(cr, uid, [o.id], {'state': 'progress'})
551             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
552
553     def procurement_lines_get(self, cr, uid, ids, *args):
554         res = []
555         for order in self.browse(cr, uid, ids, context={}):
556             for line in order.order_line:
557                 if line.procurement_id:
558                     res.append(line.procurement_id.id)
559         return res
560         
561     # if mode == 'finished':
562     #   returns True if all lines are done, False otherwise
563     # if mode == 'canceled':
564     #   returns True if there is at least one canceled line, False otherwise
565     def test_state(self, cr, uid, ids, mode, *args):
566         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
567         finished = True
568         canceled = False
569         notcanceled = False
570         write_done_ids = []
571         write_cancel_ids = []
572         for order in self.browse(cr, uid, ids, context={}):
573             for line in order.order_line:
574                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
575                     if line.state != 'done':
576                         write_done_ids.append(line.id)
577                 else:
578                     finished = False
579                 if line.procurement_id:
580                     if (line.procurement_id.state == 'cancel'):
581                         canceled = True
582                         if line.state != 'exception':
583                             write_cancel_ids.append(line.id)
584                     else:
585                         notcanceled = True
586         if write_done_ids:
587             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
588         if write_cancel_ids:
589             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
590
591         if mode == 'finished':
592             return finished
593         elif mode == 'canceled':
594             return canceled
595             if notcanceled:
596                 return False
597             return canceled
598
599     def action_ship_create(self, cr, uid, ids, *args):
600         picking_id = False
601         company = self.pool.get('res.users').browse(cr, uid, uid).company_id
602         picking_obj = self.pool.get('stock.picking')
603         sale_order_line_obj = self.pool.get('sale.order.line')
604         move_obj = self.pool.get('stock.move')
605         proc_obj = self.pool.get('mrp.procurement')
606         for order in self.browse(cr, uid, ids, context={}):
607             output_id = order.shop_id.warehouse_id.lot_output_id.id
608             picking_id = False
609             for line in order.order_line:
610                 proc_id = False
611                 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
612                 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
613                 if line.state == 'done':
614                     continue
615                 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
616                     location_id = order.shop_id.warehouse_id.lot_stock_id.id
617                     if not picking_id:
618                         loc_dest_id = order.partner_id.property_stock_customer.id
619                         picking_id = picking_obj.create(cr, uid, {
620                             'origin': order.name,
621                             'type': 'out',
622                             'state': 'auto',
623                             'move_type': order.picking_policy,
624                             'sale_id': order.id,
625                             'address_id': order.partner_shipping_id.id,
626                             'note': order.note,
627                             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
628
629                         })
630
631                     move_id = move_obj.create(cr, uid, {
632                         'name': line.name[:64],
633                         'picking_id': picking_id,
634                         'product_id': line.product_id.id,
635                         'date_planned': date_planned,
636                         'product_qty': line.product_uom_qty,
637                         'product_uom': line.product_uom.id,
638                         'product_uos_qty': line.product_uos_qty,
639                         'product_uos': (line.product_uos and line.product_uos.id)\
640                                 or line.product_uom.id,
641                         'product_packaging': line.product_packaging.id,
642                         'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
643                         'location_id': location_id,
644                         'location_dest_id': output_id,
645                         'sale_line_id': line.id,
646                         'tracking_id': False,
647                         'state': 'draft',
648                         #'state': 'waiting',
649                         'note': line.notes,
650                     })
651                     proc_id = proc_obj.create(cr, uid, {
652                         'name': order.name,
653                         'origin': order.name,
654                         'date_planned': date_planned,
655                         'product_id': line.product_id.id,
656                         'product_qty': line.product_uom_qty,
657                         'product_uom': line.product_uom.id,
658                         'product_uos_qty': (line.product_uos and line.product_uos_qty)\
659                                 or line.product_uom_qty,
660                         'product_uos': (line.product_uos and line.product_uos.id)\
661                                 or line.product_uom.id,
662                         'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
663                         'procure_method': line.type,
664                         'move_id': move_id,
665                         'property_ids': [(6, 0, [x.id for x in line.property_ids])],
666                     })
667                     wf_service = netsvc.LocalService("workflow")
668                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
669                     sale_order_line_obj.write(cr, uid, [line.id], {'procurement_id': proc_id})
670                 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
671                     proc_id = proc_obj.create(cr, uid, {
672                         'name': line.name,
673                         'origin': order.name,
674                         'date_planned': date_planned,
675                         'product_id': line.product_id.id,
676                         'product_qty': line.product_uom_qty,
677                         'product_uom': line.product_uom.id,
678                         'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
679                         'procure_method': line.type,
680                         'property_ids': [(6, 0, [x.id for x in line.property_ids])],
681                     })
682                     sale_order_line_obj.write(cr, uid, [line.id], {'procurement_id': proc_id})
683                     wf_service = netsvc.LocalService("workflow")
684                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
685                 else:
686                     #
687                     # No procurement because no product in the sale.order.line.
688                     #
689                     pass
690
691             val = {}
692             if picking_id:
693                 wf_service = netsvc.LocalService("workflow")
694                 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
695
696             if order.state == 'shipping_except':
697                 val['state'] = 'progress'
698
699                 if (order.order_policy == 'manual'):
700                     for line in order.order_line:
701                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
702                             val['state'] = 'manual'
703                             break
704             self.write(cr, uid, [order.id], val)
705
706         return True
707
708     def action_ship_end(self, cr, uid, ids, context={}):
709         for order in self.browse(cr, uid, ids):
710             val = {'shipped': True}
711             if order.state == 'shipping_except':
712                 val['state'] = 'progress'
713                 if (order.order_policy == 'manual'):
714                     for line in order.order_line:
715                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
716                             val['state'] = 'manual'
717                             break
718             for line in order.order_line:
719                 towrite = []
720                 if line.state == 'exception':
721                     towrite.append(line.id)
722                 if towrite:
723                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
724             self.write(cr, uid, [order.id], val)
725         return True
726
727     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
728         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
729         for inv in invs:
730             part = inv['partner_id'] and inv['partner_id'][0]
731             pr = inv['amount_untaxed'] or 0.0
732             partnertype = 'customer'
733             eventtype = 'sale'
734             event = {
735                 'name': 'Order: '+name,
736                 'som': False,
737                 'description': 'Order '+str(inv['id']),
738                 'document': '',
739                 'partner_id': part,
740                 'date': time.strftime('%Y-%m-%d'),
741                 'canal_id': False,
742                 'user_id': uid,
743                 'partner_type': partnertype,
744                 'probability': 1.0,
745                 'planned_revenue': pr,
746                 'planned_cost': 0.0,
747                 'type': eventtype
748             }
749             self.pool.get('res.partner.event').create(cr, uid, event)
750
751     def has_stockable_products(self, cr, uid, ids, *args):
752         for order in self.browse(cr, uid, ids):
753             for order_line in order.order_line:
754                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
755                     return True
756         return False
757 sale_order()
758
759 # TODO add a field price_unit_uos
760 # - update it on change product and unit price
761 # - use it in report if there is a uos
762 class sale_order_line(osv.osv):
763     def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
764         res = {}
765         for line in self.browse(cr, uid, ids):
766             res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
767         return res
768
769     def _amount_line(self, cr, uid, ids, field_name, arg, context):
770         res = {}
771         cur_obj = self.pool.get('res.currency')
772         for line in self.browse(cr, uid, ids):
773             res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
774             cur = line.order_id.pricelist_id.currency_id
775             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
776         return res
777
778     def _number_packages(self, cr, uid, ids, field_name, arg, context):
779         res = {}
780         for line in self.browse(cr, uid, ids):
781             try:
782                 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
783             except:
784                 res[line.id] = 1
785         return res
786
787     _name = 'sale.order.line'
788     _description = 'Sale Order line'
789     _columns = {
790         'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
791         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
792         'sequence': fields.integer('Sequence'),
793         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the the shipping of the products to the customer", readonly=True, states={'draft':[('readonly',False)]}),
794         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
795         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
796         'invoiced': fields.boolean('Invoiced', readonly=True),
797         'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
798         'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy'])), readonly=True, states={'draft':[('readonly',False)]}),
799         'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
800         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits=(16, int(config['price_accuracy']))),
801         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
802         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procure Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
803         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
804         'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
805         'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
806         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}),
807         'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
808         'product_uos': fields.many2one('product.uom', 'Product UoS'),
809         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
810         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
811         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
812         'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
813         'notes': fields.text('Notes'),
814         'th_weight': fields.float('Weight', readonly=True, states={'draft':[('readonly',False)]}),
815         'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'Status', required=True, readonly=True),
816         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer')
817     }
818     _order = 'sequence, id'
819     _defaults = {
820         'discount': lambda *a: 0.0,
821         'delay': lambda *a: 0.0,
822         'product_uom_qty': lambda *a: 1,
823         'product_uos_qty': lambda *a: 1,
824         'sequence': lambda *a: 10,
825         'invoiced': lambda *a: 0,
826         'state': lambda *a: 'draft',
827         'type': lambda *a: 'make_to_stock',
828         'product_packaging': lambda *a: False
829     }
830
831     def invoice_line_create(self, cr, uid, ids, context={}):
832         def _get_line_qty(line):
833             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
834                 if line.product_uos:
835                     return line.product_uos_qty or 0.0
836                 return line.product_uom_qty
837             else:
838                 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
839                         line.procurement_id.id, context)
840
841         def _get_line_uom(line):
842             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
843                 if line.product_uos:
844                     return line.product_uos.id
845                 return line.product_uom.id
846             else:
847                 return self.pool.get('mrp.procurement').uom_get(cr, uid,
848                         line.procurement_id.id, context)
849
850         create_ids = []
851         sales = {}
852         for line in self.browse(cr, uid, ids, context):
853             if not line.invoiced:
854                 if line.product_id:
855                     a = line.product_id.product_tmpl_id.property_account_income.id
856                     if not a:
857                         a = line.product_id.categ_id.property_account_income_categ.id
858                     if not a:
859                         raise osv.except_osv(_('Error !'),
860                                 _('There is no income account defined ' \
861                                         'for this product: "%s" (id:%d)') % \
862                                         (line.product_id.name, line.product_id.id,))
863                 else:
864                     a = self.pool.get('ir.property').get(cr, uid,
865                             'property_account_income_categ', 'product.category',
866                             context=context)
867                 uosqty = _get_line_qty(line)
868                 uos_id = _get_line_uom(line)
869                 pu = 0.0
870                 if uosqty:
871                     pu = round(line.price_unit * line.product_uom_qty / uosqty,
872                             int(config['price_accuracy']))
873                 fpos = line.order_id.fiscal_position or False
874                 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
875                 if not a:
876                     raise osv.except_osv(_('Error !'),
877                                 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
878                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
879                     'name': line.name,
880                     'origin': line.order_id.name,
881                     'account_id': a,
882                     'price_unit': pu,
883                     'quantity': uosqty,
884                     'discount': line.discount,
885                     'uos_id': uos_id,
886                     'product_id': line.product_id.id or False,
887                     'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
888                     'note': line.notes,
889                     'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
890                 })
891                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
892                 self.write(cr, uid, [line.id], {'invoiced': True})
893
894                 sales[line.order_id.id] = True
895                 create_ids.append(inv_id)
896
897         # Trigger workflow events
898         wf_service = netsvc.LocalService("workflow")
899         for sid in sales.keys():
900             wf_service.trg_write(uid, 'sale.order', sid, cr)
901         return create_ids
902
903     def button_cancel(self, cr, uid, ids, context={}):
904         for line in self.browse(cr, uid, ids, context=context):
905             if line.invoiced:
906                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
907         return self.write(cr, uid, ids, {'state': 'cancel'})
908
909     def button_confirm(self, cr, uid, ids, context={}):
910         return self.write(cr, uid, ids, {'state': 'confirmed'})
911
912     def button_done(self, cr, uid, ids, context={}):
913         wf_service = netsvc.LocalService("workflow")
914         res = self.write(cr, uid, ids, {'state': 'done'})
915         for line in self.browse(cr, uid, ids, context):
916             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
917
918         return res
919
920     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
921         product_obj = self.pool.get('product.product')
922         if not product_id:
923             return {'value': {'product_uom': product_uos,
924                 'product_uom_qty': product_uos_qty}, 'domain': {}}
925
926         product = product_obj.browse(cr, uid, product_id)
927         value = {
928             'product_uom': product.uom_id.id,
929         }
930         # FIXME must depend on uos/uom of the product and not only of the coeff.
931         try:
932             value.update({
933                 'product_uom_qty': product_uos_qty / product.uos_coeff,
934                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
935             })
936         except ZeroDivisionError:
937             pass
938         return {'value': value}
939
940     def copy_data(self, cr, uid, id, default=None, context={}):
941         if not default:
942             default = {}
943         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
944         return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
945
946     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
947             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
948             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
949         if not  partner_id:
950             raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sale form !\nPlease set one customer before choosing a product.'))
951         warning = {}
952         product_uom_obj = self.pool.get('product.uom')
953         partner_obj = self.pool.get('res.partner')
954         product_obj = self.pool.get('product.product')
955         if partner_id:
956             lang = partner_obj.browse(cr, uid, partner_id).lang
957         context = {'lang': lang, 'partner_id': partner_id}
958         if not product:
959             return {'value': {'th_weight': 0, 'product_packaging': False,
960                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
961                    'product_uos': []}}
962
963         if not date_order:
964             date_order = time.strftime('%Y-%m-%d')
965
966         result = {}
967         product_obj = product_obj.browse(cr, uid, product, context=context)
968         if not packaging and product_obj.packaging:
969             packaging = product_obj.packaging[0].id
970             result['product_packaging'] = packaging
971
972         if packaging:
973             default_uom = product_obj.uom_id and product_obj.uom_id.id
974             pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
975             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
976 #            qty = qty - qty % q + q
977             if qty and (q and not (qty % q) == 0):
978                 ean = pack.ean
979                 qty_pack = pack.qty
980                 type_ul = pack.ul
981                 warn_msg = _("You selected a quantity of %d Units.\nBut it's not compatible with the selected packaging.\nHere is a proposition of quantities according to the packaging: ") % (qty)
982                 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
983                 warning = {
984                     'title': _('Packing Information !'),
985                     'message': warn_msg
986                     }
987             result['product_uom_qty'] = qty
988
989         if uom:
990             uom2 = product_uom_obj.browse(cr, uid, uom)
991             if product_obj.uom_id.category_id.id != uom2.category_id.id:
992                 uom = False
993
994         if uos:
995             if product_obj.uos_id:
996                 uos2 = product_uom_obj.browse(cr, uid, uos)
997                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
998                     uos = False
999             else:
1000                 uos = False
1001         result.update({'type': product_obj.procure_method})
1002         if product_obj.description_sale:
1003             result['notes'] = product_obj.description_sale
1004         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1005         if update_tax: #The quantity only have changed
1006             result['delay'] = (product_obj.sale_delay or 0.0)
1007             partner = partner_obj.browse(cr, uid, partner_id)
1008             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1009         if not flag:
1010             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1011         domain = {}
1012         if (not uom) and (not uos):
1013             result['product_uom'] = product_obj.uom_id.id
1014             if product_obj.uos_id:
1015                 result['product_uos'] = product_obj.uos_id.id
1016                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1017                 uos_category_id = product_obj.uos_id.category_id.id
1018             else:
1019                 result['product_uos'] = False
1020                 result['product_uos_qty'] = qty
1021                 uos_category_id = False
1022             result['th_weight'] = qty * product_obj.weight
1023             domain = {'product_uom':
1024                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1025                         'product_uos':
1026                         [('category_id', '=', uos_category_id)]}
1027
1028         elif uos and not uom: # only happens if uom is False
1029             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1030             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1031             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1032         elif uom: # whether uos is set or not
1033             default_uom = product_obj.uom_id and product_obj.uom_id.id
1034             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1035             if product_obj.uos_id:
1036                 result['product_uos'] = product_obj.uos_id.id
1037                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1038             else:
1039                 result['product_uos'] = False
1040                 result['product_uos_qty'] = qty
1041             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1042
1043         # get unit price
1044
1045         if not pricelist:
1046             warning = {
1047                 'title': 'No Pricelist !',
1048                 'message':
1049                     'You have to select a pricelist in the sale form !\n'
1050                     'Please set one before choosing a product.'
1051                 }
1052         else:
1053             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1054                     product, qty or 1.0, partner_id, {
1055                         'uom': uom,
1056                         'date': date_order,
1057                         })[pricelist]
1058             if price is False:
1059                 warning = {
1060                     'title': 'No valid pricelist line found !',
1061                     'message':
1062                         "Couldn't find a pricelist line matching this product and quantity.\n"
1063                         "You have to change either the product, the quantity or the pricelist."
1064                     }
1065             else:
1066                 result.update({'price_unit': price})
1067         return {'value': result, 'domain': domain, 'warning': warning}
1068
1069     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1070             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1071             lang=False, update_tax=True, date_order=False):
1072         res = self.product_id_change(cursor, user, ids, pricelist, product,
1073                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1074                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1075                 date_order=date_order)
1076         if 'product_uom' in res['value']:
1077             del res['value']['product_uom']
1078         if not uom:
1079             res['value']['price_unit'] = 0.0
1080         return res
1081
1082     def unlink(self, cr, uid, ids, context={}):
1083         """Allows to delete sale order lines in draft,cancel states"""
1084         for rec in self.browse(cr, uid, ids, context=context):
1085             if rec.state not in ['draft', 'cancel']:
1086                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1087         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1088
1089 sale_order_line()
1090
1091
1092 class sale_config_picking_policy(osv.osv_memory):
1093     _name = 'sale.config.picking_policy'
1094     _columns = {
1095         'name': fields.char('Name', size=64),
1096         'picking_policy': fields.selection([
1097             ('direct', 'Direct Delivery'),
1098             ('one', 'All at Once')
1099         ], 'Packing Default Policy', required=True),
1100         'order_policy': fields.selection([
1101             ('manual', 'Invoice Based on Sales Orders'),
1102             ('picking', 'Invoice Based on Deliveries'),
1103         ], 'Shipping Default Policy', required=True),
1104         'step': fields.selection([
1105             ('one', 'Delivery Order Only'),
1106             ('two', 'Packing List & Delivery Order')
1107         ], 'Steps To Deliver a Sale Order', required=True,
1108            help="By default, Open ERP is able to manage complex routing and paths "\
1109            "of products in your warehouse and partner locations. This will configure "\
1110            "the most common and simple methods to deliver products to the customer "\
1111            "in one or two operations by the worker.")
1112     }
1113     _defaults = {
1114         'picking_policy': lambda *a: 'direct',
1115         'order_policy': lambda *a: 'picking',
1116         'step': lambda *a: 'one'
1117     }
1118
1119     def set_default(self, cr, uid, ids, context=None):
1120         for o in self.browse(cr, uid, ids, context=context):
1121             ir_values_obj = self.pool.get('ir.values')
1122             ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1123             ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1124
1125             if o.step == 'one':
1126                 md = self.pool.get('ir.model.data')
1127                 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1128                 group_id = md.browse(cr, uid, group_id, context).res_id
1129                 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1130                 menu_id = md.browse(cr, uid, menu_id, context).res_id
1131                 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1132
1133                 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1134                 location_id = md.browse(cr, uid, location_id, context).res_id
1135                 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1136
1137         return {
1138                 'view_type': 'form',
1139                 "view_mode": 'form',
1140                 'res_model': 'ir.actions.configuration.wizard',
1141                 'type': 'ir.actions.act_window',
1142                 'target': 'new',
1143          }
1144
1145     def action_cancel(self, cr, uid, ids, context=None):
1146         return {
1147                 'view_type': 'form',
1148                 "view_mode": 'form',
1149                 'res_model': 'ir.actions.configuration.wizard',
1150                 'type': 'ir.actions.act_window',
1151                 'target': 'new',
1152          }
1153
1154 sale_config_picking_policy()
1155