1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
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
32 class sale_shop(osv.osv):
34 _description = "Sales Shop"
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),
44 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
49 class sale_order(osv.osv):
51 _description = "Sales Order"
53 def copy(self, cr, uid, id, default=None, context=None):
61 'date_confirm': False,
62 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
64 return super(sale_order, self).copy(cr, uid, id, default, context=context)
66 def _amount_line_tax(self, cr, uid, line, context=None):
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)
72 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
73 cur_obj = self.pool.get('res.currency')
75 for order in self.browse(cr, uid, ids, context=context):
77 'amount_untaxed': 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']
92 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
98 tmp[id] = {'picked': 0.0, 'total': 0.0}
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
104 stock_picking p on (p.id=m.picking_id)
106 procurement_order mp on (mp.move_id=m.id)
108 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
110 for item in cr.dictfetchall():
111 if item['move_state'] == 'cancel':
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
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
123 for order in self.browse(cr, uid, ids, context=context):
125 res[order.id] = 100.0
127 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
130 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
132 for sale in self.browse(cursor, user, ids, context=context):
137 for invoice in sale.invoice_ids:
138 if invoice.state not in ('draft', 'cancel'):
139 tot += invoice.amount_untaxed
141 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
146 def _invoiced(self, cursor, user, ids, name, arg, context=None):
148 for sale in self.browse(cursor, user, ids, context=context):
150 for invoice in sale.invoice_ids:
151 if invoice.state != 'paid':
154 if not sale.invoice_ids:
158 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
167 clause += 'AND inv.state = \'paid\''
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 '
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()
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())
185 return [('id', '=', 0)]
186 return [('id', 'in', [x[0] for x in res])]
188 def _get_order(self, cr, uid, ids, context=None):
190 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
191 result[line.order_id.id] = True
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'),
208 ('cancel', 'Cancelled')
209 ], 'Order State', readonly=True, help="Gives 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."),
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."),
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'),
246 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
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),
251 multi='sums', help="The amount without tax."),
252 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
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),
257 multi='sums', help="The tax amount."),
258 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
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),
263 multi='sums', help="The total amount."),
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)
271 'picking_policy': 'direct',
272 'date_order': fields.date.context_today,
273 'order_policy': 'manual',
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'],
283 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
288 def unlink(self, cr, uid, ids, context=None):
289 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
291 for s in sale_orders:
292 if s['state'] in ['draft', 'cancel']:
293 unlink_ids.append(s['id'])
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.'))
297 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
299 def onchange_shop_id(self, cr, uid, ids, 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
309 def action_cancel_draft(self, cr, uid, ids, *args):
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")
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)
326 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
327 if (not pricelist_id) or (not order_lines):
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.')
333 return {'warning': warning}
335 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
340 val['partner_invoice_id'] = order_id
342 val['partner_shipping_id'] = order_id
343 return {'value': val}
345 def onchange_partner_id(self, cr, uid, ids, part):
347 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
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
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,
364 val['pricelist_id'] = pricelist
365 return {'value': val}
367 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
371 if policy == 'prepaid':
373 elif policy == 'picking':
374 inv_qty = 'procurement'
375 return {'value': {'invoice_quantity': inv_qty}}
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)
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)
393 def button_dummy(self, cr, uid, ids, context=None):
396 def _prepare_invoice(self, cr, uid, order, lines, context=None):
397 """ Builds the dict containing the values for the invoice
398 @param order: order object
399 @param line: list of invoice line IDs that must be attached to the invoice
400 @return: dict that will be used to create the invoice object
402 journal_ids = self.pool.get('account.journal').search(cr, uid,
403 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
406 raise osv.except_osv(_('Error !'),
407 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
409 'name': order.client_order_ref or '',
410 'origin': order.name,
411 'type': 'out_invoice',
412 'reference': order.client_order_ref or order.name,
413 'account_id': order.partner_id.property_account_receivable.id,
414 'partner_id': order.partner_id.id,
415 'journal_id': journal_ids[0],
416 'address_invoice_id': order.partner_invoice_id.id,
417 'address_contact_id': order.partner_order_id.id,
418 'invoice_line': [(6, 0, lines)],
419 'currency_id': order.pricelist_id.currency_id.id,
420 'comment': order.note,
421 'payment_term': order.payment_term and order.payment_term.id or False,
422 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
423 'date_invoice': context.get('date_invoice', False),
424 'company_id': order.company_id.id,
425 'user_id': order.user_id and order.user_id.id or False
428 def _make_invoice(self, cr, uid, order, lines, context=None):
429 inv_obj = self.pool.get('account.invoice')
430 obj_invoice_line = self.pool.get('account.invoice.line')
433 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
434 from_line_invoice_ids = []
435 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
436 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
437 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
438 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
439 for preinv in order.invoice_ids:
440 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
441 for preline in preinv.invoice_line:
442 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
443 lines.append(inv_line_id)
444 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
445 inv_id = inv_obj.create(cr, uid, inv, context=context)
446 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
447 if data.get('value', False):
448 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
449 inv_obj.button_compute(cr, uid, [inv_id])
452 def manual_invoice(self, cr, uid, ids, context=None):
453 mod_obj = self.pool.get('ir.model.data')
454 wf_service = netsvc.LocalService("workflow")
458 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
459 inv_ids.add(record.id)
460 # inv_ids would have old invoices if any
462 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
463 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
464 inv_ids1.add(record.id)
465 inv_ids = list(inv_ids1.difference(inv_ids))
467 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
468 res_id = res and res[1] or False,
471 'name': _('Customer Invoices'),
475 'res_model': 'account.invoice',
476 'context': "{'type':'out_invoice'}",
477 'type': 'ir.actions.act_window',
480 'res_id': inv_ids and inv_ids[0] or False,
483 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
487 picking_obj = self.pool.get('stock.picking')
488 invoice = self.pool.get('account.invoice')
489 obj_sale_order_line = self.pool.get('sale.order.line')
490 partner_currency = {}
493 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
494 # last day of the last month as invoice date
496 context['date_inv'] = date_inv
497 for o in self.browse(cr, uid, ids, context=context):
498 currency_id = o.pricelist_id.currency_id.id
499 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
500 raise osv.except_osv(
502 _('You cannot group sales having different currencies for the same partner.'))
504 partner_currency[o.partner_id.id] = currency_id
506 for line in o.order_line:
509 elif (line.state in states):
510 lines.append(line.id)
511 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
513 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
515 for o in self.browse(cr, uid, ids, context=context):
516 for i in o.invoice_ids:
517 if i.state == 'draft':
519 for val in invoices.values():
521 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 += o.name + '|'
525 self.write(cr, uid, [o.id], {'state': 'progress'})
526 if o.order_policy == 'picking':
527 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
528 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
529 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
531 for order, il in val:
532 res = self._make_invoice(cr, uid, order, il, context=context)
533 invoice_ids.append(res)
534 self.write(cr, uid, [order.id], {'state': 'progress'})
535 if order.order_policy == 'picking':
536 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
537 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
540 def action_invoice_cancel(self, cr, uid, ids, context=None):
543 for sale in self.browse(cr, uid, ids, context=context):
544 for line in sale.order_line:
546 # Check if the line is invoiced (has asociated invoice
547 # lines from non-cancelled invoices).
550 for iline in line.invoice_lines:
551 if iline.invoice_id and iline.invoice_id.state != 'cancel':
554 # Update the line (only when needed)
555 if line.invoiced != invoiced:
556 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
557 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
560 def action_invoice_end(self, cr, uid, ids, context=None):
561 for order in self.browse(cr, uid, ids, context=context):
563 # Update the sale order lines state (and invoiced flag).
565 for line in order.order_line:
568 # Check if the line is invoiced (has asociated invoice
569 # lines from non-cancelled invoices).
572 for iline in line.invoice_lines:
573 if iline.invoice_id and iline.invoice_id.state != 'cancel':
576 if line.invoiced != invoiced:
577 vals['invoiced'] = invoiced
578 # If the line was in exception state, now it gets confirmed.
579 if line.state == 'exception':
580 vals['state'] = 'confirmed'
581 # Update the line (only when needed).
583 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
585 # Update the sales order state.
587 if order.state == 'invoice_except':
588 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
591 def action_cancel(self, cr, uid, ids, context=None):
592 wf_service = netsvc.LocalService("workflow")
595 sale_order_line_obj = self.pool.get('sale.order.line')
596 proc_obj = self.pool.get('procurement.order')
597 for sale in self.browse(cr, uid, ids, context=context):
598 for pick in sale.picking_ids:
599 if pick.state not in ('draft', 'cancel'):
600 raise osv.except_osv(
601 _('Could not cancel sales order !'),
602 _('You must first cancel all picking attached to this sales order.'))
603 if pick.state == 'cancel':
604 for mov in pick.move_lines:
605 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
607 for proc in proc_ids:
608 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
609 for r in self.read(cr, uid, ids, ['picking_ids']):
610 for pick in r['picking_ids']:
611 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
612 for inv in sale.invoice_ids:
613 if inv.state not in ('draft', 'cancel'):
614 raise osv.except_osv(
615 _('Could not cancel this sales order !'),
616 _('You must first cancel all invoices attached to this sales order.'))
617 for r in self.read(cr, uid, ids, ['invoice_ids']):
618 for inv in r['invoice_ids']:
619 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
620 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
622 message = _("The sales order '%s' has been cancelled.") % (sale.name,)
623 self.log(cr, uid, sale.id, message)
624 self.write(cr, uid, ids, {'state': 'cancel'})
627 def action_wait(self, cr, uid, ids, context=None):
628 for o in self.browse(cr, uid, ids):
630 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
631 if (o.order_policy == 'manual'):
632 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
634 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
635 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
636 message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
637 self.log(cr, uid, o.id, message)
640 def procurement_lines_get(self, cr, uid, ids, *args):
642 for order in self.browse(cr, uid, ids, context={}):
643 for line in order.order_line:
644 if line.procurement_id:
645 res.append(line.procurement_id.id)
648 # if mode == 'finished':
649 # returns True if all lines are done, False otherwise
650 # if mode == 'canceled':
651 # returns True if there is at least one canceled line, False otherwise
652 def test_state(self, cr, uid, ids, mode, *args):
653 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
658 write_cancel_ids = []
659 for order in self.browse(cr, uid, ids, context={}):
660 for line in order.order_line:
661 if (not line.procurement_id) or (line.procurement_id.state=='done'):
662 if line.state != 'done':
663 write_done_ids.append(line.id)
666 if line.procurement_id:
667 if (line.procurement_id.state == 'cancel'):
669 if line.state != 'exception':
670 write_cancel_ids.append(line.id)
674 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
676 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
678 if mode == 'finished':
680 elif mode == 'canceled':
686 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
689 'origin': order.name,
690 'date_planned': date_planned,
691 'product_id': line.product_id.id,
692 'product_qty': line.product_uom_qty,
693 'product_uom': line.product_uom.id,
694 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
695 or line.product_uom_qty,
696 'product_uos': (line.product_uos and line.product_uos.id)\
697 or line.product_uom.id,
698 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
699 'procure_method': line.type,
701 'company_id': order.company_id.id,
705 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
706 location_id = order.shop_id.warehouse_id.lot_stock_id.id
707 output_id = order.shop_id.warehouse_id.lot_output_id.id
709 'name': line.name[:250],
710 'picking_id': picking_id,
711 'product_id': line.product_id.id,
712 'date': date_planned,
713 'date_expected': date_planned,
714 'product_qty': line.product_uom_qty,
715 'product_uom': line.product_uom.id,
716 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
717 'product_uos': (line.product_uos and line.product_uos.id)\
718 or line.product_uom.id,
719 'product_packaging': line.product_packaging.id,
720 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
721 'location_id': location_id,
722 'location_dest_id': output_id,
723 'sale_line_id': line.id,
724 'tracking_id': False,
728 'company_id': order.company_id.id,
729 'price_unit': line.product_id.standard_price or 0.0
732 def _prepare_order_picking(self, cr, uid, order, context=None):
733 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
736 'origin': order.name,
737 'date': order.date_order,
740 'move_type': order.picking_policy,
742 'address_id': order.partner_shipping_id.id,
744 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
745 'company_id': order.company_id.id,
748 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
749 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
751 Define ship_recreate for process after shipping exception
752 param order: sale order to which the order lines belong
753 param line: sale order line records to procure
754 param move_id: the ID of stock move
755 param proc_id: the ID of procurement
757 move_obj = self.pool.get('stock.move')
758 if order.state == 'shipping_except':
759 for pick in order.picking_ids:
760 for move in pick.move_lines:
761 if move.state == 'cancel':
762 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
764 for mov in move_obj.browse(cr, uid, mov_ids):
765 # 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?
766 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
767 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
770 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
771 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
772 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
775 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
776 """Create the required procurements to supply sale order lines, also connecting
777 the procurements to appropriate stock moves in order to bring the goods to the
778 sale order's requested location.
780 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
781 a standard outgoing picking will be created to wrap the stock moves, as returned
782 by :meth:`~._prepare_order_picking`.
784 Modules that wish to customize the procurements or partition the stock moves over
785 multiple stock pickings may override this method and call ``super()`` with
786 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
788 :param browse_record order: sale order to which the order lines belong
789 :param list(browse_record) order_lines: sale order line records to procure
790 :param int picking_id: optional ID of a stock picking to which the created stock moves
791 will be added. A new picking will be created if ommitted.
794 move_obj = self.pool.get('stock.move')
795 picking_obj = self.pool.get('stock.picking')
796 procurement_obj = self.pool.get('procurement.order')
799 for line in order_lines:
800 if line.state == 'done':
803 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
806 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
808 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
809 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
811 # a service has no stock move
814 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
815 proc_ids.append(proc_id)
816 line.write({'procurement_id': proc_id})
817 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
819 wf_service = netsvc.LocalService("workflow")
821 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
823 for proc_id in proc_ids:
824 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
827 if order.state == 'shipping_except':
828 val['state'] = 'progress'
829 val['shipped'] = False
831 if (order.order_policy == 'manual'):
832 for line in order.order_line:
833 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
834 val['state'] = 'manual'
839 def action_ship_create(self, cr, uid, ids, context=None):
840 for order in self.browse(cr, uid, ids, context=context):
841 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
844 def action_ship_end(self, cr, uid, ids, context=None):
845 for order in self.browse(cr, uid, ids, context=context):
846 val = {'shipped': True}
847 if order.state == 'shipping_except':
848 val['state'] = 'progress'
849 if (order.order_policy == 'manual'):
850 for line in order.order_line:
851 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
852 val['state'] = 'manual'
854 for line in order.order_line:
856 if line.state == 'exception':
857 towrite.append(line.id)
859 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
860 self.write(cr, uid, [order.id], val)
863 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
864 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
866 part = inv['partner_id'] and inv['partner_id'][0]
867 pr = inv['amount_untaxed'] or 0.0
868 partnertype = 'customer'
871 'name': 'Order: '+name,
873 'description': 'Order '+str(inv['id']),
876 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
878 'partner_type': partnertype,
880 'planned_revenue': pr,
884 self.pool.get('res.partner.event').create(cr, uid, event)
886 def has_stockable_products(self, cr, uid, ids, *args):
887 for order in self.browse(cr, uid, ids):
888 for order_line in order.order_line:
889 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
894 # TODO add a field price_unit_uos
895 # - update it on change product and unit price
896 # - use it in report if there is a uos
897 class sale_order_line(osv.osv):
899 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
900 tax_obj = self.pool.get('account.tax')
901 cur_obj = self.pool.get('res.currency')
905 for line in self.browse(cr, uid, ids, context=context):
906 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
907 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)
908 cur = line.order_id.pricelist_id.currency_id
909 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
912 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
914 for line in self.browse(cr, uid, ids, context=context):
916 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
921 def _get_uom_id(self, cr, uid, *args):
923 proxy = self.pool.get('ir.model.data')
924 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
926 except Exception, ex:
929 _name = 'sale.order.line'
930 _description = 'Sales Order Line'
932 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
933 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
934 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
935 '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)]}),
936 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
937 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
938 'invoiced': fields.boolean('Invoiced', readonly=True),
939 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
940 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
941 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
942 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
943 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
944 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."),
945 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
946 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
947 'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
948 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
949 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
950 'product_uos': fields.many2one('product.uom', 'Product UoS'),
951 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
952 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
953 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
954 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
955 'notes': fields.text('Notes'),
956 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
957 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
958 help='* The \'Draft\' state is set when the related sales order in draft state. \
959 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
960 \n* The \'Exception\' state is set when the related sales order is set as exception. \
961 \n* The \'Done\' state is set when the sales order line has been picked. \
962 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
963 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
964 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
965 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
967 _order = 'sequence, id'
969 'product_uom' : _get_uom_id,
972 'product_uom_qty': 1,
973 'product_uos_qty': 1,
977 'type': 'make_to_stock',
978 'product_packaging': False,
982 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
983 """ Builds the invoice line dict from a sale order line
984 @param line: sale order line object
985 @param account_id: the id of the account to force eventually (the method is used for picking return including service)
986 @return: dict that will be used to create the invoice line
989 def _get_line_qty(line):
990 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
992 return line.product_uos_qty or 0.0
993 return line.product_uom_qty
995 return self.pool.get('procurement.order').quantity_get(cr, uid,
996 line.procurement_id.id, context=context)
998 def _get_line_uom(line):
999 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1000 if line.product_uos:
1001 return line.product_uos.id
1002 return line.product_uom.id
1004 return self.pool.get('procurement.order').uom_get(cr, uid,
1005 line.procurement_id.id, context=context)
1007 if not line.invoiced:
1010 account_id = line.product_id.product_tmpl_id.property_account_income.id
1012 account_id = line.product_id.categ_id.property_account_income_categ.id
1014 raise osv.except_osv(_('Error !'),
1015 _('There is no income account defined for this product: "%s" (id:%d)') % \
1016 (line.product_id.name, line.product_id.id,))
1018 prop = self.pool.get('ir.property').get(cr, uid,
1019 'property_account_income_categ', 'product.category',
1021 account_id = prop and prop.id or False
1022 uosqty = _get_line_qty(line)
1023 uos_id = _get_line_uom(line)
1026 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1027 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1028 fpos = line.order_id.fiscal_position or False
1029 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1031 raise osv.except_osv(_('Error !'),
1032 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1035 'origin': line.order_id.name,
1036 'account_id': account_id,
1039 'discount': line.discount,
1041 'product_id': line.product_id.id or False,
1042 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1044 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1049 def invoice_line_create(self, cr, uid, ids, context=None):
1055 for line in self.browse(cr, uid, ids, context=context):
1056 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1058 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1059 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1060 self.write(cr, uid, [line.id], {'invoiced': True})
1061 sales.add(line.order_id.id)
1062 create_ids.append(inv_id)
1063 # Trigger workflow events
1064 wf_service = netsvc.LocalService("workflow")
1065 for sale_id in sales:
1066 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1069 def button_cancel(self, cr, uid, ids, context=None):
1070 for line in self.browse(cr, uid, ids, context=context):
1072 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1073 for move_line in line.move_ids:
1074 if move_line.state != 'cancel':
1075 raise osv.except_osv(
1076 _('Could not cancel sales order line!'),
1077 _('You must first cancel stock moves attached to this sales order line.'))
1078 return self.write(cr, uid, ids, {'state': 'cancel'})
1080 def button_confirm(self, cr, uid, ids, context=None):
1081 return self.write(cr, uid, ids, {'state': 'confirmed'})
1083 def button_done(self, cr, uid, ids, context=None):
1084 wf_service = netsvc.LocalService("workflow")
1085 res = self.write(cr, uid, ids, {'state': 'done'})
1086 for line in self.browse(cr, uid, ids, context=context):
1087 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1090 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1091 product_obj = self.pool.get('product.product')
1093 return {'value': {'product_uom': product_uos,
1094 'product_uom_qty': product_uos_qty}, 'domain': {}}
1096 product = product_obj.browse(cr, uid, product_id)
1098 'product_uom': product.uom_id.id,
1100 # FIXME must depend on uos/uom of the product and not only of the coeff.
1103 'product_uom_qty': product_uos_qty / product.uos_coeff,
1104 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1106 except ZeroDivisionError:
1108 return {'value': value}
1110 def copy_data(self, cr, uid, id, default=None, context=None):
1113 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1114 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1116 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1117 partner_id=False, packaging=False, flag=False, context=None):
1119 return {'value': {'product_packaging': False}}
1120 product_obj = self.pool.get('product.product')
1121 product_uom_obj = self.pool.get('product.uom')
1122 pack_obj = self.pool.get('product.packaging')
1127 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1128 product=product, qty=qty, uom=uom, partner_id=partner_id,
1129 packaging=packaging, flag=False, context=context)
1130 warning_msgs = res.get('warning') and res['warning']['message']
1132 products = product_obj.browse(cr, uid, product, context=context)
1133 if not products.packaging:
1134 packaging = result['product_packaging'] = False
1135 elif not packaging and products.packaging and not flag:
1136 packaging = products.packaging[0].id
1137 result['product_packaging'] = packaging
1140 default_uom = products.uom_id and products.uom_id.id
1141 pack = pack_obj.browse(cr, uid, packaging, context=context)
1142 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1143 # qty = qty - qty % q + q
1144 if qty and (q and not (qty % q) == 0):
1145 ean = pack.ean or _('(n/a)')
1148 if not warning_msgs:
1149 warn_msg = _("You selected a quantity of %d Units.\n"
1150 "But it's not compatible with the selected packaging.\n"
1151 "Here is a proposition of quantities according to the packaging:\n"
1152 "EAN: %s Quantity: %s Type of ul: %s") % \
1153 (qty, ean, qty_pack, type_ul.name)
1154 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1156 'title': _('Configuration Error !'),
1157 'message': warning_msgs
1159 result['product_uom_qty'] = qty
1161 return {'value': result, 'warning': warning}
1163 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1164 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1165 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1166 context = context or {}
1167 lang = lang or context.get('lang',False)
1169 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.'))
1171 product_uom_obj = self.pool.get('product.uom')
1172 partner_obj = self.pool.get('res.partner')
1173 product_obj = self.pool.get('product.product')
1174 context = {'lang': lang, 'partner_id': partner_id}
1176 lang = partner_obj.browse(cr, uid, partner_id).lang
1177 context_partner = {'lang': lang, 'partner_id': partner_id}
1180 return {'value': {'th_weight': 0, 'product_packaging': False,
1181 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1184 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1186 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1187 result = res.get('value', {})
1188 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1189 product_obj = product_obj.browse(cr, uid, product, context=context)
1193 uom2 = product_uom_obj.browse(cr, uid, uom)
1194 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1197 if product_obj.uos_id:
1198 uos2 = product_uom_obj.browse(cr, uid, uos)
1199 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1203 if product_obj.description_sale:
1204 result['notes'] = product_obj.description_sale
1205 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1206 if update_tax: #The quantity only have changed
1207 result['delay'] = (product_obj.sale_delay or 0.0)
1208 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1209 result.update({'type': product_obj.procure_method})
1212 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1214 if (not uom) and (not uos):
1215 result['product_uom'] = product_obj.uom_id.id
1216 if product_obj.uos_id:
1217 result['product_uos'] = product_obj.uos_id.id
1218 result['product_uos_qty'] = qty * product_obj.uos_coeff
1219 uos_category_id = product_obj.uos_id.category_id.id
1221 result['product_uos'] = False
1222 result['product_uos_qty'] = qty
1223 uos_category_id = False
1224 result['th_weight'] = qty * product_obj.weight
1225 domain = {'product_uom':
1226 [('category_id', '=', product_obj.uom_id.category_id.id)],
1228 [('category_id', '=', uos_category_id)]}
1230 elif uos and not uom: # only happens if uom is False
1231 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1232 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1233 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1234 elif uom: # whether uos is set or not
1235 default_uom = product_obj.uom_id and product_obj.uom_id.id
1236 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1237 if product_obj.uos_id:
1238 result['product_uos'] = product_obj.uos_id.id
1239 result['product_uos_qty'] = qty * product_obj.uos_coeff
1241 result['product_uos'] = False
1242 result['product_uos_qty'] = qty
1243 result['th_weight'] = q * product_obj.weight # Round the quantity up
1246 uom2 = product_obj.uom_id
1247 if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1248 and (product_obj.procure_method=='make_to_stock'):
1249 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1250 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1251 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1252 max(0,product_obj.qty_available), product_obj.uom_id.name)
1253 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1257 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1258 'Please set one before choosing a product.')
1259 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1261 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1262 product, qty or 1.0, partner_id, {
1263 'uom': uom or result.get('product_uom'),
1267 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1268 "You have to change either the product, the quantity or the pricelist.")
1270 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1272 result.update({'price_unit': price})
1275 'title': _('Configuration Error !'),
1276 'message' : warning_msgs
1278 return {'value': result, 'domain': domain, 'warning': warning}
1280 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1281 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1282 lang=False, update_tax=True, date_order=False, context=None):
1283 context = context or {}
1284 lang = lang or ('lang' in context and context['lang'])
1285 res = self.product_id_change(cursor, user, ids, pricelist, product,
1286 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1287 partner_id=partner_id, lang=lang, update_tax=update_tax,
1288 date_order=date_order, context=context)
1289 if 'product_uom' in res['value']:
1290 del res['value']['product_uom']
1292 res['value']['price_unit'] = 0.0
1295 def unlink(self, cr, uid, ids, context=None):
1298 """Allows to delete sales order lines in draft,cancel states"""
1299 for rec in self.browse(cr, uid, ids, context=context):
1300 if rec.state not in ['draft', 'cancel']:
1301 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1302 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1306 class sale_config_picking_policy(osv.osv_memory):
1307 _name = 'sale.config.picking_policy'
1308 _inherit = 'res.config'
1311 'name': fields.char('Name', size=64),
1312 'sale_orders': fields.boolean('Based on Sales Orders',),
1313 'deli_orders': fields.boolean('Based on Delivery Orders'),
1314 'task_work': fields.boolean('Based on Tasks\' Work'),
1315 'timesheet': fields.boolean('Based on Timesheet'),
1316 'order_policy': fields.selection([
1317 ('manual', 'Invoice Based on Sales Orders'),
1318 ('picking', 'Invoice Based on Deliveries'),
1319 ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1320 'charge_delivery': fields.boolean('Do you charge the delivery?'),
1321 'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1324 'order_policy': 'manual',
1325 '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,
1328 def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1331 res.update({'order_policy': 'manual'})
1333 res.update({'order_policy': 'picking'})
1334 return {'value':res}
1336 def execute(self, cr, uid, ids, context=None):
1337 ir_values_obj = self.pool.get('ir.values')
1338 data_obj = self.pool.get('ir.model.data')
1339 menu_obj = self.pool.get('ir.ui.menu')
1340 module_obj = self.pool.get('ir.module.module')
1341 module_upgrade_obj = self.pool.get('base.module.upgrade')
1344 group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1346 wizard = self.browse(cr, uid, ids)[0]
1348 if wizard.sale_orders:
1349 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1350 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1352 if wizard.deli_orders:
1353 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1354 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1356 if wizard.task_work:
1357 module_name.append('project_timesheet')
1358 module_name.append('project_mrp')
1359 module_name.append('account_analytic_analysis')
1361 if wizard.timesheet:
1362 module_name.append('account_analytic_analysis')
1364 if wizard.charge_delivery:
1365 module_name.append('delivery')
1367 if len(module_name):
1369 need_install = False
1371 for module in module_name:
1372 data_id = module_obj.name_search(cr, uid , module, [], '=')
1373 module_ids.append(data_id[0][0])
1375 for module in module_obj.browse(cr, uid, module_ids):
1376 if module.state == 'uninstalled':
1377 module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1381 pooler.restart_pool(cr.dbname, update_module=True)[1]
1383 if wizard.time_unit:
1384 prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1385 product_obj = self.pool.get('product.product')
1386 product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1388 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1389 if wizard.task_work and wizard.time_unit:
1390 company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1391 self.pool.get('res.company').write(cr, uid, [company_id], {
1392 'project_time_mode_id': wizard.time_unit.id
1395 sale_config_picking_policy()
1397 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: