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