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