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