c1ea2ea6a8eacad97ec31fcb00d1ce778226ee24
[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
707             if order.state == 'shipping_except':
708                 val['state'] = 'progress'
709
710                 if (order.order_policy == 'manual'):
711                     for line in order.order_line:
712                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
713                             val['state'] = 'manual'
714                             break
715             self.write(cr, uid, [order.id], val)
716         return True
717
718     def action_ship_end(self, cr, uid, ids, context=None):
719         if context is None:
720             context = {}
721         for order in self.browse(cr, uid, ids, context=context):
722             val = {'shipped': True}
723             if order.state == 'shipping_except':
724                 val['state'] = 'progress'
725                 if (order.order_policy == 'manual'):
726                     for line in order.order_line:
727                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
728                             val['state'] = 'manual'
729                             break
730             for line in order.order_line:
731                 towrite = []
732                 if line.state == 'exception':
733                     towrite.append(line.id)
734                 if towrite:
735                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
736             self.write(cr, uid, [order.id], val)
737         return True
738
739     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
740         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
741         for inv in invs:
742             part = inv['partner_id'] and inv['partner_id'][0]
743             pr = inv['amount_untaxed'] or 0.0
744             partnertype = 'customer'
745             eventtype = 'sale'
746             event = {
747                 'name': 'Order: '+name,
748                 'som': False,
749                 'description': 'Order '+str(inv['id']),
750                 'document': '',
751                 'partner_id': part,
752                 'date': time.strftime('%Y-%m-%d'),
753                 'canal_id': False,
754                 'user_id': uid,
755                 'partner_type': partnertype,
756                 'probability': 1.0,
757                 'planned_revenue': pr,
758                 'planned_cost': 0.0,
759                 'type': eventtype
760             }
761             self.pool.get('res.partner.event').create(cr, uid, event)
762
763     def has_stockable_products(self, cr, uid, ids, *args):
764         for order in self.browse(cr, uid, ids):
765             for order_line in order.order_line:
766                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
767                     return True
768         return False
769 sale_order()
770
771 # TODO add a field price_unit_uos
772 # - update it on change product and unit price
773 # - use it in report if there is a uos
774 class sale_order_line(osv.osv):
775     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
776         tax_obj = self.pool.get('account.tax')
777         cur_obj = self.pool.get('res.currency')
778         res = {}
779         context = context or {}
780         for line in self.browse(cr, uid, ids, context=context):
781             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
782             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)
783             cur = line.order_id.pricelist_id.currency_id
784             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
785         return res
786
787     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
788         if context is None:
789             context = {}
790         res = {}
791         for line in self.browse(cr, uid, ids, context=context):
792             try:
793                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
794             except:
795                 res[line.id] = 1
796         return res
797
798     _name = 'sale.order.line'
799     _description = 'Sale Order Line'
800     _columns = {
801         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
802         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
803         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sale order lines."),
804         '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)]}),
805         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
806         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
807         'invoiced': fields.boolean('Invoiced', readonly=True),
808         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
809         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft':[('readonly',False)]}),
810         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
811         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
812         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
813         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
814         'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
815         'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
816         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft':[('readonly',False)]}),
817         'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
818         'product_uos': fields.many2one('product.uom', 'Product UoS'),
819         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
820         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
821         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
822         'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
823         'notes': fields.text('Notes'),
824         'th_weight': fields.float('Weight', readonly=True, states={'draft':[('readonly',False)]}),
825         'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
826                 help='* The \'Draft\' state is set when the related sale order in draft state. \
827                     \n* The \'Confirmed\' state is set when the related sale order is confirmed. \
828                     \n* The \'Exception\' state is set when the related sale order is set as exception. \
829                     \n* The \'Done\' state is set when the sale order line has been picked. \
830                     \n* The \'Cancelled\' state is set when a user cancel the sale order related.'),
831         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
832         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', string='Salesman'),
833         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True, states={'draft':[('readonly',False)]}),
834     }
835     _order = 'sequence, id'
836     _defaults = {
837         'discount': 0.0,
838         'delay': 0.0,
839         'product_uom_qty': 1,
840         'product_uos_qty': 1,
841         'sequence':  10,
842         'invoiced': 0,
843         'state': 'draft',
844         'type': 'make_to_stock',
845         'product_packaging': False
846     }
847
848     def create_sale_order_line_invoice(self, cr, uid, ids, context=None):
849         if context is None:
850             context = {}
851         context.update({'active_ids' : ids,'active_id' : ids})
852         self.pool.get('sale.order.line.make.invoice').make_invoices(cr, uid, ids, context=context)
853         return True
854
855     def invoice_line_create(self, cr, uid, ids, context=None):
856         if context is None:
857             context = {}
858
859         def _get_line_qty(line):
860             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
861                 if line.product_uos:
862                     return line.product_uos_qty or 0.0
863                 return line.product_uom_qty
864             else:
865                 return self.pool.get('procurement.order').quantity_get(cr, uid,
866                         line.procurement_id.id, context=context)
867
868         def _get_line_uom(line):
869             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
870                 if line.product_uos:
871                     return line.product_uos.id
872                 return line.product_uom.id
873             else:
874                 return self.pool.get('procurement.order').uom_get(cr, uid,
875                         line.procurement_id.id, context=context)
876
877         create_ids = []
878         sales = {}
879         for line in self.browse(cr, uid, ids, context=context):
880             if not line.invoiced:
881                 if line.product_id:
882                     a = line.product_id.product_tmpl_id.property_account_income.id
883                     if not a:
884                         a = line.product_id.categ_id.property_account_income_categ.id
885                     if not a:
886                         raise osv.except_osv(_('Error !'),
887                                 _('There is no income account defined ' \
888                                         'for this product: "%s" (id:%d)') % \
889                                         (line.product_id.name, line.product_id.id,))
890                 else:
891                     a = self.pool.get('ir.property').get(cr, uid,
892                             'property_account_income_categ', 'product.category',
893                             context=context)
894                 uosqty = _get_line_qty(line)
895                 uos_id = _get_line_uom(line)
896                 pu = 0.0
897                 if uosqty:
898                     pu = round(line.price_unit * line.product_uom_qty / uosqty,
899                             self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
900                 fpos = line.order_id.fiscal_position or False
901                 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
902                 if not a:
903                     raise osv.except_osv(_('Error !'),
904                                 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
905                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
906                     'name': line.name,
907                     'origin': line.order_id.name,
908                     'account_id': a,
909                     'price_unit': pu,
910                     'quantity': uosqty,
911                     'discount': line.discount,
912                     'uos_id': uos_id,
913                     'product_id': line.product_id.id or False,
914                     'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
915                     'note': line.notes,
916                     'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
917                 })
918                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
919                 self.write(cr, uid, [line.id], {'invoiced': True})
920                 sales[line.order_id.id] = True
921                 create_ids.append(inv_id)
922         # Trigger workflow events
923         wf_service = netsvc.LocalService("workflow")
924         for sid in sales.keys():
925             wf_service.trg_write(uid, 'sale.order', sid, cr)
926         return create_ids
927
928     def button_cancel(self, cr, uid, ids, context=None):
929         if context is None:
930             context = {}
931         for line in self.browse(cr, uid, ids, context=context):
932             if line.invoiced:
933                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
934             for move_line in line.move_ids:
935                 if move_line.state != 'cancel':
936                     raise osv.except_osv(
937                             _('Could not cancel sale order line!'),
938                             _('You must first cancel stock moves attached to this sale order line.'))
939         message = _('Sale order line') + " '" + line.name + "' "+_("is cancelled")
940         self.log(cr, uid, id, message)
941         return self.write(cr, uid, ids, {'state': 'cancel'})
942
943     def button_confirm(self, cr, uid, ids, context=None):
944         if context is None:
945             context = {}
946         return self.write(cr, uid, ids, {'state': 'confirmed'})
947
948     def button_done(self, cr, uid, ids, context=None):
949         if context is None:
950             context = {}
951         wf_service = netsvc.LocalService("workflow")
952         res = self.write(cr, uid, ids, {'state': 'done'})
953         for line in self.browse(cr, uid, ids, context=context):
954             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
955         return res
956
957     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
958         product_obj = self.pool.get('product.product')
959         if not product_id:
960             return {'value': {'product_uom': product_uos,
961                 'product_uom_qty': product_uos_qty}, 'domain': {}}
962
963         product = product_obj.browse(cr, uid, product_id)
964         value = {
965             'product_uom': product.uom_id.id,
966         }
967         # FIXME must depend on uos/uom of the product and not only of the coeff.
968         try:
969             value.update({
970                 'product_uom_qty': product_uos_qty / product.uos_coeff,
971                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
972             })
973         except ZeroDivisionError:
974             pass
975         return {'value': value}
976
977     def copy_data(self, cr, uid, id, default=None, context=None):
978         if context is None:
979             context = {}
980         if not default:
981             default = {}
982         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
983         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
984
985     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
986             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
987             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
988         if not  partner_id:
989             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.'))
990         warning = {}
991         product_uom_obj = self.pool.get('product.uom')
992         partner_obj = self.pool.get('res.partner')
993         product_obj = self.pool.get('product.product')
994         if partner_id:
995             lang = partner_obj.browse(cr, uid, partner_id).lang
996         context = {'lang': lang, 'partner_id': partner_id}
997
998         if not product:
999             return {'value': {'th_weight': 0, 'product_packaging': False,
1000                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1001                    'product_uos': []}}
1002
1003         if not date_order:
1004             date_order = time.strftime('%Y-%m-%d')
1005
1006         result = {}
1007         product_obj = product_obj.browse(cr, uid, product, context=context)
1008         if not packaging and product_obj.packaging:
1009             packaging = product_obj.packaging[0].id
1010             result['product_packaging'] = packaging
1011
1012         if packaging:
1013             default_uom = product_obj.uom_id and product_obj.uom_id.id
1014             pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context=context)
1015             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1016 #            qty = qty - qty % q + q
1017             if qty and (q and not (qty % q) == 0):
1018                 ean = pack.ean
1019                 qty_pack = pack.qty
1020                 type_ul = pack.ul
1021                 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)
1022                 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
1023                 warning = {
1024                     'title': _('Picking Information !'),
1025                     'message': warn_msg
1026                     }
1027             result['product_uom_qty'] = qty
1028
1029         uom2 = False
1030         if uom:
1031             uom2 = product_uom_obj.browse(cr, uid, uom)
1032             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1033                 uom = False
1034
1035         if uos:
1036             if product_obj.uos_id:
1037                 uos2 = product_uom_obj.browse(cr, uid, uos)
1038                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1039                     uos = False
1040             else:
1041                 uos = False
1042         if product_obj.description_sale:
1043             result['notes'] = product_obj.description_sale
1044         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1045         if update_tax: #The quantity only have changed
1046             result['delay'] = (product_obj.sale_delay or 0.0)
1047             partner = partner_obj.browse(cr, uid, partner_id)
1048             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1049             result.update({'type': product_obj.procure_method})
1050
1051         if not flag:
1052             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1053         domain = {}
1054         if (not uom) and (not uos):
1055             result['product_uom'] = product_obj.uom_id.id
1056             if product_obj.uos_id:
1057                 result['product_uos'] = product_obj.uos_id.id
1058                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1059                 uos_category_id = product_obj.uos_id.category_id.id
1060             else:
1061                 result['product_uos'] = False
1062                 result['product_uos_qty'] = qty
1063                 uos_category_id = False
1064             result['th_weight'] = qty * product_obj.weight
1065             domain = {'product_uom':
1066                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1067                         'product_uos':
1068                         [('category_id', '=', uos_category_id)]}
1069
1070         elif uos and not uom: # only happens if uom is False
1071             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1072             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1073             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1074         elif uom: # whether uos is set or not
1075             default_uom = product_obj.uom_id and product_obj.uom_id.id
1076             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1077             if product_obj.uos_id:
1078                 result['product_uos'] = product_obj.uos_id.id
1079                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1080             else:
1081                 result['product_uos'] = False
1082                 result['product_uos_qty'] = qty
1083             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1084
1085         if not uom2:
1086             uom2 = product_obj.uom_id
1087         if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1088           and (product_obj.procure_method=='make_to_stock'):
1089             warning = {
1090                 'title': _('Not enough stock !'),
1091                 'message': _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') %
1092                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1093                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1094                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1095             }
1096
1097
1098         # get unit price
1099
1100         if not pricelist:
1101             warning = {
1102                 'title': 'No Pricelist !',
1103                 'message':
1104                     'You have to select a pricelist or a customer in the sale form !\n'
1105                     'Please set one before choosing a product.'
1106                 }
1107         else:
1108             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1109                     product, qty or 1.0, partner_id, {
1110                         'uom': uom,
1111                         'date': date_order,
1112                         })[pricelist]
1113             if price is False:
1114                 warning = {
1115                     'title': 'No valid pricelist line found !',
1116                     'message':
1117                         "Couldn't find a pricelist line matching this product and quantity.\n"
1118                         "You have to change either the product, the quantity or the pricelist."
1119                     }
1120             else:
1121                 result.update({'price_unit': price})
1122         return {'value': result, 'domain': domain, 'warning': warning}
1123
1124     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1125             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1126             lang=False, update_tax=True, date_order=False):
1127         res = self.product_id_change(cursor, user, ids, pricelist, product,
1128                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1129                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1130                 date_order=date_order)
1131         if 'product_uom' in res['value']:
1132             del res['value']['product_uom']
1133         if not uom:
1134             res['value']['price_unit'] = 0.0
1135         return res
1136
1137     def unlink(self, cr, uid, ids, context=None):
1138         if context is None:
1139             context = {}
1140         """Allows to delete sale order lines in draft,cancel states"""
1141         for rec in self.browse(cr, uid, ids, context=context):
1142             if rec.state not in ['draft', 'cancel']:
1143                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1144         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1145
1146 sale_order_line()
1147
1148
1149 class sale_config_picking_policy(osv.osv_memory):
1150     _name = 'sale.config.picking_policy'
1151     _inherit = 'res.config'
1152
1153     _columns = {
1154         'name': fields.char('Name', size=64),
1155         'picking_policy': fields.selection([
1156             ('direct', 'Direct Delivery'),
1157             ('one', 'All at Once')
1158         ], 'Picking Default Policy', required=True),
1159         'order_policy': fields.selection([
1160             ('manual', 'Invoice Based on Sales Orders'),
1161             ('picking', 'Invoice Based on Deliveries'),
1162         ], 'Shipping Default Policy', required=True,
1163            help="You can generate invoices based on sales orders or based on shippings."),
1164         'step': fields.selection([
1165             ('one', 'Delivery Order Only'),
1166             ('two', 'Picking List & Delivery Order')
1167         ], 'Steps To Deliver a Sale Order', required=True,
1168            help="By default, OpenERP is able to manage complex routing and paths "\
1169            "of products in your warehouse and partner locations. This will configure "\
1170            "the most common and simple methods to deliver products to the customer "\
1171            "in one or two operations by the worker.")
1172     }
1173     _defaults = {
1174         'picking_policy': lambda *a: 'direct',
1175         'order_policy': lambda *a: 'manual',
1176         'step': lambda *a: 'one'
1177     }
1178
1179     def execute(self, cr, uid, ids, context=None):
1180         if context is None:
1181             context = {}
1182         for o in self.browse(cr, uid, ids, context=context):
1183             ir_values_obj = self.pool.get('ir.values')
1184             ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1185             ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1186             if o.step == 'one':
1187                 md = self.pool.get('ir.model.data')
1188                 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1189                 group_id = md.browse(cr, uid, group_id, context=context).res_id
1190                 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1191                 menu_id = md.browse(cr, uid, menu_id, context=context).res_id
1192                 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1193                 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1194                 location_id = md.browse(cr, uid, location_id, context=context).res_id
1195                 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1196
1197 sale_config_picking_policy()
1198
1199 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: