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