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