c359c2f5822aff9e9feceab4b50d81ee3a4faa75
[odoo/odoo.git] / addons / sale / sale.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
24 import time
25 import pooler
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
29 import decimal_precision as dp
30 import netsvc
31
32 class sale_shop(osv.osv):
33     _name = "sale.shop"
34     _description = "Sales 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=False),
42     }
43     _defaults = {
44         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
45     }
46
47 sale_shop()
48
49 class sale_order(osv.osv):
50     _name = "sale.order"
51     _description = "Sales Order"
52
53     def copy(self, cr, uid, id, default=None, context=None):
54         if not default:
55             default = {}
56         default.update({
57             'state': 'draft',
58             'shipped': False,
59             'invoice_ids': [],
60             'picking_ids': [],
61             'date_confirm': False,
62             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
63         })
64         return super(sale_order, self).copy(cr, uid, id, default, context=context)
65
66     def _amount_line_tax(self, cr, uid, line, context=None):
67         val = 0.0
68         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']:
69             val += c.get('amount', 0.0)
70         return val
71
72     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
73         cur_obj = self.pool.get('res.currency')
74         res = {}
75         for order in self.browse(cr, uid, ids, context=context):
76             res[order.id] = {
77                 'amount_untaxed': 0.0,
78                 'amount_tax': 0.0,
79                 'amount_total': 0.0,
80             }
81             val = val1 = 0.0
82             cur = order.pricelist_id.currency_id
83             for line in order.order_line:
84                 val1 += line.price_subtotal
85                 val += self._amount_line_tax(cr, uid, line, context=context)
86             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
87             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
88             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
89         return res
90
91     # This is False
92     def _picked_rate(self, cr, uid, ids, name, arg, context=None):
93         if not ids:
94             return {}
95         res = {}
96         tmp = {}
97         for id in ids:
98             tmp[id] = {'picked': 0.0, 'total': 0.0}
99         cr.execute('''SELECT
100                 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
101             FROM
102                 stock_move m
103             LEFT JOIN
104                 stock_picking p on (p.id=m.picking_id)
105             LEFT JOIN
106                 procurement_order mp on (mp.move_id=m.id)
107             WHERE
108                 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
109
110         for item in cr.dictfetchall():
111             if item['move_state'] == 'cancel':
112                 continue
113
114             if item['picking_type'] == 'in':#this is a returned picking
115                 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
116                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
117                     tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
118             else:
119                 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
120                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
121                     tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
122
123         for order in self.browse(cr, uid, ids, context=context):
124             if order.shipped:
125                 res[order.id] = 100.0
126             else:
127                 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
128         return res
129
130     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
131         res = {}
132         for sale in self.browse(cursor, user, ids, context=context):
133             if sale.invoiced:
134                 res[sale.id] = 100.0
135                 continue
136             tot = 0.0
137             for invoice in sale.invoice_ids:
138                 if invoice.state not in ('draft', 'cancel'):
139                     tot += invoice.amount_untaxed
140             if tot:
141                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
142             else:
143                 res[sale.id] = 0.0
144         return res
145
146     def _invoiced(self, cursor, user, ids, name, arg, context=None):
147         res = {}
148         for sale in self.browse(cursor, user, ids, context=context):
149             res[sale.id] = True
150             for invoice in sale.invoice_ids:
151                 if invoice.state != 'paid':
152                     res[sale.id] = False
153                     break
154             if not sale.invoice_ids:
155                 res[sale.id] = False
156         return res
157
158     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
159         if not len(args):
160             return []
161         clause = ''
162         sale_clause = ''
163         no_invoiced = False
164         for arg in args:
165             if arg[1] == '=':
166                 if arg[2]:
167                     clause += 'AND inv.state = \'paid\''
168                 else:
169                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
170                     sale_clause = ',  sale_order AS sale '
171                     no_invoiced = True
172
173         cursor.execute('SELECT rel.order_id ' \
174                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
175                 'WHERE rel.invoice_id = inv.id ' + clause)
176         res = cursor.fetchall()
177         if no_invoiced:
178             cursor.execute('SELECT sale.id ' \
179                     'FROM sale_order AS sale ' \
180                     'WHERE sale.id NOT IN ' \
181                         '(SELECT rel.order_id ' \
182                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
183             res.extend(cursor.fetchall())
184         if not res:
185             return [('id', '=', 0)]
186         return [('id', 'in', [x[0] for x in res])]
187
188     def _get_order(self, cr, uid, ids, context=None):
189         result = {}
190         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
191             result[line.order_id.id] = True
192         return result.keys()
193
194     _columns = {
195         'name': fields.char('Order Reference', size=64, required=True,
196             readonly=True, states={'draft': [('readonly', False)]}, select=True),
197         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
198         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
199         'client_order_ref': fields.char('Customer Reference', size=64),
200         'state': fields.selection([
201             ('draft', 'Quotation'),
202             ('waiting_date', 'Waiting Schedule'),
203             ('manual', 'To Invoice'),
204             ('progress', 'In Progress'),
205             ('shipping_except', 'Shipping Exception'),
206             ('invoice_except', 'Invoice Exception'),
207             ('done', 'Done'),
208             ('cancel', 'Cancelled')
209             ], 'Order State', readonly=True, help="Givwizard = self.browse(cr, uid, ids)[0]es the state of the quotation or sales 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 order date.", select=True),
210         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
211         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
212         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
213         'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
214         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
215         'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sales order."),
216         '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."),
217         'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
218
219         'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
220         'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
221             '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?"""),
222         'order_policy': fields.selection([
223             ('prepaid', 'Pay before delivery'),
224             ('manual', 'Deliver & invoice on demand'),
225             ('picking', 'Invoice based on deliveries'),
226             ('postpaid', 'Invoice on order after delivery'),
227         ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
228                     help="""The Invoice Policy is used to synchronise invoice and delivery operations.
229   - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
230   - The 'Deliver & Invoice on demand' will create the picking order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice based on the sale order or the sale order lines.
231   - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
232   - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
233         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
234         'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
235
236         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
237         'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
238         'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Picking', readonly=True, help="This is a list of picking that has been generated for this sales order."),
239         'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
240         'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
241         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
242         'invoiced': fields.function(_invoiced, string='Paid',
243             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
244         'note': fields.text('Notes'),
245
246         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
247             store = {
248                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
249                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
250             },
251             multi='sums', help="The amount without tax."),
252         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
253             store = {
254                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
255                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
256             },
257             multi='sums', help="The tax amount."),
258         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
259             store = {
260                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
261                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
262             },
263             multi='sums', help="The total amount."),
264
265         '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, readonly=True, states={'draft': [('readonly', False)]}),
266         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
267         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
268         'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
269     }
270     _defaults = {
271         'picking_policy': 'direct',
272         'date_order': lambda *a: time.strftime(DEFAULT_SERVER_DATE_FORMAT),
273         'order_policy': 'manual',
274         'state': 'draft',
275         'user_id': lambda obj, cr, uid, context: uid,
276         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
277         'invoice_quantity': 'order',
278         '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'],
279         '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'],
280         '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'],
281     }
282     _sql_constraints = [
283         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
284     ]
285     _order = 'name desc'
286
287     # Form filling
288     def unlink(self, cr, uid, ids, context=None):
289         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
290         unlink_ids = []
291         for s in sale_orders:
292             if s['state'] in ['draft', 'cancel']:
293                 unlink_ids.append(s['id'])
294             else:
295                 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sale order, you must cancel it before ! To cancel a sale order, you must first cancel related picking or delivery orders.'))
296
297         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
298
299     def onchange_shop_id(self, cr, uid, ids, shop_id):
300         v = {}
301         if shop_id:
302             shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
303             v['project_id'] = shop.project_id.id
304             # Que faire si le client a une pricelist a lui ?
305             if shop.pricelist_id.id:
306                 v['pricelist_id'] = shop.pricelist_id.id
307         return {'value': v}
308
309     def action_cancel_draft(self, cr, uid, ids, *args):
310         if not len(ids):
311             return False
312         cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
313         line_ids = map(lambda x: x[0], cr.fetchall())
314         self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
315         self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
316         wf_service = netsvc.LocalService("workflow")
317         for inv_id in ids:
318             # Deleting the existing instance of workflow for SO
319             wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
320             wf_service.trg_create(uid, 'sale.order', inv_id, cr)
321         for (id,name) in self.name_get(cr, uid, ids):
322             message = _("The sales order '%s' has been set in draft state.") %(name,)
323             self.log(cr, uid, id, message)
324         return True
325
326     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
327         if (not pricelist_id) or (not order_lines):
328             return {}
329         warning = {
330             'title': _('Pricelist Warning!'),
331             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
332         }
333         return {'warning': warning}
334
335     def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
336         if not order_id:
337             return {}
338         val = {}
339         if not invoice_id:
340             val['partner_invoice_id'] = order_id
341         if not shipping_id:
342             val['partner_shipping_id'] = order_id
343         return {'value': val}
344
345     def onchange_partner_id(self, cr, uid, ids, part):
346         if not part:
347             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
348
349         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
350         part = self.pool.get('res.partner').browse(cr, uid, part)
351         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
352         payment_term = part.property_payment_term and part.property_payment_term.id or False
353         fiscal_position = part.property_account_position and part.property_account_position.id or False
354         dedicated_salesman = part.user_id and part.user_id.id or uid
355         val = {
356             'partner_invoice_id': addr['invoice'],
357             'partner_order_id': addr['contact'],
358             'partner_shipping_id': addr['delivery'],
359             'payment_term': payment_term,
360             'fiscal_position': fiscal_position,
361             'user_id': dedicated_salesman,
362         }
363         if pricelist:
364             val['pricelist_id'] = pricelist
365         return {'value': val}
366
367     def shipping_policy_change(self, cr, uid, ids, policy, context=None):
368         if not policy:
369             return {}
370         inv_qty = 'order'
371         if policy == 'prepaid':
372             inv_qty = 'order'
373         elif policy == 'picking':
374             inv_qty = 'procurement'
375         return {'value': {'invoice_quantity': inv_qty}}
376
377     def write(self, cr, uid, ids, vals, context=None):
378         if vals.get('order_policy', False):
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 vals.get('order_policy', False):
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         return True
395
396     #FIXME: the method should return the list of invoices created (invoice_ids)
397     # and not the id of the last invoice created (res). The problem is that we
398     # cannot change it directly since the method is called by the sales order
399     # workflow and I suppose it expects a single id...
400     def _inv_get(self, cr, uid, order, context=None):
401         return {}
402
403     def _make_invoice(self, cr, uid, order, lines, context=None):
404         journal_obj = self.pool.get('account.journal')
405         inv_obj = self.pool.get('account.invoice')
406         obj_invoice_line = self.pool.get('account.invoice.line')
407         if context is None:
408             context = {}
409
410         journal_ids = journal_obj.search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)], limit=1)
411         if not journal_ids:
412             raise osv.except_osv(_('Error !'),
413                 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
414         a = order.partner_id.property_account_receivable.id
415         pay_term = order.payment_term and order.payment_term.id or False
416         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
417         from_line_invoice_ids = []
418         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
419             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
420                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
421                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
422         for preinv in order.invoice_ids:
423             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
424                 for preline in preinv.invoice_line:
425                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
426                     lines.append(inv_line_id)
427         inv = {
428             'name': order.client_order_ref or '',
429             'origin': order.name,
430             'type': 'out_invoice',
431             'reference': order.client_order_ref or order.name,
432             'account_id': a,
433             'partner_id': order.partner_id.id,
434             'journal_id': journal_ids[0],
435             'address_invoice_id': order.partner_invoice_id.id,
436             'address_contact_id': order.partner_order_id.id,
437             'invoice_line': [(6, 0, lines)],
438             'currency_id': order.pricelist_id.currency_id.id,
439             'comment': order.note,
440             'payment_term': pay_term,
441             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
442             'date_invoice': context.get('date_invoice',False),
443             'company_id': order.company_id.id,
444             'user_id': order.user_id and order.user_id.id or False
445         }
446         inv.update(self._inv_get(cr, uid, order))
447         inv_id = inv_obj.create(cr, uid, inv, context=context)
448         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime(DEFAULT_SERVER_DATE_FORMAT))
449         if data.get('value', False):
450             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
451         inv_obj.button_compute(cr, uid, [inv_id])
452         return inv_id
453
454     def manual_invoice(self, cr, uid, ids, context=None):
455         mod_obj = self.pool.get('ir.model.data')
456         wf_service = netsvc.LocalService("workflow")
457         inv_ids = set()
458         inv_ids1 = set()
459         for id in ids:
460             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
461                 inv_ids.add(record.id)
462         # inv_ids would have old invoices if any
463         for id in ids:
464             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
465             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
466                 inv_ids1.add(record.id)
467         inv_ids = list(inv_ids1.difference(inv_ids))
468
469         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
470         res_id = res and res[1] or False,
471
472         return {
473             'name': _('Customer Invoices'),
474             'view_type': 'form',
475             'view_mode': 'form',
476             'view_id': [res_id],
477             'res_model': 'account.invoice',
478             'context': "{'type':'out_invoice'}",
479             'type': 'ir.actions.act_window',
480             'nodestroy': True,
481             'target': 'current',
482             'res_id': inv_ids and inv_ids[0] or False,
483         }
484
485     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
486         res = False
487         invoices = {}
488         invoice_ids = []
489         picking_obj = self.pool.get('stock.picking')
490         invoice = self.pool.get('account.invoice')
491         obj_sale_order_line = self.pool.get('sale.order.line')
492         partner_currency = {}
493         if context is None:
494             context = {}
495         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
496         # last day of the last month as invoice date
497         if date_inv:
498             context['date_inv'] = date_inv
499         for o in self.browse(cr, uid, ids, context=context):
500             currency_id = o.pricelist_id.currency_id.id
501             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
502                 raise osv.except_osv(
503                     _('Error !'),
504                     _('You cannot group sales having different currencies for the same partner.'))
505
506             partner_currency[o.partner_id.id] = currency_id
507             lines = []
508             for line in o.order_line:
509                 if line.invoiced:
510                     continue
511                 elif (line.state in states):
512                     lines.append(line.id)
513             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
514             if created_lines:
515                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
516         if not invoices:
517             for o in self.browse(cr, uid, ids, context=context):
518                 for i in o.invoice_ids:
519                     if i.state == 'draft':
520                         return i.id
521         for val in invoices.values():
522             if grouped:
523                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
524                 invoice_ref = ''
525                 for o, l in val:
526                     invoice_ref += o.name + '|'
527                     self.write(cr, uid, [o.id], {'state': 'progress'})
528                     if o.order_policy == 'picking':
529                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
530                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
531                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
532             else:
533                 for order, il in val:
534                     res = self._make_invoice(cr, uid, order, il, context=context)
535                     invoice_ids.append(res)
536                     self.write(cr, uid, [order.id], {'state': 'progress'})
537                     if order.order_policy == 'picking':
538                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
539                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
540         return res
541
542     def action_invoice_cancel(self, cr, uid, ids, context=None):
543         if context is None:
544             context = {}
545         for sale in self.browse(cr, uid, ids, context=context):
546             for line in sale.order_line:
547                 #
548                 # Check if the line is invoiced (has asociated invoice
549                 # lines from non-cancelled invoices).
550                 #
551                 invoiced = False
552                 for iline in line.invoice_lines:
553                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
554                         invoiced = True
555                         break
556                 # Update the line (only when needed)
557                 if line.invoiced != invoiced:
558                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
559         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
560         return True
561
562     def action_invoice_end(self, cr, uid, ids, context=None):
563         for order in self.browse(cr, uid, ids, context=context):
564             #
565             # Update the sale order lines state (and invoiced flag).
566             #
567             for line in order.order_line:
568                 vals = {}
569                 #
570                 # Check if the line is invoiced (has asociated invoice
571                 # lines from non-cancelled invoices).
572                 #
573                 invoiced = False
574                 for iline in line.invoice_lines:
575                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
576                         invoiced = True
577                         break
578                 if line.invoiced != invoiced:
579                     vals['invoiced'] = invoiced
580                 # If the line was in exception state, now it gets confirmed.
581                 if line.state == 'exception':
582                     vals['state'] = 'confirmed'
583                 # Update the line (only when needed).
584                 if vals:
585                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
586             #
587             # Update the sales order state.
588             #
589             if order.state == 'invoice_except':
590                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
591         return True
592
593     def action_cancel(self, cr, uid, ids, context=None):
594         wf_service = netsvc.LocalService("workflow")
595         if context is None:
596             context = {}
597         sale_order_line_obj = self.pool.get('sale.order.line')
598         proc_obj = self.pool.get('procurement.order')
599         for sale in self.browse(cr, uid, ids, context=context):
600             for pick in sale.picking_ids:
601                 if pick.state not in ('draft', 'cancel'):
602                     raise osv.except_osv(
603                         _('Could not cancel sales order !'),
604                         _('You must first cancel all picking attached to this sales order.'))
605                 if pick.state == 'cancel':
606                     for mov in pick.move_lines:
607                         proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
608                         if proc_ids:
609                             for proc in proc_ids:
610                                 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
611             for r in self.read(cr, uid, ids, ['picking_ids']):
612                 for pick in r['picking_ids']:
613                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
614             for inv in sale.invoice_ids:
615                 if inv.state not in ('draft', 'cancel'):
616                     raise osv.except_osv(
617                         _('Could not cancel this sales order !'),
618                         _('You must first cancel all invoices attached to this sales order.'))
619             for r in self.read(cr, uid, ids, ['invoice_ids']):
620                 for inv in r['invoice_ids']:
621                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
622             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
623                     {'state': 'cancel'})
624             message = _("The sales order '%s' has been cancelled.") % (sale.name,)
625             self.log(cr, uid, sale.id, message)
626         self.write(cr, uid, ids, {'state': 'cancel'})
627         return True
628
629     def action_wait(self, cr, uid, ids, *args):
630         for o in self.browse(cr, uid, ids):
631             if not o.order_line:
632                 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
633             if (o.order_policy == 'manual'):
634                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime(DEFAULT_SERVER_DATE_FORMAT)})
635             else:
636                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime(DEFAULT_SERVER_DATE_FORMAT)})
637             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
638             message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
639             self.log(cr, uid, o.id, message)
640         return True
641
642     def procurement_lines_get(self, cr, uid, ids, *args):
643         res = []
644         for order in self.browse(cr, uid, ids, context={}):
645             for line in order.order_line:
646                 if line.procurement_id:
647                     res.append(line.procurement_id.id)
648         return res
649
650     # if mode == 'finished':
651     #   returns True if all lines are done, False otherwise
652     # if mode == 'canceled':
653     #   returns True if there is at least one canceled line, False otherwise
654     def test_state(self, cr, uid, ids, mode, *args):
655         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
656         finished = True
657         canceled = False
658         notcanceled = False
659         write_done_ids = []
660         write_cancel_ids = []
661         for order in self.browse(cr, uid, ids, context={}):
662             for line in order.order_line:
663                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
664                     if line.state != 'done':
665                         write_done_ids.append(line.id)
666                 else:
667                     finished = False
668                 if line.procurement_id:
669                     if (line.procurement_id.state == 'cancel'):
670                         canceled = True
671                         if line.state != 'exception':
672                             write_cancel_ids.append(line.id)
673                     else:
674                         notcanceled = True
675         if write_done_ids:
676             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
677         if write_cancel_ids:
678             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
679
680         if mode == 'finished':
681             return finished
682         elif mode == 'canceled':
683             return canceled
684             if notcanceled:
685                 return False
686             return canceled
687
688     def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
689         return {
690             'name': line.name,
691             'origin': order.name,
692             'date_planned': date_planned,
693             'product_id': line.product_id.id,
694             'product_qty': line.product_uom_qty,
695             'product_uom': line.product_uom.id,
696             'product_uos_qty': (line.product_uos and line.product_uos_qty)\
697                     or line.product_uom_qty,
698             'product_uos': (line.product_uos and line.product_uos.id)\
699                     or line.product_uom.id,
700             'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
701             'procure_method': line.type,
702             'move_id': move_id,
703             'company_id': order.company_id.id,
704             'note': line.notes
705         }
706
707     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
708         location_id = order.shop_id.warehouse_id.lot_stock_id.id
709         output_id = order.shop_id.warehouse_id.lot_output_id.id
710         return {
711             'name': line.name[:250],
712             'picking_id': picking_id,
713             'product_id': line.product_id.id,
714             'date': date_planned,
715             'date_expected': date_planned,
716             'product_qty': line.product_uom_qty,
717             'product_uom': line.product_uom.id,
718             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
719             'product_uos': (line.product_uos and line.product_uos.id)\
720                     or line.product_uom.id,
721             'product_packaging': line.product_packaging.id,
722             'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
723             'location_id': location_id,
724             'location_dest_id': output_id,
725             'sale_line_id': line.id,
726             'tracking_id': False,
727             'state': 'draft',
728             #'state': 'waiting',
729             'note': line.notes,
730             'company_id': order.company_id.id,
731             'price_unit': line.product_id.standard_price or 0.0
732         }
733
734     def _prepare_order_picking(self, cr, uid, order, context=None):
735         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
736         return {
737             'name': pick_name,
738             'origin': order.name,
739             'date': order.date_order,
740             'type': 'out',
741             'state': 'auto',
742             'move_type': order.picking_policy,
743             'sale_id': order.id,
744             'address_id': order.partner_shipping_id.id,
745             'note': order.note,
746             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
747             'company_id': order.company_id.id,
748         }
749
750     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
751         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
752         """
753         Define ship_recreate for process after shipping exception
754         param order: sale order to which the order lines belong
755         param line: sale order line records to procure
756         param move_id: the ID of stock move
757         param proc_id: the ID of procurement
758         """
759         move_obj = self.pool.get('stock.move')
760         if order.state == 'shipping_except':
761             for pick in order.picking_ids:
762                 for move in pick.move_lines:
763                     if move.state == 'cancel':
764                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
765                         if mov_ids:
766                             for mov in move_obj.browse(cr, uid, mov_ids):
767                                 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
768                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
769                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
770         return True
771
772     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
773         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)    
774         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
775         return date_planned
776
777     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
778         """Create the required procurements to supply sale order lines, also connecting
779         the procurements to appropriate stock moves in order to bring the goods to the
780         sale order's requested location.
781
782         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
783         a standard outgoing picking will be created to wrap the stock moves, as returned
784         by :meth:`~._prepare_order_picking`.
785
786         Modules that wish to customize the procurements or partition the stock moves over
787         multiple stock pickings may override this method and call ``super()`` with
788         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
789
790         :param browse_record order: sale order to which the order lines belong
791         :param list(browse_record) order_lines: sale order line records to procure
792         :param int picking_id: optional ID of a stock picking to which the created stock moves
793                                will be added. A new picking will be created if ommitted.
794         :return: True
795         """
796         move_obj = self.pool.get('stock.move')
797         picking_obj = self.pool.get('stock.picking')
798         procurement_obj = self.pool.get('procurement.order')
799         proc_ids = []
800
801         for line in order_lines:
802             if line.state == 'done':
803                 continue
804
805             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
806
807             if line.product_id:
808                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
809                     if not picking_id:
810                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
811                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
812                 else:
813                     # a service has no stock move
814                     move_id = False
815
816                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
817                 proc_ids.append(proc_id)
818                 line.write({'procurement_id': proc_id})
819                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
820
821         wf_service = netsvc.LocalService("workflow")
822         if picking_id:
823             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
824
825         for proc_id in proc_ids:
826             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
827
828         val = {}
829         if order.state == 'shipping_except':
830             val['state'] = 'progress'
831             val['shipped'] = False
832
833             if (order.order_policy == 'manual'):
834                 for line in order.order_line:
835                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
836                         val['state'] = 'manual'
837                         break
838         order.write(val)
839         return True
840
841     def action_ship_create(self, cr, uid, ids, context=None):
842         for order in self.browse(cr, uid, ids, context=context):
843             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
844         return True
845
846     def action_ship_end(self, cr, uid, ids, context=None):
847         for order in self.browse(cr, uid, ids, context=context):
848             val = {'shipped': True}
849             if order.state == 'shipping_except':
850                 val['state'] = 'progress'
851                 if (order.order_policy == 'manual'):
852                     for line in order.order_line:
853                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
854                             val['state'] = 'manual'
855                             break
856             for line in order.order_line:
857                 towrite = []
858                 if line.state == 'exception':
859                     towrite.append(line.id)
860                 if towrite:
861                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
862             self.write(cr, uid, [order.id], val)
863         return True
864
865     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
866         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
867         for inv in invs:
868             part = inv['partner_id'] and inv['partner_id'][0]
869             pr = inv['amount_untaxed'] or 0.0
870             partnertype = 'customer'
871             eventtype = 'sale'
872             event = {
873                 'name': 'Order: '+name,
874                 'som': False,
875                 'description': 'Order '+str(inv['id']),
876                 'document': '',
877                 'partner_id': part,
878                 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
879                 'user_id': uid,
880                 'partner_type': partnertype,
881                 'probability': 1.0,
882                 'planned_revenue': pr,
883                 'planned_cost': 0.0,
884                 'type': eventtype
885             }
886             self.pool.get('res.partner.event').create(cr, uid, event)
887
888     def has_stockable_products(self, cr, uid, ids, *args):
889         for order in self.browse(cr, uid, ids):
890             for order_line in order.order_line:
891                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
892                     return True
893         return False
894 sale_order()
895
896 # TODO add a field price_unit_uos
897 # - update it on change product and unit price
898 # - use it in report if there is a uos
899 class sale_order_line(osv.osv):
900
901     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
902         tax_obj = self.pool.get('account.tax')
903         cur_obj = self.pool.get('res.currency')
904         res = {}
905         if context is None:
906             context = {}
907         for line in self.browse(cr, uid, ids, context=context):
908             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
909             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)
910             cur = line.order_id.pricelist_id.currency_id
911             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
912         return res
913
914     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
915         res = {}
916         for line in self.browse(cr, uid, ids, context=context):
917             try:
918                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
919             except:
920                 res[line.id] = 1
921         return res
922
923     def _get_uom_id(self, cr, uid, *args):
924         try:
925             proxy = self.pool.get('ir.model.data')
926             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
927             return result[1]
928         except Exception, ex:
929             return False
930
931     _name = 'sale.order.line'
932     _description = 'Sales Order Line'
933     _columns = {
934         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
935         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
936         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
937         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
938         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
939         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
940         'invoiced': fields.boolean('Invoiced', readonly=True),
941         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
942         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
943         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
944         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
945         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
946             help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
947         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
948         'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
949         'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
950         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
951         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
952         'product_uos': fields.many2one('product.uom', 'Product UoS'),
953         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
954         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
955         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
956         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
957         'notes': fields.text('Notes'),
958         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
959         'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
960                 help='* The \'Draft\' state is set when the related sales order in draft state. \
961                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
962                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
963                     \n* The \'Done\' state is set when the sales order line has been picked. \
964                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
965         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
966         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
967         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
968     }
969     _order = 'sequence, id'
970     _defaults = {
971         'product_uom' : _get_uom_id,
972         'discount': 0.0,
973         'delay': 0.0,
974         'product_uom_qty': 1,
975         'product_uos_qty': 1,
976         'sequence': 10,
977         'invoiced': 0,
978         'state': 'draft',
979         'type': 'make_to_stock',
980         'product_packaging': False,
981         'price_unit': 0.0,
982     }
983
984     def invoice_line_create(self, cr, uid, ids, context=None):
985         if context is None:
986             context = {}
987
988         def _get_line_qty(line):
989             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
990                 if line.product_uos:
991                     return line.product_uos_qty or 0.0
992                 return line.product_uom_qty
993             else:
994                 return self.pool.get('procurement.order').quantity_get(cr, uid,
995                         line.procurement_id.id, context=context)
996
997         def _get_line_uom(line):
998             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
999                 if line.product_uos:
1000                     return line.product_uos.id
1001                 return line.product_uom.id
1002             else:
1003                 return self.pool.get('procurement.order').uom_get(cr, uid,
1004                         line.procurement_id.id, context=context)
1005
1006         create_ids = []
1007         sales = {}
1008         for line in self.browse(cr, uid, ids, context=context):
1009             if not line.invoiced:
1010                 if line.product_id:
1011                     a = line.product_id.product_tmpl_id.property_account_income.id
1012                     if not a:
1013                         a = line.product_id.categ_id.property_account_income_categ.id
1014                     if not a:
1015                         raise osv.except_osv(_('Error !'),
1016                                 _('There is no income account defined ' \
1017                                         'for this product: "%s" (id:%d)') % \
1018                                         (line.product_id.name, line.product_id.id,))
1019                 else:
1020                     prop = self.pool.get('ir.property').get(cr, uid,
1021                             'property_account_income_categ', 'product.category',
1022                             context=context)
1023                     a = prop and prop.id or False
1024                 uosqty = _get_line_qty(line)
1025                 uos_id = _get_line_uom(line)
1026                 pu = 0.0
1027                 if uosqty:
1028                     pu = round(line.price_unit * line.product_uom_qty / uosqty,
1029                             self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1030                 fpos = line.order_id.fiscal_position or False
1031                 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
1032                 if not a:
1033                     raise osv.except_osv(_('Error !'),
1034                                 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1035                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
1036                     'name': line.name,
1037                     'origin': line.order_id.name,
1038                     'account_id': a,
1039                     'price_unit': pu,
1040                     'quantity': uosqty,
1041                     'discount': line.discount,
1042                     'uos_id': uos_id,
1043                     'product_id': line.product_id.id or False,
1044                     'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1045                     'note': line.notes,
1046                     'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1047                 })
1048                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1049                 self.write(cr, uid, [line.id], {'invoiced': True})
1050                 sales[line.order_id.id] = True
1051                 create_ids.append(inv_id)
1052         # Trigger workflow events
1053         wf_service = netsvc.LocalService("workflow")
1054         for sid in sales.keys():
1055             wf_service.trg_write(uid, 'sale.order', sid, cr)
1056         return create_ids
1057
1058     def button_cancel(self, cr, uid, ids, context=None):
1059         for line in self.browse(cr, uid, ids, context=context):
1060             if line.invoiced:
1061                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1062             for move_line in line.move_ids:
1063                 if move_line.state != 'cancel':
1064                     raise osv.except_osv(
1065                             _('Could not cancel sales order line!'),
1066                             _('You must first cancel stock moves attached to this sales order line.'))
1067         return self.write(cr, uid, ids, {'state': 'cancel'})
1068
1069     def button_confirm(self, cr, uid, ids, context=None):
1070         return self.write(cr, uid, ids, {'state': 'confirmed'})
1071
1072     def button_done(self, cr, uid, ids, context=None):
1073         wf_service = netsvc.LocalService("workflow")
1074         res = self.write(cr, uid, ids, {'state': 'done'})
1075         for line in self.browse(cr, uid, ids, context=context):
1076             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1077         return res
1078
1079     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1080         product_obj = self.pool.get('product.product')
1081         if not product_id:
1082             return {'value': {'product_uom': product_uos,
1083                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1084
1085         product = product_obj.browse(cr, uid, product_id)
1086         value = {
1087             'product_uom': product.uom_id.id,
1088         }
1089         # FIXME must depend on uos/uom of the product and not only of the coeff.
1090         try:
1091             value.update({
1092                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1093                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1094             })
1095         except ZeroDivisionError:
1096             pass
1097         return {'value': value}
1098
1099     def copy_data(self, cr, uid, id, default=None, context=None):
1100         if not default:
1101             default = {}
1102         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1103         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1104
1105     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1106                                    partner_id=False, packaging=False, flag=False, context=None):
1107         if not product:
1108             return {'value': {'product_packaging': False}}
1109         product_obj = self.pool.get('product.product')
1110         product_uom_obj = self.pool.get('product.uom')
1111         pack_obj = self.pool.get('product.packaging')
1112         warning = {}
1113         result = {}
1114         warning_msgs = ''
1115         if flag:
1116             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1117                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1118                     packaging=packaging, flag=False, context=context)
1119             warning_msgs = res.get('warning') and res['warning']['message']
1120
1121         products = product_obj.browse(cr, uid, product, context=context)
1122         if not products.packaging:
1123             packaging = result['product_packaging'] = False
1124         elif not packaging and products.packaging and not flag:
1125             packaging = products.packaging[0].id
1126             result['product_packaging'] = packaging
1127
1128         if packaging:
1129             default_uom = products.uom_id and products.uom_id.id
1130             pack = pack_obj.browse(cr, uid, packaging, context=context)
1131             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1132 #            qty = qty - qty % q + q
1133             if qty and (q and not (qty % q) == 0):
1134                 ean = pack.ean or _('(n/a)')
1135                 qty_pack = pack.qty
1136                 type_ul = pack.ul
1137                 if not warning_msgs:
1138                     warn_msg = _("You selected a quantity of %d Units.\n"
1139                                 "But it's not compatible with the selected packaging.\n"
1140                                 "Here is a proposition of quantities according to the packaging:\n"
1141                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1142                                     (qty, ean, qty_pack, type_ul.name)
1143                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1144                 warning = {
1145                        'title': _('Configuration Error !'),
1146                        'message': warning_msgs
1147                 }
1148             result['product_uom_qty'] = qty
1149
1150         return {'value': result, 'warning': warning}
1151
1152     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1153             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1154             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1155         context = context or {}
1156         lang = lang or context.get('lang',False)
1157         if not  partner_id:
1158             raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.'))
1159         warning = {}
1160         product_uom_obj = self.pool.get('product.uom')
1161         partner_obj = self.pool.get('res.partner')
1162         product_obj = self.pool.get('product.product')
1163         context = {'lang': lang, 'partner_id': partner_id}
1164         if partner_id:
1165             lang = partner_obj.browse(cr, uid, partner_id).lang
1166         context_partner = {'lang': lang, 'partner_id': partner_id}
1167
1168         if not product:
1169             return {'value': {'th_weight': 0, 'product_packaging': False,
1170                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1171                    'product_uos': []}}
1172         if not date_order:
1173             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1174
1175         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1176         result = res.get('value', {})
1177         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1178         product_obj = product_obj.browse(cr, uid, product, context=context)
1179
1180         uom2 = False
1181         if uom:
1182             uom2 = product_uom_obj.browse(cr, uid, uom)
1183             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1184                 uom = False
1185         if uos:
1186             if product_obj.uos_id:
1187                 uos2 = product_uom_obj.browse(cr, uid, uos)
1188                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1189                     uos = False
1190             else:
1191                 uos = False
1192         if product_obj.description_sale:
1193             result['notes'] = product_obj.description_sale
1194         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1195         if update_tax: #The quantity only have changed
1196             result['delay'] = (product_obj.sale_delay or 0.0)
1197             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1198             result.update({'type': product_obj.procure_method})
1199
1200         if not flag:
1201             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1202         domain = {}
1203         if (not uom) and (not uos):
1204             result['product_uom'] = product_obj.uom_id.id
1205             if product_obj.uos_id:
1206                 result['product_uos'] = product_obj.uos_id.id
1207                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1208                 uos_category_id = product_obj.uos_id.category_id.id
1209             else:
1210                 result['product_uos'] = False
1211                 result['product_uos_qty'] = qty
1212                 uos_category_id = False
1213             result['th_weight'] = qty * product_obj.weight
1214             domain = {'product_uom':
1215                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1216                         'product_uos':
1217                         [('category_id', '=', uos_category_id)]}
1218
1219         elif uos and not uom: # only happens if uom is False
1220             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1221             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1222             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1223         elif uom: # whether uos is set or not
1224             default_uom = product_obj.uom_id and product_obj.uom_id.id
1225             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1226             if product_obj.uos_id:
1227                 result['product_uos'] = product_obj.uos_id.id
1228                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1229             else:
1230                 result['product_uos'] = False
1231                 result['product_uos_qty'] = qty
1232             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1233
1234         if not uom2:
1235             uom2 = product_obj.uom_id
1236         if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1237           and (product_obj.procure_method=='make_to_stock'):
1238             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1239                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1240                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1241                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1242             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1243         # get unit price
1244
1245         if not pricelist:
1246             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1247                     'Please set one before choosing a product.')
1248             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1249         else:
1250             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1251                     product, qty or 1.0, partner_id, {
1252                         'uom': uom or result.get('product_uom'),
1253                         'date': date_order,
1254                         })[pricelist]
1255             if price is False:
1256                 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1257                         "You have to change either the product, the quantity or the pricelist.")
1258
1259                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1260             else:
1261                 result.update({'price_unit': price})
1262         if warning_msgs:
1263             warning = {
1264                        'title': _('Configuration Error !'),
1265                        'message' : warning_msgs
1266                     }
1267         return {'value': result, 'domain': domain, 'warning': warning}
1268
1269     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1270             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1271             lang=False, update_tax=True, date_order=False, context=None):
1272         context = context or {}
1273         lang = lang or ('lang' in context and context['lang'])
1274         res = self.product_id_change(cursor, user, ids, pricelist, product,
1275                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1276                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1277                 date_order=date_order, context=context)
1278         if 'product_uom' in res['value']:
1279             del res['value']['product_uom']
1280         if not uom:
1281             res['value']['price_unit'] = 0.0
1282         return res
1283
1284     def unlink(self, cr, uid, ids, context=None):
1285         if context is None:
1286             context = {}
1287         """Allows to delete sales order lines in draft,cancel states"""
1288         for rec in self.browse(cr, uid, ids, context=context):
1289             if rec.state not in ['draft', 'cancel']:
1290                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1291         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1292
1293 sale_order_line()
1294
1295 class sale_config_picking_policy(osv.osv_memory):
1296     _name = 'sale.config.picking_policy'
1297     _inherit = 'res.config'
1298
1299     _columns = {
1300         'name': fields.char('Name', size=64),
1301         'sale_orders': fields.boolean('Based on Sales Orders',),
1302         'deli_orders': fields.boolean('Based on Delivery Orders'),
1303         'task_work': fields.boolean('Based on Tasks\' Work'),
1304         'timesheet': fields.boolean('Based on Timesheet'),
1305         'order_policy': fields.selection([
1306             ('manual', 'Invoice Based on Sales Orders'),
1307             ('picking', 'Invoice Based on Deliveries'),
1308         ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1309         'charge_delivery': fields.boolean('Do you charge the delivery?'),
1310         'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1311     }
1312     _defaults = {
1313         'order_policy': 'manual',
1314         'time_unit': lambda self, cr, uid, c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c) and self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c)[0] or False,
1315     }
1316
1317     def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1318         res = {}
1319         if sale:
1320             res.update({'order_policy': 'manual'})
1321         elif deli:
1322             res.update({'order_policy': 'picking'})
1323         return {'value':res}
1324
1325     def execute(self, cr, uid, ids, context=None):
1326         ir_values_obj = self.pool.get('ir.values')
1327         data_obj = self.pool.get('ir.model.data')
1328         menu_obj = self.pool.get('ir.ui.menu')
1329         module_obj = self.pool.get('ir.module.module')
1330         module_upgrade_obj = self.pool.get('base.module.upgrade')
1331         module_name = []
1332
1333         group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1334
1335         wizard = self.browse(cr, uid, ids)[0]
1336
1337         if wizard.sale_orders:
1338             menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1339             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1340
1341         if wizard.deli_orders:
1342             menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1343             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1344
1345         if wizard.task_work:
1346             module_name.append('project_timesheet')
1347             module_name.append('project_mrp')
1348             module_name.append('account_analytic_analysis')
1349
1350         if wizard.timesheet:
1351             module_name.append('account_analytic_analysis')
1352
1353         if wizard.charge_delivery:
1354             module_name.append('delivery')
1355
1356         if len(module_name):
1357             module_ids = []
1358             need_install = False
1359             module_ids = []
1360             for module in module_name:
1361                 data_id = module_obj.name_search(cr, uid , module, [], '=')
1362                 module_ids.append(data_id[0][0])
1363
1364             for module in module_obj.browse(cr, uid, module_ids):
1365                 if module.state == 'uninstalled':
1366                     module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1367                     need_install = True
1368                     cr.commit()
1369             if need_install:
1370                 pooler.restart_pool(cr.dbname, update_module=True)[1]
1371
1372         if wizard.time_unit:
1373             prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1374             product_obj = self.pool.get('product.product')
1375             product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1376
1377         ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1378         if wizard.task_work and wizard.time_unit:
1379             company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1380             self.pool.get('res.company').write(cr, uid, [company_id], {
1381                 'project_time_mode_id': wizard.time_unit.id
1382             }, context=context)
1383
1384 sale_config_picking_policy()
1385
1386 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: