[IMP] sale:-made company_id field on sale.shop required=False
[odoo/odoo.git] / addons / sale / sale.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
24 from osv import fields, osv
25 from tools.translate import _
26 import decimal_precision as dp
27 import netsvc
28 import time
29
30
31 class sale_shop(osv.osv):
32     _name = "sale.shop"
33     _description = "Sale Shop"
34     _columns = {
35         'name': fields.char('Shop Name', size=64, required=True),
36         'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
37         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
38         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39         'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
40         'company_id': fields.many2one('res.company', 'Company', required=False),
41     }
42     _defaults = {
43         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
44     }
45
46 sale_shop()
47
48 def _incoterm_get(self, cr, uid, context=None):
49     if context is None:
50         context = {}
51     cr.execute('select code, code||\', \'||name from stock_incoterms where active')
52     return cr.fetchall()
53
54 class sale_order(osv.osv):
55     _name = "sale.order"
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         cur_obj = self.pool.get('res.currency')
82         if context is None:
83             context = {}
84         res = {}
85         for order in self.browse(cr, uid, ids, context=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     # This is False
102     def _picked_rate(self, cr, uid, ids, name, arg, context=None):
103         if context is None:
104             context = {}
105         if not ids:
106             return {}
107         res = {}
108         for id in ids:
109             res[id] = [0.0, 0.0]
110         cr.execute('''SELECT
111                 p.sale_id,sum(m.product_qty), mp.state as mp_state
112             FROM
113                 stock_move m
114             LEFT JOIN
115                 stock_picking p on (p.id=m.picking_id)
116             LEFT JOIN
117                 procurement_order mp on (mp.move_id=m.id)
118             WHERE
119                 p.sale_id IN %s GROUP BY mp.state, p.sale_id''',(tuple(ids),))
120         for oid, nbr, mp_state in cr.fetchall():
121             if mp_state == 'cancel':
122                 continue
123             if mp_state == 'done':
124                 res[oid][0] += nbr or 0.0
125                 res[oid][1] += nbr or 0.0
126             else:
127                 res[oid][1] += nbr or 0.0
128         for r in res:
129             if not res[r][1]:
130                 res[r] = 0.0
131             else:
132                 res[r] = 100.0 * res[r][0] / res[r][1]
133         for order in self.browse(cr, uid, ids, context=context):
134             if order.shipped:
135                 res[order.id] = 100.0
136         return res
137
138     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
139         if context is None:
140             context = {}
141         res = {}
142         for sale in self.browse(cursor, user, ids, context=context):
143             if sale.invoiced:
144                 res[sale.id] = 100.0
145                 continue
146             tot = 0.0
147             for invoice in sale.invoice_ids:
148                 if invoice.state not in ('draft', 'cancel'):
149                     tot += invoice.amount_untaxed
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,
209             readonly=True, states={'draft': [('readonly', False)]}, select=True),
210         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
211         'origin': fields.char('Source document', size=64, help="Reference of the document that generated this sale order request."),
212         'client_order_ref': fields.char('Customer Reference', size=64),
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. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the 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, help="Date on which sale order is created"),
225         'date_confirm': fields.date('Confirmation Date', readonly=True, help="Date on which sale order is confirmed"),
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)]}, help="Invoice address for current sale order"),
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, help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction"),
233         'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
234             'Picking Policy', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
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)]}, help="The analytic account related to a sale order"),
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, help="It indicates that a picking has been done. It will be set to True if the ordered quantities are available and the picking is done. If the ordered quantities are not available Procurement generates a Purchase/Manufacturing order. Unless its Picking and Purchase/Manufacturing order are not in the done state it wont be set to 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', help="It indicates that an invoice has been paid"),
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', help="The amount without tax"),
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', help="The tax amount"),
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', help="The total amount"),
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 want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True),
279         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
280         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
281         'company_id': fields.many2one('res.company','Company')
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': 'direct',
286         'date_order': time.strftime('%Y-%m-%d'),
287         'order_policy': 'manual',
288         'state': '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': '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     }
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         return {'value': v}
320
321     def action_cancel_draft(self, cr, uid, ids, *args):
322         if not len(ids):
323             return False
324         cr.execute('select id from sale_order_line where order_id IN %s and state=%s',(tuple(ids),'cancel'))
325         line_ids = map(lambda x: x[0], cr.fetchall())
326         self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
327         self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
328         wf_service = netsvc.LocalService("workflow")
329         for inv_id in ids:
330             # Deleting the existing instance of workflow for SO
331             wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
332             wf_service.trg_create(uid, 'sale.order', inv_id, cr)
333         for (id,name) in self.name_get(cr, uid, ids):
334             message = _('Sale order ') + " '" + name + "' "+ _("is in draft state")
335             self.log(cr, uid, id, message)
336         return True
337
338     def onchange_partner_id(self, cr, uid, ids, part):
339         if not part:
340             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
341
342         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
343         part = self.pool.get('res.partner').browse(cr, uid, part)
344         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
345         payment_term = part.property_payment_term and part.property_payment_term.id or False
346         fiscal_position = part.property_account_position and part.property_account_position.id or False
347         dedicated_salesman = part.user_id and part.user_id.id or uid
348
349         val = {
350             'partner_invoice_id': addr['invoice'],
351             'partner_order_id': addr['contact'],
352             'partner_shipping_id': addr['delivery'],
353             'payment_term': payment_term,
354             'fiscal_position': fiscal_position,
355             'user_id': dedicated_salesman,
356         }
357
358         if pricelist:
359             val['pricelist_id'] = pricelist
360
361         return {'value': val}
362
363     def shipping_policy_change(self, cr, uid, ids, policy, context=None):
364         if context is None:
365             context = {}
366         if not policy:
367             return {}
368         inv_qty = 'order'
369         if policy == 'prepaid':
370             inv_qty = 'order'
371         elif policy == 'picking':
372             inv_qty = 'procurement'
373         return {'value': {'invoice_quantity': inv_qty}}
374
375     def write(self, cr, uid, ids, vals, context=None):
376         if context is None:
377             context = {}
378         if 'order_policy' in vals:
379             if vals['order_policy'] == 'prepaid':
380                 vals.update({'invoice_quantity': 'order'})
381             elif vals['order_policy'] == 'picking':
382                 vals.update({'invoice_quantity': 'procurement'})
383         return super(sale_order, self).write(cr, uid, ids, vals, context=context)
384
385     def create(self, cr, uid, vals, context=None):
386         if context is None:
387             context = {}
388         if 'order_policy' in vals:
389             if vals['order_policy'] == 'prepaid':
390                 vals.update({'invoice_quantity': 'order'})
391             if vals['order_policy'] == 'picking':
392                 vals.update({'invoice_quantity': 'procurement'})
393         return super(sale_order, self).create(cr, uid, vals, context=context)
394
395     def button_dummy(self, cr, uid, ids, context=None):
396         if context is None:
397             context = {}
398         return True
399
400     #FIXME: the method should return the list of invoices created (invoice_ids)
401     # and not the id of the last invoice created (res). The problem is that we
402     # cannot change it directly since the method is called by the sale order
403     # workflow and I suppose it expects a single id...
404     def _inv_get(self, cr, uid, order, context=None):
405         if context is None:
406             context = {}
407         return {}
408
409     def _make_invoice(self, cr, uid, order, lines, context=None):
410         journal_obj = self.pool.get('account.journal')
411         inv_obj = self.pool.get('account.invoice')
412
413         if context is None:
414             context = {}
415
416         journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', order.company_id.id)], limit=1)
417         if not journal_ids:
418             raise osv.except_osv(_('Error !'),
419                 _('There is no sale journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
420         a = order.partner_id.property_account_receivable.id
421         if order.payment_term:
422             pay_term = order.payment_term.id
423         else:
424             pay_term = False
425         for preinv in order.invoice_ids:
426             if preinv.state not in ('cancel',):
427                 for preline in preinv.invoice_line:
428                     inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
429                     lines.append(inv_line_id)
430         inv = {
431             'name': order.client_order_ref or order.name,
432             'origin': order.name,
433             'type': 'out_invoice',
434             'reference': "P%dSO%d" % (order.partner_id.id, order.id),
435             'account_id': a,
436             'partner_id': order.partner_id.id,
437             'journal_id': journal_ids[0],
438             'address_invoice_id': order.partner_invoice_id.id,
439             'address_contact_id': order.partner_order_id.id,
440             'invoice_line': [(6, 0, lines)],
441             'currency_id': order.pricelist_id.currency_id.id,
442             'comment': order.note,
443             'payment_term': pay_term,
444             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
445             'date_invoice' : context.get('date_invoice',False),
446             'company_id' : order.company_id.id,
447             'user_id':order.user_id and order.user_id.id or False
448         }
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, context=None):
458         res = False
459         invoices = {}
460         invoice_ids = []
461         picking_obj = self.pool.get('stock.picking')
462         invoice = self.pool.get('account.invoice')
463         if context is None:
464             context = {}
465         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
466         # last day of the last month as invoice date
467         if date_inv:
468             context['date_inv'] = date_inv
469         for o in self.browse(cr, uid, ids):
470             lines = []
471             for line in o.order_line:
472                 if line.invoiced:
473                     #raise osv.except_osv(_('Error !'), _('The Sale Order already has some lines invoiced. You should continue the billing process by line.'))
474                     continue
475                 elif (line.state in states):
476                     lines.append(line.id)
477             created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
478             if created_lines:
479                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
480         if not invoices:
481             for o in self.browse(cr, uid, ids):
482                 for i in o.invoice_ids:
483                     if i.state == 'draft':
484                         return i.id
485         for val in invoices.values():
486             if grouped:
487                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
488                 invoice_ref = ''
489                 for o, l in val:
490                     invoice_ref += o.name + '|'
491                     self.write(cr, uid, [o.id], {'state': 'progress'})
492                     if o.order_policy == 'picking':
493                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
494                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
495                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
496             else:
497                 for order, il in val:
498                     res = self._make_invoice(cr, uid, order, il, context=context)
499                     invoice_ids.append(res)
500                     self.write(cr, uid, [order.id], {'state': 'progress'})
501                     if order.order_policy == 'picking':
502                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
503                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
504         return res
505
506     def action_invoice_cancel(self, cr, uid, ids, context=None):
507         if context is None:
508             context = {}
509         for sale in self.browse(cr, uid, ids, context=context):
510             for line in sale.order_line:
511                 invoiced = False
512                 for iline in line.invoice_lines:
513                     if iline.invoice_id and iline.invoice_id.state == 'cancel':
514                         continue
515                     else:
516                         invoiced = True
517                 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
518         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
519         return True
520
521     def action_invoice_end(self, cr, uid, ids, context=None):
522         if context is None:
523             context = {}
524
525         for order in self.browse(cr, uid, ids, context=context):
526             for line in order.order_line:
527                 if line.state == 'exception':
528                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'state': 'confirmed'}, context=context)
529
530             if order.state == 'invoice_except':
531                 self.write(cr, uid, [order.id], {'state' : 'progress'}, context=context)
532
533         return True
534
535     def action_cancel(self, cr, uid, ids, context=None):
536         if context is None:
537             context = {}
538         sale_order_line_obj = self.pool.get('sale.order.line')
539         for sale in self.browse(cr, uid, ids, context=context):
540             for pick in sale.picking_ids:
541                 if pick.state not in ('draft', 'cancel'):
542                     raise osv.except_osv(
543                         _('Could not cancel sale order !'),
544                         _('You must first cancel all picking attached to this sale order.'))
545             for r in self.read(cr, uid, ids, ['picking_ids']):
546                 for pick in r['picking_ids']:
547                     wf_service = netsvc.LocalService("workflow")
548                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
549             for inv in sale.invoice_ids:
550                 if inv.state not in ('draft', 'cancel'):
551                     raise osv.except_osv(
552                         _('Could not cancel this sale order !'),
553                         _('You must first cancel all invoices attached to this sale order.'))
554             for r in self.read(cr, uid, ids, ['invoice_ids']):
555                 for inv in r['invoice_ids']:
556                     wf_service = netsvc.LocalService("workflow")
557                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
558             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
559                     {'state': 'cancel'})
560             message = _('Sale order') + " '" + sale.name + _(" is cancelled")
561             self.log(cr, uid, sale.id, message)
562         self.write(cr, uid, ids, {'state': 'cancel'})
563         return True
564
565     def action_wait(self, cr, uid, ids, *args):
566         for o in self.browse(cr, uid, ids):
567             if (o.order_policy == 'manual'):
568                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime('%Y-%m-%d')})
569             else:
570                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime('%Y-%m-%d')})
571             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
572             message = _('Quotation') + " '" + o.name + "' "+ _("is converted to Sale order")
573             self.log(cr, uid, o.id, message)
574         return True
575
576     def procurement_lines_get(self, cr, uid, ids, *args):
577         res = []
578         for order in self.browse(cr, uid, ids, context={}):
579             for line in order.order_line:
580                 if line.procurement_id:
581                     res.append(line.procurement_id.id)
582         return res
583
584     # if mode == 'finished':
585     #   returns True if all lines are done, False otherwise
586     # if mode == 'canceled':
587     #   returns True if there is at least one canceled line, False otherwise
588     def test_state(self, cr, uid, ids, mode, *args):
589         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
590         finished = True
591         canceled = False
592         notcanceled = False
593         write_done_ids = []
594         write_cancel_ids = []
595         for order in self.browse(cr, uid, ids, context={}):
596             for line in order.order_line:
597                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
598                     if line.state != 'done':
599                         write_done_ids.append(line.id)
600                 else:
601                     finished = False
602                 if line.procurement_id:
603                     if (line.procurement_id.state == 'cancel'):
604                         canceled = True
605                         if line.state != 'exception':
606                             write_cancel_ids.append(line.id)
607                     else:
608                         notcanceled = True
609         if write_done_ids:
610             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
611         if write_cancel_ids:
612             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
613
614         if mode == 'finished':
615             return finished
616         elif mode == 'canceled':
617             return canceled
618             if notcanceled:
619                 return False
620             return canceled
621
622     def action_ship_create(self, cr, uid, ids, *args):
623         picking_id = False
624         company = self.pool.get('res.users').browse(cr, uid, uid).company_id
625         for order in self.browse(cr, uid, ids, context={}):
626             proc_ids = []
627             output_id = order.shop_id.warehouse_id.lot_output_id.id
628             picking_id = False
629             for line in order.order_line:
630                 proc_id = False
631                 date_planned = datetime.now() + relativedelta(days=line.delay or 0.0)
632                 date_planned = (date_planned - timedelta(days=company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
633
634                 if line.state == 'done':
635                     continue
636                 move_id = False
637                 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
638                     location_id = order.shop_id.warehouse_id.lot_stock_id.id
639                     if not picking_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                     move_id = self.pool.get('stock.move').create(cr, uid, {
654                         'name': line.name[:64],
655                         'picking_id': picking_id,
656                         'product_id': line.product_id.id,
657                         'date_planned': date_planned,
658                         'date_expected': 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                 if line.product_id:
676                     proc_id = self.pool.get('procurement.order').create(cr, uid, {
677                         'name': line.name,
678                         'origin': order.name,
679                         'date_planned': date_planned,
680                         'product_id': line.product_id.id,
681                         'product_qty': line.product_uom_qty,
682                         'product_uom': line.product_uom.id,
683                         'product_uos_qty': (line.product_uos and line.product_uos_qty)\
684                                 or line.product_uom_qty,
685                         'product_uos': (line.product_uos and line.product_uos.id)\
686                                 or line.product_uom.id,
687                         'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
688                         'procure_method': line.type,
689                         'move_id': move_id,
690                         'property_ids': [(6, 0, [x.id for x in line.property_ids])],
691                         'company_id': order.company_id.id,
692                     })
693                     proc_ids.append(proc_id)
694                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
695
696             val = {}
697             for proc_id in proc_ids:
698                 wf_service = netsvc.LocalService("workflow")
699                 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
700
701             if picking_id:
702                 wf_service = netsvc.LocalService("workflow")
703                 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
704                 
705             if order.state == 'shipping_except':
706                 val['state'] = 'progress'
707
708                 if (order.order_policy == 'manual'):
709                     for line in order.order_line:
710                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
711                             val['state'] = 'manual'
712                             break
713             self.write(cr, uid, [order.id], val)
714         return True
715
716     def action_ship_end(self, cr, uid, ids, context=None):
717         if context is None:
718             context = {}
719         for order in self.browse(cr, uid, ids, context=context):
720             val = {'shipped': True}
721             if order.state == 'shipping_except':
722                 val['state'] = 'progress'
723                 if (order.order_policy == 'manual'):
724                     for line in order.order_line:
725                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
726                             val['state'] = 'manual'
727                             break
728             for line in order.order_line:
729                 towrite = []
730                 if line.state == 'exception':
731                     towrite.append(line.id)
732                 if towrite:
733                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
734             self.write(cr, uid, [order.id], val)
735         return True
736
737     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
738         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
739         for inv in invs:
740             part = inv['partner_id'] and inv['partner_id'][0]
741             pr = inv['amount_untaxed'] or 0.0
742             partnertype = 'customer'
743             eventtype = 'sale'
744             event = {
745                 'name': 'Order: '+name,
746                 'som': False,
747                 'description': 'Order '+str(inv['id']),
748                 'document': '',
749                 'partner_id': part,
750                 'date': time.strftime('%Y-%m-%d'),
751                 'canal_id': False,
752                 'user_id': uid,
753                 'partner_type': partnertype,
754                 'probability': 1.0,
755                 'planned_revenue': pr,
756                 'planned_cost': 0.0,
757                 'type': eventtype
758             }
759             self.pool.get('res.partner.event').create(cr, uid, event)
760
761     def has_stockable_products(self, cr, uid, ids, *args):
762         for order in self.browse(cr, uid, ids):
763             for order_line in order.order_line:
764                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
765                     return True
766         return False
767 sale_order()
768
769 # TODO add a field price_unit_uos
770 # - update it on change product and unit price
771 # - use it in report if there is a uos
772 class sale_order_line(osv.osv):
773
774     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
775         tax_obj = self.pool.get('account.tax')
776         cur_obj = self.pool.get('res.currency')
777         res = {}
778         context = context or {}
779         for line in self.browse(cr, uid, ids, context=context):
780             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
781             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)
782             cur = line.order_id.pricelist_id.currency_id
783             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
784         return res
785
786     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
787         if context is None:
788             context = {}
789         res = {}
790         for line in self.browse(cr, uid, ids, context=context):
791             try:
792                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
793             except:
794                 res[line.id] = 1
795         return res
796
797     _name = 'sale.order.line'
798     _description = 'Sale Order Line'
799     _columns = {
800         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
801         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
802         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sale order lines."),
803         '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)]}),
804         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
805         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
806         'invoiced': fields.boolean('Invoiced', readonly=True),
807         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
808         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft':[('readonly',False)]}),
809         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
810         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
811         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
812         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
813         'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
814         'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
815         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft':[('readonly',False)]}),
816         'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
817         'product_uos': fields.many2one('product.uom', 'Product UoS'),
818         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
819         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
820         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
821         'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
822         'notes': fields.text('Notes'),
823         'th_weight': fields.float('Weight', readonly=True, states={'draft':[('readonly',False)]}),
824         'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
825                 help='* The \'Draft\' state is set when the related sale order in draft state. \
826                     \n* The \'Confirmed\' state is set when the related sale order is confirmed. \
827                     \n* The \'Exception\' state is set when the related sale order is set as exception. \
828                     \n* The \'Done\' state is set when the sale order line has been picked. \
829                     \n* The \'Cancelled\' state is set when a user cancel the sale order related.'),
830         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
831         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', string='Salesman'),
832         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True, states={'draft':[('readonly',False)]}),
833     }
834     _order = 'sequence, id'
835     _defaults = {
836         'discount': 0.0,
837         'delay': 0.0,
838         'product_uom_qty': 1,
839         'product_uos_qty': 1,
840         'sequence':  10,
841         'invoiced': 0,
842         'state': 'draft',
843         'type': 'make_to_stock',
844         'product_packaging': False
845     }
846
847     def invoice_line_create(self, cr, uid, ids, context=None):
848         if context is None:
849             context = {}
850
851         def _get_line_qty(line):
852             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
853                 if line.product_uos:
854                     return line.product_uos_qty or 0.0
855                 return line.product_uom_qty
856             else:
857                 return self.pool.get('procurement.order').quantity_get(cr, uid,
858                         line.procurement_id.id, context=context)
859
860         def _get_line_uom(line):
861             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
862                 if line.product_uos:
863                     return line.product_uos.id
864                 return line.product_uom.id
865             else:
866                 return self.pool.get('procurement.order').uom_get(cr, uid,
867                         line.procurement_id.id, context=context)
868
869         create_ids = []
870         sales = {}
871         for line in self.browse(cr, uid, ids, context=context):
872             if not line.invoiced:
873                 if line.product_id:
874                     a = line.product_id.product_tmpl_id.property_account_income.id
875                     if not a:
876                         a = line.product_id.categ_id.property_account_income_categ.id
877                     if not a:
878                         raise osv.except_osv(_('Error !'),
879                                 _('There is no income account defined ' \
880                                         'for this product: "%s" (id:%d)') % \
881                                         (line.product_id.name, line.product_id.id,))
882                 else:
883                     a = self.pool.get('ir.property').get(cr, uid,
884                             'property_account_income_categ', 'product.category',
885                             context=context)
886                 uosqty = _get_line_qty(line)
887                 uos_id = _get_line_uom(line)
888                 pu = 0.0
889                 if uosqty:
890                     pu = round(line.price_unit * line.product_uom_qty / uosqty,
891                             self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
892                 fpos = line.order_id.fiscal_position or False
893                 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
894                 if not a:
895                     raise osv.except_osv(_('Error !'),
896                                 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
897                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
898                     'name': line.name,
899                     'origin': line.order_id.name,
900                     'account_id': a,
901                     'price_unit': pu,
902                     'quantity': uosqty,
903                     'discount': line.discount,
904                     'uos_id': uos_id,
905                     'product_id': line.product_id.id or False,
906                     'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
907                     'note': line.notes,
908                     'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
909                 })
910                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
911                 self.write(cr, uid, [line.id], {'invoiced': True})
912                 sales[line.order_id.id] = True
913                 create_ids.append(inv_id)
914         # Trigger workflow events
915         wf_service = netsvc.LocalService("workflow")
916         for sid in sales.keys():
917             wf_service.trg_write(uid, 'sale.order', sid, cr)
918         return create_ids
919
920     def button_cancel(self, cr, uid, ids, context=None):
921         if context is None:
922             context = {}
923         for line in self.browse(cr, uid, ids, context=context):
924             if line.invoiced:
925                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
926             for move_line in line.move_ids:
927                 if move_line.state != 'cancel':
928                     raise osv.except_osv(
929                             _('Could not cancel sale order line!'),
930                             _('You must first cancel stock moves attached to this sale order line.'))
931         message = _('Sale order line') + " '" + line.name + "' "+_("is cancelled")
932         self.log(cr, uid, id, message)
933         return self.write(cr, uid, ids, {'state': 'cancel'})
934
935     def button_confirm(self, cr, uid, ids, context=None):
936         if context is None:
937             context = {}
938         return self.write(cr, uid, ids, {'state': 'confirmed'})
939
940     def button_done(self, cr, uid, ids, context=None):
941         if context is None:
942             context = {}
943         wf_service = netsvc.LocalService("workflow")
944         res = self.write(cr, uid, ids, {'state': 'done'})
945         for line in self.browse(cr, uid, ids, context=context):
946             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
947         return res
948
949     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
950         product_obj = self.pool.get('product.product')
951         if not product_id:
952             return {'value': {'product_uom': product_uos,
953                 'product_uom_qty': product_uos_qty}, 'domain': {}}
954
955         product = product_obj.browse(cr, uid, product_id)
956         value = {
957             'product_uom': product.uom_id.id,
958         }
959         # FIXME must depend on uos/uom of the product and not only of the coeff.
960         try:
961             value.update({
962                 'product_uom_qty': product_uos_qty / product.uos_coeff,
963                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
964             })
965         except ZeroDivisionError:
966             pass
967         return {'value': value}
968
969     def copy_data(self, cr, uid, id, default=None, context=None):
970         if context is None:
971             context = {}
972         if not default:
973             default = {}
974         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
975         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
976
977     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
978             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
979             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
980         if not  partner_id:
981             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.'))
982         warning = {}
983         product_uom_obj = self.pool.get('product.uom')
984         partner_obj = self.pool.get('res.partner')
985         product_obj = self.pool.get('product.product')
986         if partner_id:
987             lang = partner_obj.browse(cr, uid, partner_id).lang
988         context = {'lang': lang, 'partner_id': partner_id}
989
990         if not product:
991             return {'value': {'th_weight': 0, 'product_packaging': False,
992                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
993                    'product_uos': []}}
994
995         if not date_order:
996             date_order = time.strftime('%Y-%m-%d')
997
998         result = {}
999         product_obj = product_obj.browse(cr, uid, product, context=context)
1000         if not packaging and product_obj.packaging:
1001             packaging = product_obj.packaging[0].id
1002             result['product_packaging'] = packaging
1003
1004         if packaging:
1005             default_uom = product_obj.uom_id and product_obj.uom_id.id
1006             pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context=context)
1007             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1008 #            qty = qty - qty % q + q
1009             if qty and (q and not (qty % q) == 0):
1010                 ean = pack.ean
1011                 qty_pack = pack.qty
1012                 type_ul = pack.ul
1013                 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)
1014                 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
1015                 warning = {
1016                     'title': _('Picking Information !'),
1017                     'message': warn_msg
1018                     }
1019             result['product_uom_qty'] = qty
1020
1021         uom2 = False
1022         if uom:
1023             uom2 = product_uom_obj.browse(cr, uid, uom)
1024             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1025                 uom = False
1026
1027         if uos:
1028             if product_obj.uos_id:
1029                 uos2 = product_uom_obj.browse(cr, uid, uos)
1030                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1031                     uos = False
1032             else:
1033                 uos = False
1034         if product_obj.description_sale:
1035             result['notes'] = product_obj.description_sale
1036         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1037         if update_tax: #The quantity only have changed
1038             result['delay'] = (product_obj.sale_delay or 0.0)
1039             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1040             result.update({'type': product_obj.procure_method})
1041
1042         if not flag:
1043             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1044         domain = {}
1045         if (not uom) and (not uos):
1046             result['product_uom'] = product_obj.uom_id.id
1047             if product_obj.uos_id:
1048                 result['product_uos'] = product_obj.uos_id.id
1049                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1050                 uos_category_id = product_obj.uos_id.category_id.id
1051             else:
1052                 result['product_uos'] = False
1053                 result['product_uos_qty'] = qty
1054                 uos_category_id = False
1055             result['th_weight'] = qty * product_obj.weight
1056             domain = {'product_uom':
1057                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1058                         'product_uos':
1059                         [('category_id', '=', uos_category_id)]}
1060
1061         elif uos and not uom: # only happens if uom is False
1062             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1063             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1064             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1065         elif uom: # whether uos is set or not
1066             default_uom = product_obj.uom_id and product_obj.uom_id.id
1067             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1068             if product_obj.uos_id:
1069                 result['product_uos'] = product_obj.uos_id.id
1070                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1071             else:
1072                 result['product_uos'] = False
1073                 result['product_uos_qty'] = qty
1074             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1075
1076         if not uom2:
1077             uom2 = product_obj.uom_id
1078         if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1079           and (product_obj.procure_method=='make_to_stock'):
1080             warning = {
1081                 'title': _('Not enough stock !'),
1082                 'message': _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') %
1083                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1084                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1085                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1086             }
1087
1088
1089         # get unit price
1090
1091         if not pricelist:
1092             warning = {
1093                 'title': 'No Pricelist !',
1094                 'message':
1095                     'You have to select a pricelist or a customer in the sale form !\n'
1096                     'Please set one before choosing a product.'
1097                 }
1098         else:
1099             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1100                     product, qty or 1.0, partner_id, {
1101                         'uom': uom,
1102                         'date': date_order,
1103                         })[pricelist]
1104             if price is False:
1105                 warning = {
1106                     'title': 'No valid pricelist line found !',
1107                     'message':
1108                         "Couldn't find a pricelist line matching this product and quantity.\n"
1109                         "You have to change either the product, the quantity or the pricelist."
1110                     }
1111             else:
1112                 result.update({'price_unit': price})
1113         return {'value': result, 'domain': domain, 'warning': warning}
1114
1115     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1116             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1117             lang=False, update_tax=True, date_order=False):
1118         res = self.product_id_change(cursor, user, ids, pricelist, product,
1119                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1120                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1121                 date_order=date_order)
1122         if 'product_uom' in res['value']:
1123             del res['value']['product_uom']
1124         if not uom:
1125             res['value']['price_unit'] = 0.0
1126         return res
1127
1128     def unlink(self, cr, uid, ids, context=None):
1129         if context is None:
1130             context = {}
1131         """Allows to delete sale order lines in draft,cancel states"""
1132         for rec in self.browse(cr, uid, ids, context=context):
1133             if rec.state not in ['draft', 'cancel']:
1134                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1135         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1136
1137 sale_order_line()
1138
1139
1140 class sale_config_picking_policy(osv.osv_memory):
1141     _name = 'sale.config.picking_policy'
1142     _inherit = 'res.config'
1143
1144     _columns = {
1145         'name': fields.char('Name', size=64),
1146         'picking_policy': fields.selection([
1147             ('direct', 'Direct Delivery'),
1148             ('one', 'All at Once')
1149         ], 'Picking Default Policy', required=True, help="The Shipping Policy is used to synchronise invoices and delivery operations."),
1150         'order_policy': fields.selection([
1151             ('manual', 'Invoice Based on Sales Orders'),
1152             ('picking', 'Invoice Based on Deliveries'),
1153         ], 'Shipping Default Policy', required=True,
1154            help="You can generate invoices based on sales orders or based on shippings."),
1155         'step': fields.selection([
1156             ('one', 'Delivery Order Only'),
1157             ('two', 'Picking List & Delivery Order')
1158         ], 'Steps To Deliver a Sale Order', required=True,
1159            help="By default, OpenERP is able to manage complex routing and paths "\
1160            "of products in your warehouse and partner locations. This will configure "\
1161            "the most common and simple methods to deliver products to the customer "\
1162            "in one or two operations by the worker.")
1163     }
1164     _defaults = {
1165         'picking_policy': 'direct',
1166         'order_policy': 'manual',
1167         'step': 'one'
1168     }
1169
1170     def execute(self, cr, uid, ids, context=None):
1171         if context is None:
1172             context = {}
1173         for o in self.browse(cr, uid, ids, context=context):
1174             ir_values_obj = self.pool.get('ir.values')
1175             ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1176             ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1177             if o.step == 'one':
1178                 md = self.pool.get('ir.model.data')
1179                 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1180                 group_id = md.browse(cr, uid, group_id, context=context).res_id
1181                 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1182                 menu_id = md.browse(cr, uid, menu_id, context=context).res_id
1183                 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1184                 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1185                 location_id = md.browse(cr, uid, location_id, context=context).res_id
1186                 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1187
1188 sale_config_picking_policy()
1189
1190 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: