7c0d5e0252c20fe279ea357e31bf72d4fc9e0f0e
[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 date 'Ordered Date'.", select=True),
210         'date_order': fields.date('Ordered 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 products 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', '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)', 'Order Reference must be unique !'),
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         print order_lines
328         if (not pricelist_id) or (not order_lines):
329             return {}
330         warning = {
331             'title': _('Pricelist Warning!'),
332             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
333         }
334         return {'warning': warning}
335
336     def onchange_partner_id(self, cr, uid, ids, part):
337         if not part:
338             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
339
340         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
341         part = self.pool.get('res.partner').browse(cr, uid, part)
342         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
343         payment_term = part.property_payment_term and part.property_payment_term.id or False
344         fiscal_position = part.property_account_position and part.property_account_position.id or False
345         dedicated_salesman = part.user_id and part.user_id.id or uid
346         val = {
347             'partner_invoice_id': addr['invoice'],
348             'partner_order_id': addr['contact'],
349             'partner_shipping_id': addr['delivery'],
350             'payment_term': payment_term,
351             'fiscal_position': fiscal_position,
352             'user_id': dedicated_salesman,
353         }
354         if pricelist:
355             val['pricelist_id'] = pricelist
356         return {'value': val}
357
358     def shipping_policy_change(self, cr, uid, ids, policy, context=None):
359         if not policy:
360             return {}
361         inv_qty = 'order'
362         if policy == 'prepaid':
363             inv_qty = 'order'
364         elif policy == 'picking':
365             inv_qty = 'procurement'
366         return {'value': {'invoice_quantity': inv_qty}}
367
368     def write(self, cr, uid, ids, vals, context=None):
369         if vals.get('order_policy', False):
370             if vals['order_policy'] == 'prepaid':
371                 vals.update({'invoice_quantity': 'order'})
372             elif vals['order_policy'] == 'picking':
373                 vals.update({'invoice_quantity': 'procurement'})
374         return super(sale_order, self).write(cr, uid, ids, vals, context=context)
375
376     def create(self, cr, uid, vals, context=None):
377         if vals.get('order_policy', False):
378             if vals['order_policy'] == 'prepaid':
379                 vals.update({'invoice_quantity': 'order'})
380             if vals['order_policy'] == 'picking':
381                 vals.update({'invoice_quantity': 'procurement'})
382         return super(sale_order, self).create(cr, uid, vals, context=context)
383
384     def button_dummy(self, cr, uid, ids, context=None):
385         return True
386
387     #FIXME: the method should return the list of invoices created (invoice_ids)
388     # and not the id of the last invoice created (res). The problem is that we
389     # cannot change it directly since the method is called by the sales order
390     # workflow and I suppose it expects a single id...
391     def _inv_get(self, cr, uid, order, context=None):
392         return {}
393
394     def _make_invoice(self, cr, uid, order, lines, context=None):
395         journal_obj = self.pool.get('account.journal')
396         inv_obj = self.pool.get('account.invoice')
397         obj_invoice_line = self.pool.get('account.invoice.line')
398         if context is None:
399             context = {}
400
401         journal_ids = journal_obj.search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)], limit=1)
402         if not journal_ids:
403             raise osv.except_osv(_('Error !'),
404                 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
405         a = order.partner_id.property_account_receivable.id
406         pay_term = order.payment_term and order.payment_term.id or False
407         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
408         from_line_invoice_ids = []
409         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
410             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
411                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
412                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
413         for preinv in order.invoice_ids:
414             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
415                 for preline in preinv.invoice_line:
416                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
417                     lines.append(inv_line_id)
418         inv = {
419             'name': order.client_order_ref or '',
420             'origin': order.name,
421             'type': 'out_invoice',
422             'reference': order.client_order_ref or order.name,
423             'account_id': a,
424             'partner_id': order.partner_id.id,
425             'journal_id': journal_ids[0],
426             'address_invoice_id': order.partner_invoice_id.id,
427             'address_contact_id': order.partner_order_id.id,
428             'invoice_line': [(6, 0, lines)],
429             'currency_id': order.pricelist_id.currency_id.id,
430             'comment': order.note,
431             'payment_term': pay_term,
432             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
433             'date_invoice': context.get('date_invoice',False),
434             'company_id': order.company_id.id,
435             'user_id': order.user_id and order.user_id.id or False
436         }
437         inv.update(self._inv_get(cr, uid, order))
438         inv_id = inv_obj.create(cr, uid, inv, context=context)
439         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime(DEFAULT_SERVER_DATE_FORMAT))
440         if data.get('value', False):
441             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
442         inv_obj.button_compute(cr, uid, [inv_id])
443         return inv_id
444
445     def manual_invoice(self, cr, uid, ids, context=None):
446         mod_obj = self.pool.get('ir.model.data')
447         wf_service = netsvc.LocalService("workflow")
448         inv_ids = set()
449         inv_ids1 = set()
450         for id in ids:
451             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
452                 inv_ids.add(record.id)
453         # inv_ids would have old invoices if any
454         for id in ids:
455             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
456             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
457                 inv_ids1.add(record.id)
458         inv_ids = list(inv_ids1.difference(inv_ids))
459
460         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
461         res_id = res and res[1] or False,
462
463         return {
464             'name': _('Customer Invoices'),
465             'view_type': 'form',
466             'view_mode': 'form',
467             'view_id': [res_id],
468             'res_model': 'account.invoice',
469             'context': "{'type':'out_invoice'}",
470             'type': 'ir.actions.act_window',
471             'nodestroy': True,
472             'target': 'current',
473             'res_id': inv_ids and inv_ids[0] or False,
474         }
475
476     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
477         res = False
478         invoices = {}
479         invoice_ids = []
480         picking_obj = self.pool.get('stock.picking')
481         invoice = self.pool.get('account.invoice')
482         obj_sale_order_line = self.pool.get('sale.order.line')
483         partner_currency = {}
484         if context is None:
485             context = {}
486         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
487         # last day of the last month as invoice date
488         if date_inv:
489             context['date_inv'] = date_inv
490         for o in self.browse(cr, uid, ids, context=context):
491             currency_id = o.pricelist_id.currency_id.id
492             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
493                 raise osv.except_osv(
494                     _('Error !'),
495                     _('You cannot group sales having different currencies for the same partner.'))
496
497             partner_currency[o.partner_id.id] = currency_id
498             lines = []
499             for line in o.order_line:
500                 if line.invoiced:
501                     continue
502                 elif (line.state in states):
503                     lines.append(line.id)
504             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
505             if created_lines:
506                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
507         if not invoices:
508             for o in self.browse(cr, uid, ids, context=context):
509                 for i in o.invoice_ids:
510                     if i.state == 'draft':
511                         return i.id
512         for val in invoices.values():
513             if grouped:
514                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
515                 invoice_ref = ''
516                 for o, l in val:
517                     invoice_ref += o.name + '|'
518                     self.write(cr, uid, [o.id], {'state': 'progress'})
519                     if o.order_policy == 'picking':
520                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
521                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
522                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
523             else:
524                 for order, il in val:
525                     res = self._make_invoice(cr, uid, order, il, context=context)
526                     invoice_ids.append(res)
527                     self.write(cr, uid, [order.id], {'state': 'progress'})
528                     if order.order_policy == 'picking':
529                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
530                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
531         return res
532
533     def action_invoice_cancel(self, cr, uid, ids, context=None):
534         if context is None:
535             context = {}
536         for sale in self.browse(cr, uid, ids, context=context):
537             for line in sale.order_line:
538                 #
539                 # Check if the line is invoiced (has asociated invoice
540                 # lines from non-cancelled invoices).
541                 #
542                 invoiced = False
543                 for iline in line.invoice_lines:
544                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
545                         invoiced = True
546                         break
547                 # Update the line (only when needed)
548                 if line.invoiced != invoiced:
549                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
550         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
551         return True
552
553     def action_invoice_end(self, cr, uid, ids, context=None):
554         for order in self.browse(cr, uid, ids, context=context):
555             #
556             # Update the sale order lines state (and invoiced flag).
557             #
558             for line in order.order_line:
559                 vals = {}
560                 #
561                 # Check if the line is invoiced (has asociated invoice
562                 # lines from non-cancelled invoices).
563                 #
564                 invoiced = False
565                 for iline in line.invoice_lines:
566                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
567                         invoiced = True
568                         break
569                 if line.invoiced != invoiced:
570                     vals['invoiced'] = invoiced
571                 # If the line was in exception state, now it gets confirmed.
572                 if line.state == 'exception':
573                     vals['state'] = 'confirmed'
574                 # Update the line (only when needed).
575                 if vals:
576                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
577             #
578             # Update the sales order state.
579             #
580             if order.state == 'invoice_except':
581                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
582         return True
583
584     def action_cancel(self, cr, uid, ids, context=None):
585         wf_service = netsvc.LocalService("workflow")
586         if context is None:
587             context = {}
588         sale_order_line_obj = self.pool.get('sale.order.line')
589         proc_obj = self.pool.get('procurement.order')
590         for sale in self.browse(cr, uid, ids, context=context):
591             for pick in sale.picking_ids:
592                 if pick.state not in ('draft', 'cancel'):
593                     raise osv.except_osv(
594                         _('Could not cancel sales order !'),
595                         _('You must first cancel all picking attached to this sales order.'))
596                 if pick.state == 'cancel':
597                     for mov in pick.move_lines:
598                         proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
599                         if proc_ids:
600                             for proc in proc_ids:
601                                 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
602             for r in self.read(cr, uid, ids, ['picking_ids']):
603                 for pick in r['picking_ids']:
604                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
605             for inv in sale.invoice_ids:
606                 if inv.state not in ('draft', 'cancel'):
607                     raise osv.except_osv(
608                         _('Could not cancel this sales order !'),
609                         _('You must first cancel all invoices attached to this sales order.'))
610             for r in self.read(cr, uid, ids, ['invoice_ids']):
611                 for inv in r['invoice_ids']:
612                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
613             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
614                     {'state': 'cancel'})
615             message = _("The sales order '%s' has been cancelled.") % (sale.name,)
616             self.log(cr, uid, sale.id, message)
617         self.write(cr, uid, ids, {'state': 'cancel'})
618         return True
619
620     def action_wait(self, cr, uid, ids, *args):
621         for o in self.browse(cr, uid, ids):
622             if not o.order_line:
623                 raise osv.except_osv(_('No Sale Order Lines !'), _('Please create sale order lines.'))
624             if (o.order_policy == 'manual'):
625                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime(DEFAULT_SERVER_DATE_FORMAT)})
626             else:
627                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime(DEFAULT_SERVER_DATE_FORMAT)})
628             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
629             message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
630             self.log(cr, uid, o.id, message)
631         return True
632
633     def procurement_lines_get(self, cr, uid, ids, *args):
634         res = []
635         for order in self.browse(cr, uid, ids, context={}):
636             for line in order.order_line:
637                 if line.procurement_id:
638                     res.append(line.procurement_id.id)
639         return res
640
641     # if mode == 'finished':
642     #   returns True if all lines are done, False otherwise
643     # if mode == 'canceled':
644     #   returns True if there is at least one canceled line, False otherwise
645     def test_state(self, cr, uid, ids, mode, *args):
646         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
647         finished = True
648         canceled = False
649         notcanceled = False
650         write_done_ids = []
651         write_cancel_ids = []
652         for order in self.browse(cr, uid, ids, context={}):
653             for line in order.order_line:
654                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
655                     if line.state != 'done':
656                         write_done_ids.append(line.id)
657                 else:
658                     finished = False
659                 if line.procurement_id:
660                     if (line.procurement_id.state == 'cancel'):
661                         canceled = True
662                         if line.state != 'exception':
663                             write_cancel_ids.append(line.id)
664                     else:
665                         notcanceled = True
666         if write_done_ids:
667             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
668         if write_cancel_ids:
669             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
670
671         if mode == 'finished':
672             return finished
673         elif mode == 'canceled':
674             return canceled
675             if notcanceled:
676                 return False
677             return canceled
678
679     def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, *args):
680         return {
681             'name': line.name,
682             'origin': order.name,
683             'date_planned': date_planned,
684             'product_id': line.product_id.id,
685             'product_qty': line.product_uom_qty,
686             'product_uom': line.product_uom.id,
687             'product_uos_qty': (line.product_uos and line.product_uos_qty)\
688                     or line.product_uom_qty,
689             'product_uos': (line.product_uos and line.product_uos.id)\
690                     or line.product_uom.id,
691             'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
692             'procure_method': line.type,
693             'move_id': move_id,
694             'property_ids': [(6, 0, [x.id for x in line.property_ids])],
695             'company_id': order.company_id.id,
696             'sale_line_id': line.id,
697         }
698
699     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, *args):
700         location_id = order.shop_id.warehouse_id.lot_stock_id.id
701         output_id = order.shop_id.warehouse_id.lot_output_id.id
702         return {
703             'name': line.name[:64],
704             'picking_id': picking_id,
705             'product_id': line.product_id.id,
706             'date': date_planned,
707             'date_expected': date_planned,
708             'product_qty': line.product_uom_qty,
709             'product_uom': line.product_uom.id,
710             'product_uos_qty': line.product_uos_qty,
711             'product_uos': (line.product_uos and line.product_uos.id)\
712                     or line.product_uom.id,
713             'product_packaging': line.product_packaging.id,
714             'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
715             'location_id': location_id,
716             'location_dest_id': output_id,
717             'sale_line_id': line.id,
718             'tracking_id': False,
719             'state': 'draft',
720             #'state': 'waiting',
721             'note': line.notes,
722             'company_id': order.company_id.id,
723             'price_unit': line.product_id.standard_price or 0.0
724         }
725
726     def _prepare_order_picking(self, cr, uid, order, *args):
727         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
728         return {
729             'name': pick_name,
730             'origin': order.name,
731             'date': order.date_order,
732             'type': 'out',
733             'state': 'auto',
734             'move_type': order.picking_policy,
735             'sale_id': order.id,
736             'address_id': order.partner_shipping_id.id,
737             'note': order.note,
738             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
739             'company_id': order.company_id.id,
740         }
741
742     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, *args):
743         """Create the required procurements to supply sale order lines, also connecting
744         the procurements to appropriate stock moves in order to bring the goods to the
745         sale order's requested location.
746
747         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
748         a standard outgoing picking will be created to wrap the stock moves, as returned
749         by :meth:`~._prepare_order_picking`.
750
751         Modules that wish to customize the procurements or partition the stock moves over
752         multiple stock pickings may override this method and call ``super()`` with
753         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
754
755         :param browse_record order: sale order to which the order lines belong
756         :param list(browse_record) order_lines: sale order line records to procure
757         :param int picking_id: optional ID of a stock picking to which the created stock moves
758                                will be added. A new picking will be created if ommitted.
759         :return: True
760         """
761         proc_ids = []
762         for line in order_lines:
763             if line.state == 'done':
764                 continue
765
766             date_planned = datetime.strptime(order.date_order, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
767             date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
768
769             if line.product_id:
770                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
771                     if not picking_id:
772                         picking_id = self.pool.get('stock.picking').create(cr, uid, self._prepare_order_picking(cr, uid, order, *args))
773                     move_id = self.pool.get('stock.move').create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, *args))
774                 else:
775                     # a service has no stock move
776                     move_id = False
777
778                 proc_id = self.pool.get('procurement.order').create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, *args))
779                 proc_ids.append(proc_id)
780                 line.write({'procurement_id': proc_id})
781
782                 # FIXME: deals with potentially cancelled shipments, seems broken, see below
783                 # FIXME: was introduced by revid: mtr@mtr-20101125100355-0a1b7m792t63mssv
784                 if order.state == 'shipping_except':
785                     for pick in order.picking_ids:
786                         for move in pick.move_lines:
787                             if move.state == 'cancel':
788                                 mov_ids = self.pool.get('stock.move').search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
789                                 if mov_ids:
790                                     for mov in move_obj.browse(cr, uid, mov_ids):
791                                         # 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?
792                                         self.pool.get('stock.move').write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
793                                         self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
794
795         wf_service = netsvc.LocalService("workflow")
796         if picking_id:
797             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
798
799         for proc_id in proc_ids:
800             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
801
802         val = {}
803         if order.state == 'shipping_except':
804             val['state'] = 'progress'
805             val['shipped'] = False
806
807             if (order.order_policy == 'manual'):
808                 for line in order.order_line:
809                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
810                         val['state'] = 'manual'
811                         break
812         order.write(val)
813         return True
814
815     def action_ship_create(self, cr, uid, ids, *args):
816         for order in self.browse(cr, uid, ids, context={}):
817             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, *args)
818         return True
819
820     def action_ship_end(self, cr, uid, ids, context=None):
821         for order in self.browse(cr, uid, ids, context=context):
822             val = {'shipped': True}
823             if order.state == 'shipping_except':
824                 val['state'] = 'progress'
825                 if (order.order_policy == 'manual'):
826                     for line in order.order_line:
827                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
828                             val['state'] = 'manual'
829                             break
830             for line in order.order_line:
831                 towrite = []
832                 if line.state == 'exception':
833                     towrite.append(line.id)
834                 if towrite:
835                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
836             self.write(cr, uid, [order.id], val)
837         return True
838
839     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
840         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
841         for inv in invs:
842             part = inv['partner_id'] and inv['partner_id'][0]
843             pr = inv['amount_untaxed'] or 0.0
844             partnertype = 'customer'
845             eventtype = 'sale'
846             event = {
847                 'name': 'Order: '+name,
848                 'som': False,
849                 'description': 'Order '+str(inv['id']),
850                 'document': '',
851                 'partner_id': part,
852                 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
853                 'user_id': uid,
854                 'partner_type': partnertype,
855                 'probability': 1.0,
856                 'planned_revenue': pr,
857                 'planned_cost': 0.0,
858                 'type': eventtype
859             }
860             self.pool.get('res.partner.event').create(cr, uid, event)
861
862     def has_stockable_products(self, cr, uid, ids, *args):
863         for order in self.browse(cr, uid, ids):
864             for order_line in order.order_line:
865                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
866                     return True
867         return False
868 sale_order()
869
870 # TODO add a field price_unit_uos
871 # - update it on change product and unit price
872 # - use it in report if there is a uos
873 class sale_order_line(osv.osv):
874
875     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
876         tax_obj = self.pool.get('account.tax')
877         cur_obj = self.pool.get('res.currency')
878         res = {}
879         if context is None:
880             context = {}
881         for line in self.browse(cr, uid, ids, context=context):
882             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
883             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)
884             cur = line.order_id.pricelist_id.currency_id
885             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
886         return res
887
888     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
889         res = {}
890         for line in self.browse(cr, uid, ids, context=context):
891             try:
892                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
893             except:
894                 res[line.id] = 1
895         return res
896
897     def _get_uom_id(self, cr, uid, *args):
898         try:
899             proxy = self.pool.get('ir.model.data')
900             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
901             return result[1]
902         except Exception, ex:
903             return False
904
905     _name = 'sale.order.line'
906     _description = 'Sales Order Line'
907     _columns = {
908         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
909         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
910         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
911         '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)]}),
912         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
913         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
914         'invoiced': fields.boolean('Invoiced', readonly=True),
915         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
916         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
917         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
918         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
919         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
920             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."),
921         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
922         'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
923         'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
924         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
925         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
926         'product_uos': fields.many2one('product.uom', 'Product UoS'),
927         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
928         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
929         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
930         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
931         'notes': fields.text('Notes'),
932         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
933         'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
934                 help='* The \'Draft\' state is set when the related sales order in draft state. \
935                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
936                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
937                     \n* The \'Done\' state is set when the sales order line has been picked. \
938                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
939         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
940         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
941         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
942     }
943     _order = 'sequence, id'
944     _defaults = {
945         'product_uom' : _get_uom_id,
946         'discount': 0.0,
947         'delay': 0.0,
948         'product_uom_qty': 1,
949         'product_uos_qty': 1,
950         'sequence': 10,
951         'invoiced': 0,
952         'state': 'draft',
953         'type': 'make_to_stock',
954         'product_packaging': False,
955         'price_unit': 0.0,
956     }
957
958     def invoice_line_create(self, cr, uid, ids, context=None):
959         if context is None:
960             context = {}
961
962         def _get_line_qty(line):
963             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
964                 if line.product_uos:
965                     return line.product_uos_qty or 0.0
966                 return line.product_uom_qty
967             else:
968                 return self.pool.get('procurement.order').quantity_get(cr, uid,
969                         line.procurement_id.id, context=context)
970
971         def _get_line_uom(line):
972             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
973                 if line.product_uos:
974                     return line.product_uos.id
975                 return line.product_uom.id
976             else:
977                 return self.pool.get('procurement.order').uom_get(cr, uid,
978                         line.procurement_id.id, context=context)
979
980         create_ids = []
981         sales = {}
982         for line in self.browse(cr, uid, ids, context=context):
983             if not line.invoiced:
984                 if line.product_id:
985                     a = line.product_id.product_tmpl_id.property_account_income.id
986                     if not a:
987                         a = line.product_id.categ_id.property_account_income_categ.id
988                     if not a:
989                         raise osv.except_osv(_('Error !'),
990                                 _('There is no income account defined ' \
991                                         'for this product: "%s" (id:%d)') % \
992                                         (line.product_id.name, line.product_id.id,))
993                 else:
994                     prop = self.pool.get('ir.property').get(cr, uid,
995                             'property_account_income_categ', 'product.category',
996                             context=context)
997                     a = prop and prop.id or False
998                 uosqty = _get_line_qty(line)
999                 uos_id = _get_line_uom(line)
1000                 pu = 0.0
1001                 if uosqty:
1002                     pu = round(line.price_unit * line.product_uom_qty / uosqty,
1003                             self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1004                 fpos = line.order_id.fiscal_position or False
1005                 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
1006                 if not a:
1007                     raise osv.except_osv(_('Error !'),
1008                                 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1009                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
1010                     'name': line.name,
1011                     'origin': line.order_id.name,
1012                     'account_id': a,
1013                     'price_unit': pu,
1014                     'quantity': uosqty,
1015                     'discount': line.discount,
1016                     'uos_id': uos_id,
1017                     'product_id': line.product_id.id or False,
1018                     'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1019                     'note': line.notes,
1020                     'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1021                 })
1022                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1023                 self.write(cr, uid, [line.id], {'invoiced': True})
1024                 sales[line.order_id.id] = True
1025                 create_ids.append(inv_id)
1026         # Trigger workflow events
1027         wf_service = netsvc.LocalService("workflow")
1028         for sid in sales.keys():
1029             wf_service.trg_write(uid, 'sale.order', sid, cr)
1030         return create_ids
1031
1032     def button_cancel(self, cr, uid, ids, context=None):
1033         for line in self.browse(cr, uid, ids, context=context):
1034             if line.invoiced:
1035                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1036             for move_line in line.move_ids:
1037                 if move_line.state != 'cancel':
1038                     raise osv.except_osv(
1039                             _('Could not cancel sales order line!'),
1040                             _('You must first cancel stock moves attached to this sales order line.'))
1041         return self.write(cr, uid, ids, {'state': 'cancel'})
1042
1043     def button_confirm(self, cr, uid, ids, context=None):
1044         return self.write(cr, uid, ids, {'state': 'confirmed'})
1045
1046     def button_done(self, cr, uid, ids, context=None):
1047         wf_service = netsvc.LocalService("workflow")
1048         res = self.write(cr, uid, ids, {'state': 'done'})
1049         for line in self.browse(cr, uid, ids, context=context):
1050             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1051         return res
1052
1053     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1054         product_obj = self.pool.get('product.product')
1055         if not product_id:
1056             return {'value': {'product_uom': product_uos,
1057                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1058
1059         product = product_obj.browse(cr, uid, product_id)
1060         value = {
1061             'product_uom': product.uom_id.id,
1062         }
1063         # FIXME must depend on uos/uom of the product and not only of the coeff.
1064         try:
1065             value.update({
1066                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1067                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1068             })
1069         except ZeroDivisionError:
1070             pass
1071         return {'value': value}
1072
1073     def copy_data(self, cr, uid, id, default=None, context=None):
1074         if not default:
1075             default = {}
1076         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1077         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1078
1079     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1080                                    partner_id=False, packaging=False, flag=False, context=None):
1081         if not product:
1082             return {'value': {'product_packaging': False}}
1083         product_obj = self.pool.get('product.product')
1084         product_uom_obj = self.pool.get('product.uom')
1085         pack_obj = self.pool.get('product.packaging')
1086         warning = {}
1087         result = {}
1088         warning_msgs = ''
1089         if flag:
1090             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1091                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1092                     packaging=packaging, flag=False, context=context)
1093             warning_msgs = res.get('warning') and res['warning']['message']
1094
1095         products = product_obj.browse(cr, uid, product, context=context)
1096         if not products.packaging:
1097             packaging = result['product_packaging'] = False
1098         elif not packaging and products.packaging and not flag:
1099             packaging = products.packaging[0].id
1100             result['product_packaging'] = packaging
1101
1102         if packaging:
1103             default_uom = products.uom_id and products.uom_id.id
1104             pack = pack_obj.browse(cr, uid, packaging, context=context)
1105             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1106 #            qty = qty - qty % q + q
1107             if qty and (q and not (qty % q) == 0):
1108                 ean = pack.ean or _('(n/a)')
1109                 qty_pack = pack.qty
1110                 type_ul = pack.ul
1111                 if not warning_msgs:
1112                     warn_msg = _("You selected a quantity of %d Units.\n"
1113                                 "But it's not compatible with the selected packaging.\n"
1114                                 "Here is a proposition of quantities according to the packaging:\n"
1115                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1116                                     (qty, ean, qty_pack, type_ul.name)
1117                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1118                 warning = {
1119                        'title': _('Configuration Error !'),
1120                        'message': warning_msgs
1121                 }
1122             result['product_uom_qty'] = qty
1123
1124         return {'value': result, 'warning': warning}
1125
1126     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1127             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1128             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1129         context = context or {}
1130         lang = lang or context.get('lang',False)
1131         if not  partner_id:
1132             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.'))
1133         warning = {}
1134         product_uom_obj = self.pool.get('product.uom')
1135         partner_obj = self.pool.get('res.partner')
1136         product_obj = self.pool.get('product.product')
1137         context = {'lang': lang, 'partner_id': partner_id}
1138         if partner_id:
1139             lang = partner_obj.browse(cr, uid, partner_id).lang
1140         context_partner = {'lang': lang, 'partner_id': partner_id}
1141
1142         if not product:
1143             return {'value': {'th_weight': 0, 'product_packaging': False,
1144                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1145                    'product_uos': []}}
1146         if not date_order:
1147             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1148
1149         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1150         result = res.get('value', {})
1151         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1152         product_obj = product_obj.browse(cr, uid, product, context=context)
1153
1154         uom2 = False
1155         if uom:
1156             uom2 = product_uom_obj.browse(cr, uid, uom)
1157             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1158                 uom = False
1159         if uos:
1160             if product_obj.uos_id:
1161                 uos2 = product_uom_obj.browse(cr, uid, uos)
1162                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1163                     uos = False
1164             else:
1165                 uos = False
1166         if product_obj.description_sale:
1167             result['notes'] = product_obj.description_sale
1168         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1169         if update_tax: #The quantity only have changed
1170             result['delay'] = (product_obj.sale_delay or 0.0)
1171             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1172             result.update({'type': product_obj.procure_method})
1173
1174         if not flag:
1175             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1176         domain = {}
1177         if (not uom) and (not uos):
1178             result['product_uom'] = product_obj.uom_id.id
1179             if product_obj.uos_id:
1180                 result['product_uos'] = product_obj.uos_id.id
1181                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1182                 uos_category_id = product_obj.uos_id.category_id.id
1183             else:
1184                 result['product_uos'] = False
1185                 result['product_uos_qty'] = qty
1186                 uos_category_id = False
1187             result['th_weight'] = qty * product_obj.weight
1188             domain = {'product_uom':
1189                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1190                         'product_uos':
1191                         [('category_id', '=', uos_category_id)]}
1192
1193         elif uos and not uom: # only happens if uom is False
1194             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1195             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1196             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1197         elif uom: # whether uos is set or not
1198             default_uom = product_obj.uom_id and product_obj.uom_id.id
1199             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1200             if product_obj.uos_id:
1201                 result['product_uos'] = product_obj.uos_id.id
1202                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1203             else:
1204                 result['product_uos'] = False
1205                 result['product_uos_qty'] = qty
1206             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1207
1208         if not uom2:
1209             uom2 = product_obj.uom_id
1210         if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1211           and (product_obj.procure_method=='make_to_stock'):
1212             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1213                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1214                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1215                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1216             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1217         # get unit price
1218
1219         if not pricelist:
1220             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1221                     'Please set one before choosing a product.')
1222             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1223         else:
1224             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1225                     product, qty or 1.0, partner_id, {
1226                         'uom': uom or result.get('product_uom'),
1227                         'date': date_order,
1228                         })[pricelist]
1229             if price is False:
1230                 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1231                         "You have to change either the product, the quantity or the pricelist.")
1232
1233                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1234             else:
1235                 result.update({'price_unit': price})
1236         if warning_msgs:
1237             warning = {
1238                        'title': _('Configuration Error !'),
1239                        'message' : warning_msgs
1240                     }
1241         return {'value': result, 'domain': domain, 'warning': warning}
1242
1243     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1244             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1245             lang=False, update_tax=True, date_order=False, context=None):
1246         context = context or {}
1247         lang = lang or ('lang' in context and context['lang'])
1248         res = self.product_id_change(cursor, user, ids, pricelist, product,
1249                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1250                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1251                 date_order=date_order)
1252         if 'product_uom' in res['value']:
1253             del res['value']['product_uom']
1254         if not uom:
1255             res['value']['price_unit'] = 0.0
1256         return res
1257
1258     def unlink(self, cr, uid, ids, context=None):
1259         if context is None:
1260             context = {}
1261         """Allows to delete sales order lines in draft,cancel states"""
1262         for rec in self.browse(cr, uid, ids, context=context):
1263             if rec.state not in ['draft', 'cancel']:
1264                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1265         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1266
1267 sale_order_line()
1268
1269 class sale_config_picking_policy(osv.osv_memory):
1270     _name = 'sale.config.picking_policy'
1271     _inherit = 'res.config'
1272
1273     _columns = {
1274         'name': fields.char('Name', size=64),
1275         'sale_orders': fields.boolean('Based on Sales Orders',),
1276         'deli_orders': fields.boolean('Based on Delivery Orders'),
1277         'task_work': fields.boolean('Based on Tasks\' Work'),
1278         'timesheet': fields.boolean('Based on Timesheet'),
1279         'order_policy': fields.selection([
1280             ('manual', 'Invoice Based on Sales Orders'),
1281             ('picking', 'Invoice Based on Deliveries'),
1282         ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1283         'charge_delivery': fields.boolean('Do you charge the delivery?'),
1284         'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1285     }
1286     _defaults = {
1287         'order_policy': 'manual',
1288         '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,
1289     }
1290
1291     def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1292         res = {}
1293         if sale:
1294             res.update({'order_policy': 'manual'})
1295         elif deli:
1296             res.update({'order_policy': 'picking'})
1297         return {'value':res}
1298
1299     def execute(self, cr, uid, ids, context=None):
1300         ir_values_obj = self.pool.get('ir.values')
1301         data_obj = self.pool.get('ir.model.data')
1302         menu_obj = self.pool.get('ir.ui.menu')
1303         module_obj = self.pool.get('ir.module.module')
1304         module_upgrade_obj = self.pool.get('base.module.upgrade')
1305         module_name = []
1306
1307         group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1308
1309         wizard = self.browse(cr, uid, ids)[0]
1310
1311         if wizard.sale_orders:
1312             menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1313             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1314
1315         if wizard.deli_orders:
1316             menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1317             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1318
1319         if wizard.task_work:
1320             module_name.append('project_timesheet')
1321             module_name.append('project_mrp')
1322             module_name.append('account_analytic_analysis')
1323
1324         if wizard.timesheet:
1325             module_name.append('account_analytic_analysis')
1326
1327         if wizard.charge_delivery:
1328             module_name.append('delivery')
1329
1330         if len(module_name):
1331             module_ids = []
1332             need_install = False
1333             module_ids = []
1334             for module in module_name:
1335                 data_id = module_obj.name_search(cr, uid , module, [], '=')
1336                 module_ids.append(data_id[0][0])
1337
1338             for module in module_obj.browse(cr, uid, module_ids):
1339                 if module.state == 'uninstalled':
1340                     module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1341                     need_install = True
1342                     cr.commit()
1343             if need_install:
1344                 pooler.restart_pool(cr.dbname, update_module=True)[1]
1345
1346         if wizard.time_unit:
1347             prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1348             product_obj = self.pool.get('product.product')
1349             product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1350
1351         ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1352         if wizard.task_work and wizard.time_unit:
1353             company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1354             self.pool.get('res.company').write(cr, uid, [company_id], {
1355                 'project_time_mode_id': wizard.time_unit.id
1356             }, context=context)
1357
1358 sale_config_picking_policy()
1359
1360 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: