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, float_compare
29 import decimal_precision as dp
31 #from openerp.addons.base.res import res_config_sale as sale_config
33 class sale_shop(osv.osv):
35 _description = "Sales Shop"
37 'name': fields.char('Shop Name', size=64, required=True),
38 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
39 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
40 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
41 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
42 'company_id': fields.many2one('res.company', 'Company', required=False),
45 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
50 class sale_order(osv.osv):
52 _description = "Sales Order"
54 def copy(self, cr, uid, id, default=None, context=None):
62 'date_confirm': False,
63 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
65 return super(sale_order, self).copy(cr, uid, id, default, context=context)
67 def _amount_line_tax(self, cr, uid, line, context=None):
69 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']:
70 val += c.get('amount', 0.0)
73 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
74 cur_obj = self.pool.get('res.currency')
76 for order in self.browse(cr, uid, ids, context=context):
78 'amount_untaxed': 0.0,
83 cur = order.pricelist_id.currency_id
84 for line in order.order_line:
85 val1 += line.price_subtotal
86 val += self._amount_line_tax(cr, uid, line, context=context)
87 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
88 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
89 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
93 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
99 tmp[id] = {'picked': 0.0, 'total': 0.0}
101 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
105 stock_picking p on (p.id=m.picking_id)
107 procurement_order mp on (mp.move_id=m.id)
109 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
111 for item in cr.dictfetchall():
112 if item['move_state'] == 'cancel':
115 if item['picking_type'] == 'in':#this is a returned picking
116 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
117 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
118 tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
120 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
121 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
122 tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
124 for order in self.browse(cr, uid, ids, context=context):
126 res[order.id] = 100.0
128 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
131 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
133 for sale in self.browse(cursor, user, ids, context=context):
138 for invoice in sale.invoice_ids:
139 if invoice.state not in ('draft', 'cancel'):
140 tot += invoice.amount_untaxed
142 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
147 def _invoiced(self, cursor, user, ids, name, arg, context=None):
149 for sale in self.browse(cursor, user, ids, context=context):
151 invoice_existence = False
152 for invoice in sale.invoice_ids:
153 if invoice.state!='cancel':
154 invoice_existence = True
155 if invoice.state != 'paid':
158 if not invoice_existence:
162 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
171 clause += 'AND inv.state = \'paid\''
173 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
174 sale_clause = ', sale_order AS sale '
177 cursor.execute('SELECT rel.order_id ' \
178 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
179 'WHERE rel.invoice_id = inv.id ' + clause)
180 res = cursor.fetchall()
182 cursor.execute('SELECT sale.id ' \
183 'FROM sale_order AS sale ' \
184 'WHERE sale.id NOT IN ' \
185 '(SELECT rel.order_id ' \
186 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
187 res.extend(cursor.fetchall())
189 return [('id', '=', 0)]
190 return [('id', 'in', [x[0] for x in res])]
192 def _get_order(self, cr, uid, ids, context=None):
194 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
195 result[line.order_id.id] = True
199 'name': fields.char('Order Reference', size=64, required=True,
200 readonly=True, states={'draft': [('readonly', False)]}, select=True),
201 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
202 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
203 'client_order_ref': fields.char('Customer Reference', size=64),
204 'state': fields.selection([
205 ('draft', 'Quotation'),
206 ('waiting_date', 'Waiting Schedule'),
207 ('manual', 'To Invoice'),
208 ('progress', 'In Progress'),
209 ('shipping_except', 'Shipping Exception'),
210 ('invoice_except', 'Invoice Exception'),
212 ('cancel', 'Cancelled')
213 ], '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),
214 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
215 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
216 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
217 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
218 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
219 '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."),
220 '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."),
221 '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."),
223 '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."),
224 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
225 '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?"""),
226 'order_policy': fields.selection([
227 ('prepaid', 'Pay before delivery'),
228 ('manual', 'Deliver & invoice on demand'),
229 ('picking', 'Invoice based on deliveries'),
230 ('postpaid', 'Invoice on order after delivery'),
231 ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
232 help="""The Invoice Policy is used to synchronise invoice and delivery operations.
233 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
234 - 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.
235 - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
236 - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
237 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
238 '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."),
240 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
241 '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)."),
242 '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."),
243 '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."),
244 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
245 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
246 'invoiced': fields.function(_invoiced, string='Paid',
247 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
248 'note': fields.text('Notes'),
250 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
252 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
255 multi='sums', help="The amount without tax."),
256 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
258 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
259 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
261 multi='sums', help="The tax amount."),
262 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
264 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
265 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
267 multi='sums', help="The total amount."),
269 '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)]}),
270 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
271 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
272 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True),
273 'tax_id': fields.many2one('account.tax', 'Tax'),
276 'picking_policy': 'direct',
277 'date_order': fields.date.context_today,
278 'order_policy': 'manual',
280 'user_id': lambda obj, cr, uid, context: uid,
281 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
282 'invoice_quantity': 'order',
283 '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'],
284 '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'],
285 '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'],
288 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
293 def unlink(self, cr, uid, ids, context=None):
294 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
296 for s in sale_orders:
297 if s['state'] in ['draft', 'cancel']:
298 unlink_ids.append(s['id'])
300 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.'))
302 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
304 def onchange_shop_id(self, cr, uid, ids, shop_id):
307 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
308 v['project_id'] = shop.project_id.id
309 # Que faire si le client a une pricelist a lui ?
310 if shop.pricelist_id.id:
311 v['pricelist_id'] = shop.pricelist_id.id
314 def onchange_taxes(self, cr, uid, ids, tax_id, context=None):
315 sol_obj = self.pool.get('sale.order.line')
317 for order in self.browse(cr, uid, ids, context=context):
318 for line in order.order_line:
319 sol_obj.write(cr, uid, line.id, {'tax_id': [(4,tax_id)]})
322 def action_cancel_draft(self, cr, uid, ids, *args):
325 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
326 line_ids = map(lambda x: x[0], cr.fetchall())
327 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
328 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
329 wf_service = netsvc.LocalService("workflow")
331 # Deleting the existing instance of workflow for SO
332 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
333 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
334 for (id,name) in self.name_get(cr, uid, ids):
335 message = _("The sales order '%s' has been set in draft state.") %(name,)
336 self.log(cr, uid, id, message)
339 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
340 if (not pricelist_id) or (not order_lines):
343 'title': _('Pricelist Warning!'),
344 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
346 return {'warning': warning}
348 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
353 val['partner_invoice_id'] = order_id
355 val['partner_shipping_id'] = order_id
356 return {'value': val}
358 def onchange_partner_id(self, cr, uid, ids, part):
360 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
362 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
363 part = self.pool.get('res.partner').browse(cr, uid, part)
364 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
365 payment_term = part.property_payment_term and part.property_payment_term.id or False
366 fiscal_position = part.property_account_position and part.property_account_position.id or False
367 dedicated_salesman = part.user_id and part.user_id.id or uid
369 'partner_invoice_id': addr['invoice'],
370 'partner_order_id': addr['contact'],
371 'partner_shipping_id': addr['delivery'],
372 'payment_term': payment_term,
373 'fiscal_position': fiscal_position,
374 'user_id': dedicated_salesman,
377 val['pricelist_id'] = pricelist
378 return {'value': val}
380 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
384 if policy == 'prepaid':
386 elif policy == 'picking':
387 inv_qty = 'procurement'
388 return {'value': {'invoice_quantity': inv_qty}}
390 def write(self, cr, uid, ids, vals, context=None):
391 if vals.get('order_policy', False):
392 if vals['order_policy'] == 'prepaid':
393 vals.update({'invoice_quantity': 'order'})
394 elif vals['order_policy'] == 'picking':
395 vals.update({'invoice_quantity': 'procurement'})
396 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
398 def create(self, cr, uid, vals, context=None):
399 if vals.get('order_policy', False):
400 if vals['order_policy'] == 'prepaid':
401 vals.update({'invoice_quantity': 'order'})
402 if vals['order_policy'] == 'picking':
403 vals.update({'invoice_quantity': 'procurement'})
404 return super(sale_order, self).create(cr, uid, vals, context=context)
406 def button_dummy(self, cr, uid, ids, context=None):
409 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
410 # can be removed after 6.1.
411 def _inv_get(self, cr, uid, order, context=None):
414 def _prepare_invoice(self, cr, uid, order, lines, context=None):
415 """Prepare the dict of values to create the new invoice for a
416 sale order. This method may be overridden to implement custom
417 invoice generation (making sure to call super() to establish
418 a clean extension chain).
420 :param browse_record order: sale.order record to invoice
421 :param list(int) line: list of invoice line IDs that must be
422 attached to the invoice
423 :return: dict of value to create() the invoice
427 journal_ids = self.pool.get('account.journal').search(cr, uid,
428 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
431 raise osv.except_osv(_('Error !'),
432 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
435 'name': order.client_order_ref or '',
436 'origin': order.name,
437 'type': 'out_invoice',
438 'reference': order.client_order_ref or order.name,
439 'account_id': order.partner_id.property_account_receivable.id,
440 'partner_id': order.partner_id.id,
441 'journal_id': journal_ids[0],
442 'address_invoice_id': order.partner_invoice_id.id,
443 'address_contact_id': order.partner_order_id.id,
444 'invoice_line': [(6, 0, lines)],
445 'currency_id': order.pricelist_id.currency_id.id,
446 'comment': order.note,
447 'payment_term': order.payment_term and order.payment_term.id or False,
448 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
449 'date_invoice': context.get('date_invoice', False),
450 'company_id': order.company_id.id,
451 'user_id': order.user_id and order.user_id.id or False
454 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
455 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
459 def _make_invoice(self, cr, uid, order, lines, context=None):
460 inv_obj = self.pool.get('account.invoice')
461 obj_invoice_line = self.pool.get('account.invoice.line')
464 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
465 from_line_invoice_ids = []
466 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
467 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
468 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
469 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
470 for preinv in order.invoice_ids:
471 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
472 for preline in preinv.invoice_line:
473 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
474 lines.append(inv_line_id)
475 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
476 inv_id = inv_obj.create(cr, uid, inv, context=context)
477 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
478 if data.get('value', False):
479 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
480 inv_obj.button_compute(cr, uid, [inv_id])
483 def manual_invoice(self, cr, uid, ids, context=None):
484 mod_obj = self.pool.get('ir.model.data')
485 wf_service = netsvc.LocalService("workflow")
489 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
490 inv_ids.add(record.id)
491 # inv_ids would have old invoices if any
493 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
494 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
495 inv_ids1.add(record.id)
496 inv_ids = list(inv_ids1.difference(inv_ids))
498 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
499 res_id = res and res[1] or False,
502 'name': _('Customer Invoices'),
506 'res_model': 'account.invoice',
507 'context': "{'type':'out_invoice'}",
508 'type': 'ir.actions.act_window',
511 'res_id': inv_ids and inv_ids[0] or False,
514 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
518 picking_obj = self.pool.get('stock.picking')
519 invoice = self.pool.get('account.invoice')
520 obj_sale_order_line = self.pool.get('sale.order.line')
521 partner_currency = {}
524 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
525 # last day of the last month as invoice date
527 context['date_inv'] = date_inv
528 for o in self.browse(cr, uid, ids, context=context):
529 currency_id = o.pricelist_id.currency_id.id
530 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
531 raise osv.except_osv(
533 _('You cannot group sales having different currencies for the same partner.'))
535 partner_currency[o.partner_id.id] = currency_id
537 for line in o.order_line:
540 elif (line.state in states):
541 lines.append(line.id)
542 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
544 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
546 for o in self.browse(cr, uid, ids, context=context):
547 for i in o.invoice_ids:
548 if i.state == 'draft':
550 for val in invoices.values():
552 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
555 invoice_ref += o.name + '|'
556 self.write(cr, uid, [o.id], {'state': 'progress'})
557 if o.order_policy == 'picking':
558 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
559 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
560 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
562 for order, il in val:
563 res = self._make_invoice(cr, uid, order, il, context=context)
564 invoice_ids.append(res)
565 self.write(cr, uid, [order.id], {'state': 'progress'})
566 if order.order_policy == 'picking':
567 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
568 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
571 def action_invoice_cancel(self, cr, uid, ids, context=None):
574 for sale in self.browse(cr, uid, ids, context=context):
575 for line in sale.order_line:
577 # Check if the line is invoiced (has asociated invoice
578 # lines from non-cancelled invoices).
581 for iline in line.invoice_lines:
582 if iline.invoice_id and iline.invoice_id.state != 'cancel':
585 # Update the line (only when needed)
586 if line.invoiced != invoiced:
587 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
588 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
591 def action_invoice_end(self, cr, uid, ids, context=None):
592 for order in self.browse(cr, uid, ids, context=context):
594 # Update the sale order lines state (and invoiced flag).
596 for line in order.order_line:
599 # Check if the line is invoiced (has asociated invoice
600 # lines from non-cancelled invoices).
603 for iline in line.invoice_lines:
604 if iline.invoice_id and iline.invoice_id.state != 'cancel':
607 if line.invoiced != invoiced:
608 vals['invoiced'] = invoiced
609 # If the line was in exception state, now it gets confirmed.
610 if line.state == 'exception':
611 vals['state'] = 'confirmed'
612 # Update the line (only when needed).
614 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
616 # Update the sales order state.
618 if order.state == 'invoice_except':
619 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
622 def action_cancel(self, cr, uid, ids, context=None):
623 wf_service = netsvc.LocalService("workflow")
626 sale_order_line_obj = self.pool.get('sale.order.line')
627 proc_obj = self.pool.get('procurement.order')
628 for sale in self.browse(cr, uid, ids, context=context):
629 for pick in sale.picking_ids:
630 if pick.state not in ('draft', 'cancel'):
631 raise osv.except_osv(
632 _('Could not cancel sales order !'),
633 _('You must first cancel all picking attached to this sales order.'))
634 if pick.state == 'cancel':
635 for mov in pick.move_lines:
636 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
638 for proc in proc_ids:
639 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
640 for r in self.read(cr, uid, ids, ['picking_ids']):
641 for pick in r['picking_ids']:
642 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
643 for inv in sale.invoice_ids:
644 if inv.state not in ('draft', 'cancel'):
645 raise osv.except_osv(
646 _('Could not cancel this sales order !'),
647 _('You must first cancel all invoices attached to this sales order.'))
648 for r in self.read(cr, uid, ids, ['invoice_ids']):
649 for inv in r['invoice_ids']:
650 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
651 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
653 message = _("The sales order '%s' has been cancelled.") % (sale.name,)
654 self.log(cr, uid, sale.id, message)
655 self.write(cr, uid, ids, {'state': 'cancel'})
658 def action_wait(self, cr, uid, ids, context=None):
659 for o in self.browse(cr, uid, ids):
661 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
662 if (o.order_policy == 'manual'):
663 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
665 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
666 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
667 message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
668 self.log(cr, uid, o.id, message)
671 def procurement_lines_get(self, cr, uid, ids, *args):
673 for order in self.browse(cr, uid, ids, context={}):
674 for line in order.order_line:
675 if line.procurement_id:
676 res.append(line.procurement_id.id)
679 # if mode == 'finished':
680 # returns True if all lines are done, False otherwise
681 # if mode == 'canceled':
682 # returns True if there is at least one canceled line, False otherwise
683 def test_state(self, cr, uid, ids, mode, *args):
684 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
689 write_cancel_ids = []
690 for order in self.browse(cr, uid, ids, context={}):
691 for line in order.order_line:
692 if (not line.procurement_id) or (line.procurement_id.state=='done'):
693 if line.state != 'done':
694 write_done_ids.append(line.id)
697 if line.procurement_id:
698 if (line.procurement_id.state == 'cancel'):
700 if line.state != 'exception':
701 write_cancel_ids.append(line.id)
705 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
707 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
709 if mode == 'finished':
711 elif mode == 'canceled':
717 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
720 'origin': order.name,
721 'date_planned': date_planned,
722 'product_id': line.product_id.id,
723 'product_qty': line.product_uom_qty,
724 'product_uom': line.product_uom.id,
725 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
726 or line.product_uom_qty,
727 'product_uos': (line.product_uos and line.product_uos.id)\
728 or line.product_uom.id,
729 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
730 'procure_method': line.type,
732 'company_id': order.company_id.id,
736 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
737 location_id = order.shop_id.warehouse_id.lot_stock_id.id
738 output_id = order.shop_id.warehouse_id.lot_output_id.id
740 'name': line.name[:250],
741 'picking_id': picking_id,
742 'product_id': line.product_id.id,
743 'date': date_planned,
744 'date_expected': date_planned,
745 'product_qty': line.product_uom_qty,
746 'product_uom': line.product_uom.id,
747 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
748 'product_uos': (line.product_uos and line.product_uos.id)\
749 or line.product_uom.id,
750 'product_packaging': line.product_packaging.id,
751 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
752 'location_id': location_id,
753 'location_dest_id': output_id,
754 'sale_line_id': line.id,
755 'tracking_id': False,
759 'company_id': order.company_id.id,
760 'price_unit': line.product_id.standard_price or 0.0
763 def _prepare_order_picking(self, cr, uid, order, context=None):
764 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
767 'origin': order.name,
768 'date': order.date_order,
771 'move_type': order.picking_policy,
773 'address_id': order.partner_shipping_id.id,
775 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
776 'company_id': order.company_id.id,
779 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
780 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
782 Define ship_recreate for process after shipping exception
783 param order: sale order to which the order lines belong
784 param line: sale order line records to procure
785 param move_id: the ID of stock move
786 param proc_id: the ID of procurement
788 move_obj = self.pool.get('stock.move')
789 if order.state == 'shipping_except':
790 for pick in order.picking_ids:
791 for move in pick.move_lines:
792 if move.state == 'cancel':
793 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
795 for mov in move_obj.browse(cr, uid, mov_ids):
796 # 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?
797 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
798 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
801 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
802 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
803 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
806 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
807 """Create the required procurements to supply sale order lines, also connecting
808 the procurements to appropriate stock moves in order to bring the goods to the
809 sale order's requested location.
811 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
812 a standard outgoing picking will be created to wrap the stock moves, as returned
813 by :meth:`~._prepare_order_picking`.
815 Modules that wish to customize the procurements or partition the stock moves over
816 multiple stock pickings may override this method and call ``super()`` with
817 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
819 :param browse_record order: sale order to which the order lines belong
820 :param list(browse_record) order_lines: sale order line records to procure
821 :param int picking_id: optional ID of a stock picking to which the created stock moves
822 will be added. A new picking will be created if ommitted.
825 move_obj = self.pool.get('stock.move')
826 picking_obj = self.pool.get('stock.picking')
827 procurement_obj = self.pool.get('procurement.order')
830 for line in order_lines:
831 if line.state == 'done':
834 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
837 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
839 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
840 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
842 # a service has no stock move
845 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
846 proc_ids.append(proc_id)
847 line.write({'procurement_id': proc_id})
848 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
850 wf_service = netsvc.LocalService("workflow")
852 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
854 for proc_id in proc_ids:
855 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
858 if order.state == 'shipping_except':
859 val['state'] = 'progress'
860 val['shipped'] = False
862 if (order.order_policy == 'manual'):
863 for line in order.order_line:
864 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
865 val['state'] = 'manual'
870 def action_ship_create(self, cr, uid, ids, context=None):
871 for order in self.browse(cr, uid, ids, context=context):
872 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
875 def action_ship_end(self, cr, uid, ids, context=None):
876 for order in self.browse(cr, uid, ids, context=context):
877 val = {'shipped': True}
878 if order.state == 'shipping_except':
879 val['state'] = 'progress'
880 if (order.order_policy == 'manual'):
881 for line in order.order_line:
882 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
883 val['state'] = 'manual'
885 for line in order.order_line:
887 if line.state == 'exception':
888 towrite.append(line.id)
890 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
891 self.write(cr, uid, [order.id], val)
894 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
895 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
897 part = inv['partner_id'] and inv['partner_id'][0]
898 pr = inv['amount_untaxed'] or 0.0
899 partnertype = 'customer'
902 'name': 'Order: '+name,
904 'description': 'Order '+str(inv['id']),
907 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
909 'partner_type': partnertype,
911 'planned_revenue': pr,
915 self.pool.get('res.partner.event').create(cr, uid, event)
917 def has_stockable_products(self, cr, uid, ids, *args):
918 for order in self.browse(cr, uid, ids):
919 for order_line in order.order_line:
920 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
925 # TODO add a field price_unit_uos
926 # - update it on change product and unit price
927 # - use it in report if there is a uos
928 class sale_order_line(osv.osv):
930 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
931 tax_obj = self.pool.get('account.tax')
932 cur_obj = self.pool.get('res.currency')
936 for line in self.browse(cr, uid, ids, context=context):
937 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
938 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)
939 cur = line.order_id.pricelist_id.currency_id
940 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
943 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
945 for line in self.browse(cr, uid, ids, context=context):
947 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
952 def _get_uom_id(self, cr, uid, *args):
954 proxy = self.pool.get('ir.model.data')
955 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
957 except Exception, ex:
960 _name = 'sale.order.line'
961 _description = 'Sales Order Line'
963 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
964 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
965 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
966 '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)]}),
967 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
968 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
969 'invoiced': fields.boolean('Invoiced', readonly=True),
970 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
971 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
972 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
973 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
974 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
975 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."),
976 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
977 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
978 'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
979 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
980 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
981 'product_uos': fields.many2one('product.uom', 'Product UoS'),
982 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
983 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
984 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
985 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
986 'notes': fields.text('Notes'),
987 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
988 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
989 help='* The \'Draft\' state is set when the related sales order in draft state. \
990 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
991 \n* The \'Exception\' state is set when the related sales order is set as exception. \
992 \n* The \'Done\' state is set when the sales order line has been picked. \
993 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
994 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
995 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
996 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
998 _order = 'sequence, id'
1000 'product_uom' : _get_uom_id,
1003 'product_uom_qty': 1,
1004 'product_uos_qty': 1,
1008 'type': 'make_to_stock',
1009 'product_packaging': False,
1013 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1014 """Prepare the dict of values to create the new invoice line for a
1015 sale order line. This method may be overridden to implement custom
1016 invoice generation (making sure to call super() to establish
1017 a clean extension chain).
1019 :param browse_record line: sale.order.line record to invoice
1020 :param int account_id: optional ID of a G/L account to force
1021 (this is used for returning products including service)
1022 :return: dict of values to create() the invoice line
1025 def _get_line_qty(line):
1026 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1027 if line.product_uos:
1028 return line.product_uos_qty or 0.0
1029 return line.product_uom_qty
1031 return self.pool.get('procurement.order').quantity_get(cr, uid,
1032 line.procurement_id.id, context=context)
1034 def _get_line_uom(line):
1035 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1036 if line.product_uos:
1037 return line.product_uos.id
1038 return line.product_uom.id
1040 return self.pool.get('procurement.order').uom_get(cr, uid,
1041 line.procurement_id.id, context=context)
1043 if not line.invoiced:
1046 account_id = line.product_id.product_tmpl_id.property_account_income.id
1048 account_id = line.product_id.categ_id.property_account_income_categ.id
1050 raise osv.except_osv(_('Error !'),
1051 _('There is no income account defined for this product: "%s" (id:%d)') % \
1052 (line.product_id.name, line.product_id.id,))
1054 prop = self.pool.get('ir.property').get(cr, uid,
1055 'property_account_income_categ', 'product.category',
1057 account_id = prop and prop.id or False
1058 uosqty = _get_line_qty(line)
1059 uos_id = _get_line_uom(line)
1062 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1063 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1064 fpos = line.order_id.fiscal_position or False
1065 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1067 raise osv.except_osv(_('Error !'),
1068 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1071 'origin': line.order_id.name,
1072 'account_id': account_id,
1075 'discount': line.discount,
1077 'product_id': line.product_id.id or False,
1078 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1080 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1085 def invoice_line_create(self, cr, uid, ids, context=None):
1091 for line in self.browse(cr, uid, ids, context=context):
1092 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1094 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1095 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1096 self.write(cr, uid, [line.id], {'invoiced': True})
1097 sales.add(line.order_id.id)
1098 create_ids.append(inv_id)
1099 # Trigger workflow events
1100 wf_service = netsvc.LocalService("workflow")
1101 for sale_id in sales:
1102 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1105 def button_cancel(self, cr, uid, ids, context=None):
1106 for line in self.browse(cr, uid, ids, context=context):
1108 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1109 for move_line in line.move_ids:
1110 if move_line.state != 'cancel':
1111 raise osv.except_osv(
1112 _('Could not cancel sales order line!'),
1113 _('You must first cancel stock moves attached to this sales order line.'))
1114 return self.write(cr, uid, ids, {'state': 'cancel'})
1116 def button_confirm(self, cr, uid, ids, context=None):
1117 return self.write(cr, uid, ids, {'state': 'confirmed'})
1119 def button_done(self, cr, uid, ids, context=None):
1120 wf_service = netsvc.LocalService("workflow")
1121 res = self.write(cr, uid, ids, {'state': 'done'})
1122 for line in self.browse(cr, uid, ids, context=context):
1123 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1126 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1127 product_obj = self.pool.get('product.product')
1129 return {'value': {'product_uom': product_uos,
1130 'product_uom_qty': product_uos_qty}, 'domain': {}}
1132 product = product_obj.browse(cr, uid, product_id)
1134 'product_uom': product.uom_id.id,
1136 # FIXME must depend on uos/uom of the product and not only of the coeff.
1139 'product_uom_qty': product_uos_qty / product.uos_coeff,
1140 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1142 except ZeroDivisionError:
1144 return {'value': value}
1146 def copy_data(self, cr, uid, id, default=None, context=None):
1149 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1150 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1152 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1153 partner_id=False, packaging=False, flag=False, context=None):
1155 return {'value': {'product_packaging': False}}
1156 product_obj = self.pool.get('product.product')
1157 product_uom_obj = self.pool.get('product.uom')
1158 pack_obj = self.pool.get('product.packaging')
1163 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1164 product=product, qty=qty, uom=uom, partner_id=partner_id,
1165 packaging=packaging, flag=False, context=context)
1166 warning_msgs = res.get('warning') and res['warning']['message']
1168 products = product_obj.browse(cr, uid, product, context=context)
1169 if not products.packaging:
1170 packaging = result['product_packaging'] = False
1171 elif not packaging and products.packaging and not flag:
1172 packaging = products.packaging[0].id
1173 result['product_packaging'] = packaging
1176 default_uom = products.uom_id and products.uom_id.id
1177 pack = pack_obj.browse(cr, uid, packaging, context=context)
1178 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1179 # qty = qty - qty % q + q
1180 if qty and (q and not (qty % q) == 0):
1181 ean = pack.ean or _('(n/a)')
1184 if not warning_msgs:
1185 warn_msg = _("You selected a quantity of %d Units.\n"
1186 "But it's not compatible with the selected packaging.\n"
1187 "Here is a proposition of quantities according to the packaging:\n"
1188 "EAN: %s Quantity: %s Type of ul: %s") % \
1189 (qty, ean, qty_pack, type_ul.name)
1190 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1192 'title': _('Configuration Error !'),
1193 'message': warning_msgs
1195 result['product_uom_qty'] = qty
1197 return {'value': result, 'warning': warning}
1199 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1200 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1201 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1202 context = context or {}
1203 lang = lang or context.get('lang',False)
1205 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.'))
1207 product_uom_obj = self.pool.get('product.uom')
1208 partner_obj = self.pool.get('res.partner')
1209 product_obj = self.pool.get('product.product')
1210 context = {'lang': lang, 'partner_id': partner_id}
1212 lang = partner_obj.browse(cr, uid, partner_id).lang
1213 context_partner = {'lang': lang, 'partner_id': partner_id}
1216 return {'value': {'th_weight': 0, 'product_packaging': False,
1217 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1220 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1222 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1223 result = res.get('value', {})
1224 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1225 product_obj = product_obj.browse(cr, uid, product, context=context)
1229 uom2 = product_uom_obj.browse(cr, uid, uom)
1230 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1233 if product_obj.uos_id:
1234 uos2 = product_uom_obj.browse(cr, uid, uos)
1235 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1239 if product_obj.description_sale:
1240 result['notes'] = product_obj.description_sale
1241 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1242 if update_tax: #The quantity only have changed
1243 result['delay'] = (product_obj.sale_delay or 0.0)
1244 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1245 result.update({'type': product_obj.procure_method})
1248 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1250 if (not uom) and (not uos):
1251 result['product_uom'] = product_obj.uom_id.id
1252 if product_obj.uos_id:
1253 result['product_uos'] = product_obj.uos_id.id
1254 result['product_uos_qty'] = qty * product_obj.uos_coeff
1255 uos_category_id = product_obj.uos_id.category_id.id
1257 result['product_uos'] = False
1258 result['product_uos_qty'] = qty
1259 uos_category_id = False
1260 result['th_weight'] = qty * product_obj.weight
1261 domain = {'product_uom':
1262 [('category_id', '=', product_obj.uom_id.category_id.id)],
1264 [('category_id', '=', uos_category_id)]}
1266 elif uos and not uom: # only happens if uom is False
1267 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1268 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1269 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1270 elif uom: # whether uos is set or not
1271 default_uom = product_obj.uom_id and product_obj.uom_id.id
1272 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1273 if product_obj.uos_id:
1274 result['product_uos'] = product_obj.uos_id.id
1275 result['product_uos_qty'] = qty * product_obj.uos_coeff
1277 result['product_uos'] = False
1278 result['product_uos_qty'] = qty
1279 result['th_weight'] = q * product_obj.weight # Round the quantity up
1282 uom2 = product_obj.uom_id
1283 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1284 if (product_obj.type=='product') and int(compare_qty) == -1 \
1285 and (product_obj.procure_method=='make_to_stock'):
1286 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1287 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1288 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1289 max(0,product_obj.qty_available), product_obj.uom_id.name)
1290 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1294 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1295 'Please set one before choosing a product.')
1296 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1298 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1299 product, qty or 1.0, partner_id, {
1300 'uom': uom or result.get('product_uom'),
1304 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1305 "You have to change either the product, the quantity or the pricelist.")
1307 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1309 result.update({'price_unit': price})
1312 'title': _('Configuration Error !'),
1313 'message' : warning_msgs
1315 return {'value': result, 'domain': domain, 'warning': warning}
1317 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1318 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1319 lang=False, update_tax=True, date_order=False, context=None):
1320 context = context or {}
1321 lang = lang or ('lang' in context and context['lang'])
1322 res = self.product_id_change(cursor, user, ids, pricelist, product,
1323 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1324 partner_id=partner_id, lang=lang, update_tax=update_tax,
1325 date_order=date_order, context=context)
1326 if 'product_uom' in res['value']:
1327 del res['value']['product_uom']
1329 res['value']['price_unit'] = 0.0
1332 def unlink(self, cr, uid, ids, context=None):
1335 """Allows to delete sales order lines in draft,cancel states"""
1336 for rec in self.browse(cr, uid, ids, context=context):
1337 if rec.state not in ['draft', 'cancel']:
1338 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1339 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1343 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: