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