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, DATETIME_FORMATS_MAP, float_compare
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 _inherit = ['ir.needaction_mixin', 'mail.thread']
52 _description = "Sales Order"
55 def copy(self, cr, uid, id, default=None, context=None):
63 'date_confirm': False,
64 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
66 return super(sale_order, self).copy(cr, uid, id, default, context=context)
68 def _amount_line_tax(self, cr, uid, line, context=None):
70 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.product_id, line.order_id.partner_id)['taxes']:
71 val += c.get('amount', 0.0)
74 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
75 cur_obj = self.pool.get('res.currency')
77 for order in self.browse(cr, uid, ids, context=context):
79 'amount_untaxed': 0.0,
84 cur = order.pricelist_id.currency_id
85 for line in order.order_line:
86 val1 += line.price_subtotal
87 val += self._amount_line_tax(cr, uid, line, context=context)
88 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
89 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
90 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
94 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
100 tmp[id] = {'picked': 0.0, 'total': 0.0}
102 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
106 stock_picking p on (p.id=m.picking_id)
108 procurement_order mp on (mp.move_id=m.id)
110 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
112 for item in cr.dictfetchall():
113 if item['move_state'] == 'cancel':
116 if item['picking_type'] == 'in':#this is a returned picking
117 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
118 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
119 tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
121 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
122 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
123 tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
125 for order in self.browse(cr, uid, ids, context=context):
127 res[order.id] = 100.0
129 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
132 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
134 for sale in self.browse(cursor, user, ids, context=context):
139 for invoice in sale.invoice_ids:
140 if invoice.state not in ('draft', 'cancel'):
141 tot += invoice.amount_untaxed
143 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
148 def _invoiced(self, cursor, user, ids, name, arg, context=None):
150 for sale in self.browse(cursor, user, ids, context=context):
152 invoice_existence = False
153 for invoice in sale.invoice_ids:
154 if invoice.state!='cancel':
155 invoice_existence = True
156 if invoice.state != 'paid':
159 if not invoice_existence:
163 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
172 clause += 'AND inv.state = \'paid\''
174 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
175 sale_clause = ', sale_order AS sale '
178 cursor.execute('SELECT rel.order_id ' \
179 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
180 'WHERE rel.invoice_id = inv.id ' + clause)
181 res = cursor.fetchall()
183 cursor.execute('SELECT sale.id ' \
184 'FROM sale_order AS sale ' \
185 'WHERE sale.id NOT IN ' \
186 '(SELECT rel.order_id ' \
187 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
188 res.extend(cursor.fetchall())
190 return [('id', '=', 0)]
191 return [('id', 'in', [x[0] for x in res])]
193 def _get_order(self, cr, uid, ids, context=None):
195 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
196 result[line.order_id.id] = True
200 'name': fields.char('Order Reference', size=64, required=True,
201 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
202 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
203 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
204 'client_order_ref': fields.char('Customer Reference', size=64),
205 'state': fields.selection([
206 ('draft', 'Draft Quotation'),
207 ('sent', 'Quotation Sent'),
208 ('cancel', 'Cancelled'),
209 ('waiting_date', 'Waiting Schedule'),
210 ('progress', 'Sale Order'),
211 ('manual', 'Sale to Invoice'),
212 ('shipping_except', 'Shipping Exception'),
213 ('invoice_except', 'Invoice Exception'),
215 ], 'Status', 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),
216 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
217 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
218 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
219 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
220 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
221 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."),
222 'partner_shipping_id': fields.many2one('res.partner', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Shipping address for current sales order."),
224 '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."),
225 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
226 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
227 help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
228 'order_policy': fields.selection([
229 ('manual', 'On Demand'),
230 ('picking', 'On Delivery Order'),
231 ('prepaid', 'Before Delivery'),
232 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
233 help="""This field controls how invoice and delivery operations are synchronized.
234 - With 'On Demand', the invoice is created manually when needed.
235 - With 'On Delivery Order', a draft invoice is generated after all pickings have been processed.
236 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
237 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('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)], 'sent': [('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)], 'sent': [('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.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders 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('Terms and conditions'),
250 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), 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('Account'), 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('Account'), 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)
275 'picking_policy': 'direct',
276 'date_order': fields.date.context_today,
277 'order_policy': 'manual',
279 'user_id': lambda obj, cr, uid, context: uid,
280 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
281 'invoice_quantity': 'order',
282 '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'],
283 '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'],
286 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
291 def unlink(self, cr, uid, ids, context=None):
292 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
294 for s in sale_orders:
295 if s['state'] in ['draft', 'cancel']:
296 unlink_ids.append(s['id'])
298 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it.\nTo do so, you must first cancel related picking for delivery orders.'))
300 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
302 def onchange_shop_id(self, cr, uid, ids, shop_id):
305 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
306 v['project_id'] = shop.project_id.id
307 # Que faire si le client a une pricelist a lui ?
308 if shop.pricelist_id.id:
309 v['pricelist_id'] = shop.pricelist_id.id
312 def action_cancel_draft(self, cr, uid, ids, context=None):
315 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
316 line_ids = map(lambda x: x[0], cr.fetchall())
317 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
318 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
319 wf_service = netsvc.LocalService("workflow")
321 # Deleting the existing instance of workflow for SO
322 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
323 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
324 self.action_cancel_draft_send_note(cr, uid, ids, context=context)
327 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
328 if (not pricelist_id) or (not order_lines):
331 'title': _('Pricelist Warning!'),
332 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
334 return {'warning': warning}
336 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
341 val['partner_invoice_id'] = order_id
343 val['partner_shipping_id'] = order_id
344 return {'value': val}
346 def onchange_partner_id(self, cr, uid, ids, part):
348 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
350 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
351 part = self.pool.get('res.partner').browse(cr, uid, part)
352 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
353 payment_term = part.property_payment_term and part.property_payment_term.id or False
354 fiscal_position = part.property_account_position and part.property_account_position.id or False
355 dedicated_salesman = part.user_id and part.user_id.id or uid
357 'partner_invoice_id': addr['invoice'],
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 order = super(sale_order, self).create(cr, uid, vals, context=context)
393 self.create_send_note(cr, uid, [order], context=context)
396 def button_dummy(self, cr, uid, ids, context=None):
399 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
400 # can be removed after 6.1.
401 def _inv_get(self, cr, uid, order, context=None):
404 def _prepare_invoice(self, cr, uid, order, lines, context=None):
405 """Prepare the dict of values to create the new invoice for a
406 sale order. This method may be overridden to implement custom
407 invoice generation (making sure to call super() to establish
408 a clean extension chain).
410 :param browse_record order: sale.order record to invoice
411 :param list(int) line: list of invoice line IDs that must be
412 attached to the invoice
413 :return: dict of value to create() the invoice
417 journal_ids = self.pool.get('account.journal').search(cr, uid,
418 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
421 raise osv.except_osv(_('Error!'),
422 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
425 'name': order.client_order_ref or '',
426 'origin': order.name,
427 'type': 'out_invoice',
428 'reference': order.client_order_ref or order.name,
429 'account_id': order.partner_id.property_account_receivable.id,
430 'partner_id': order.partner_id.id,
431 'journal_id': journal_ids[0],
432 'invoice_line': [(6, 0, lines)],
433 'currency_id': order.pricelist_id.currency_id.id,
434 'comment': order.note,
435 'payment_term': order.payment_term and order.payment_term.id or False,
436 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
437 'date_invoice': context.get('date_invoice', False),
438 'company_id': order.company_id.id,
439 'user_id': order.user_id and order.user_id.id or False
442 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
443 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
447 def _make_invoice(self, cr, uid, order, lines, context=None):
448 inv_obj = self.pool.get('account.invoice')
449 obj_invoice_line = self.pool.get('account.invoice.line')
452 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
453 from_line_invoice_ids = []
454 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
455 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
456 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
457 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
458 for preinv in order.invoice_ids:
459 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
460 for preline in preinv.invoice_line:
461 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
462 lines.append(inv_line_id)
463 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
464 inv_id = inv_obj.create(cr, uid, inv, context=context)
465 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
466 if data.get('value', False):
467 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
468 inv_obj.button_compute(cr, uid, [inv_id])
471 def print_quotation(self, cr, uid, ids, context=None):
473 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
475 assert len(ids) == 1, 'This option should only be used for a single id at a time'
476 wf_service = netsvc.LocalService("workflow")
477 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
479 'model': 'sale.order',
481 'form': self.read(cr, uid, ids[0], context=context),
483 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
485 def manual_invoice(self, cr, uid, ids, context=None):
486 """ create invoices for the given sale orders (ids), and open the form
487 view of one of the newly created invoices
489 mod_obj = self.pool.get('ir.model.data')
490 wf_service = netsvc.LocalService("workflow")
492 # create invoices through the sale orders' workflow
493 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
495 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
496 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
497 # determine newly created invoices
498 new_inv_ids = list(inv_ids1 - inv_ids0)
500 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
501 res_id = res and res[1] or False,
504 'name': _('Customer Invoices'),
508 'res_model': 'account.invoice',
509 'context': "{'type':'out_invoice'}",
510 'type': 'ir.actions.act_window',
513 'res_id': new_inv_ids and new_inv_ids[0] or False,
516 def action_view_invoice(self, cr, uid, ids, context=None):
518 This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
520 mod_obj = self.pool.get('ir.model.data')
522 'name': _('Cutomer Invoice'),
524 'res_model': 'account.invoice',
525 'context': "{'type':'out_invoice', 'journal_type': 'sale'}",
526 'type': 'ir.actions.act_window',
530 #compute the number of invoices to display
532 for so in self.browse(cr, uid, ids, context=context):
533 inv_ids += [invoice.id for invoice in so.invoice_ids]
534 #choose the view_mode accordingly
536 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_tree')
538 'view_mode': 'tree,form',
539 'res_id': inv_ids or False
542 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
545 'res_id': inv_ids and inv_ids[0] or False,
547 result.update(view_id = res and res[1] or False)
551 def action_view_delivery(self, cr, uid, ids, context=None):
553 This function returns an action that display existing delivery orders of given sale order ids. It can either be a in a list or in a form view, if there is only one delivery order to show.
555 mod_obj = self.pool.get('ir.model.data')
557 'name': _('Delivery Order'),
559 'res_model': 'stock.picking',
560 'context': "{'type':'out'}",
561 'type': 'ir.actions.act_window',
565 #compute the number of delivery orders to display
567 for so in self.browse(cr, uid, ids, context=context):
568 pick_ids += [picking.id for picking in so.picking_ids]
569 #choose the view_mode accordingly
570 if len(pick_ids) > 1:
571 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_tree')
573 'view_mode': 'tree,form',
574 'res_id': pick_ids or False
577 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
580 'res_id': pick_ids and pick_ids[0] or False,
582 result.update(view_id = res and res[1] or False)
585 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
589 picking_obj = self.pool.get('stock.picking')
590 invoice = self.pool.get('account.invoice')
591 obj_sale_order_line = self.pool.get('sale.order.line')
592 partner_currency = {}
595 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
596 # last day of the last month as invoice date
598 context['date_inv'] = date_inv
599 for o in self.browse(cr, uid, ids, context=context):
600 currency_id = o.pricelist_id.currency_id.id
601 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
602 raise osv.except_osv(
604 _('You cannot group sales having different currencies for the same partner.'))
606 partner_currency[o.partner_id.id] = currency_id
608 for line in o.order_line:
611 elif (line.state in states):
612 lines.append(line.id)
613 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
615 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
617 for o in self.browse(cr, uid, ids, context=context):
618 for i in o.invoice_ids:
619 if i.state == 'draft':
621 for val in invoices.values():
623 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
626 invoice_ref += o.name + '|'
627 self.write(cr, uid, [o.id], {'state': 'progress'})
628 if o.order_policy == 'picking':
629 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
630 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
631 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
633 for order, il in val:
634 res = self._make_invoice(cr, uid, order, il, context=context)
635 invoice_ids.append(res)
636 self.write(cr, uid, [order.id], {'state': 'progress'})
637 if order.order_policy == 'picking':
638 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
639 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
641 self.invoice_send_note(cr, uid, ids, res, context)
644 def action_invoice_cancel(self, cr, uid, ids, context=None):
647 for sale in self.browse(cr, uid, ids, context=context):
648 for line in sale.order_line:
650 # Check if the line is invoiced (has asociated invoice
651 # lines from non-cancelled invoices).
654 for iline in line.invoice_lines:
655 if iline.invoice_id and iline.invoice_id.state != 'cancel':
658 # Update the line (only when needed)
659 if line.invoiced != invoiced:
660 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
661 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
664 def action_invoice_end(self, cr, uid, ids, context=None):
665 for order in self.browse(cr, uid, ids, context=context):
667 # Update the sale order lines state (and invoiced flag).
669 for line in order.order_line:
672 # Check if the line is invoiced (has asociated invoice
673 # lines from non-cancelled invoices).
676 for iline in line.invoice_lines:
677 if iline.invoice_id and iline.invoice_id.state != 'cancel':
680 if line.invoiced != invoiced:
681 vals['invoiced'] = invoiced
682 # If the line was in exception state, now it gets confirmed.
683 if line.state == 'exception':
684 vals['state'] = 'confirmed'
685 # Update the line (only when needed).
687 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
689 # Update the sales order state.
691 if order.state == 'invoice_except':
692 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
695 def action_cancel(self, cr, uid, ids, context=None):
696 wf_service = netsvc.LocalService("workflow")
699 sale_order_line_obj = self.pool.get('sale.order.line')
700 proc_obj = self.pool.get('procurement.order')
701 for sale in self.browse(cr, uid, ids, context=context):
702 for pick in sale.picking_ids:
703 if pick.state not in ('draft', 'cancel'):
704 raise osv.except_osv(
705 _('Cannot cancel sales order!'),
706 _('You must first cancel all delivery order(s) attached to this sales order.'))
707 if pick.state == 'cancel':
708 for mov in pick.move_lines:
709 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
711 for proc in proc_ids:
712 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
713 for r in self.read(cr, uid, ids, ['picking_ids']):
714 for pick in r['picking_ids']:
715 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
716 for inv in sale.invoice_ids:
717 if inv.state not in ('draft', 'cancel'):
718 raise osv.except_osv(
719 _('Cannot cancel this sales order!'),
720 _('First cancel all invoices attached to this sales order.'))
721 for r in self.read(cr, uid, ids, ['invoice_ids']):
722 for inv in r['invoice_ids']:
723 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
724 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
726 self.cancel_send_note(cr, uid, [sale.id], context=None)
727 self.write(cr, uid, ids, {'state': 'cancel'})
730 def action_button_confirm(self, cr, uid, ids, context=None):
731 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
732 wf_service = netsvc.LocalService('workflow')
733 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
735 # redisplay the record as a sale order
736 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
737 view_id = view_ref and view_ref[1] or False,
739 'type': 'ir.actions.act_window',
740 'name': _('Sales Order'),
741 'res_model': 'sale.order',
750 def action_wait(self, cr, uid, ids, context=None):
751 for o in self.browse(cr, uid, ids):
753 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
754 if (o.order_policy == 'manual'):
755 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
757 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
758 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
759 self.confirm_send_note(cr, uid, ids, context)
762 def action_quotation_send(self, cr, uid, ids, context=None):
764 This function opens a window to compose an email, with the edi sale template message loaded by default
766 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
767 mod_obj = self.pool.get('ir.model.data')
768 template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
769 template_id = template and template[1] or False
770 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
771 res_id = res and res[1] or False
772 ctx = dict(context, active_model='sale.order', active_id=ids[0])
773 ctx.update({'mail.compose.template_id': template_id})
777 'res_model': 'mail.compose.message',
778 'views': [(res_id,'form')],
780 'type': 'ir.actions.act_window',
786 def procurement_lines_get(self, cr, uid, ids, *args):
788 for order in self.browse(cr, uid, ids, context={}):
789 for line in order.order_line:
790 if line.procurement_id:
791 res.append(line.procurement_id.id)
794 # if mode == 'finished':
795 # returns True if all lines are done, False otherwise
796 # if mode == 'canceled':
797 # returns True if there is at least one canceled line, False otherwise
798 def test_state(self, cr, uid, ids, mode, *args):
799 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
804 write_cancel_ids = []
805 for order in self.browse(cr, uid, ids, context={}):
806 for line in order.order_line:
807 if (not line.procurement_id) or (line.procurement_id.state=='done'):
808 if line.state != 'done':
809 write_done_ids.append(line.id)
812 if line.procurement_id:
813 if (line.procurement_id.state == 'cancel'):
815 if line.state != 'exception':
816 write_cancel_ids.append(line.id)
820 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
822 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
824 if mode == 'finished':
826 elif mode == 'canceled':
832 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
834 'name': line.name.split('\n')[0],
835 'origin': order.name,
836 'date_planned': date_planned,
837 'product_id': line.product_id.id,
838 'product_qty': line.product_uom_qty,
839 'product_uom': line.product_uom.id,
840 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
841 or line.product_uom_qty,
842 'product_uos': (line.product_uos and line.product_uos.id)\
843 or line.product_uom.id,
844 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
845 'procure_method': line.type,
847 'company_id': order.company_id.id,
848 'note': '\n'.join(line.name.split('\n')[1:]),
849 'property_ids': [(6, 0, [x.id for x in line.property_ids])]
852 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
853 location_id = order.shop_id.warehouse_id.lot_stock_id.id
854 output_id = order.shop_id.warehouse_id.lot_output_id.id
856 'name': line.name.split('\n')[0][:250],
857 'picking_id': picking_id,
858 'product_id': line.product_id.id,
859 'date': date_planned,
860 'date_expected': date_planned,
861 'product_qty': line.product_uom_qty,
862 'product_uom': line.product_uom.id,
863 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
864 'product_uos': (line.product_uos and line.product_uos.id)\
865 or line.product_uom.id,
866 'product_packaging': line.product_packaging.id,
867 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
868 'location_id': location_id,
869 'location_dest_id': output_id,
870 'sale_line_id': line.id,
871 'tracking_id': False,
874 'note': '\n'.join(line.name.split('\n')[1:]),
875 'company_id': order.company_id.id,
876 'price_unit': line.product_id.standard_price or 0.0
879 def _prepare_order_picking(self, cr, uid, order, context=None):
880 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
883 'origin': order.name,
884 'date': order.date_order,
887 'move_type': order.picking_policy,
889 'partner_id': order.partner_shipping_id.id,
891 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
892 'company_id': order.company_id.id,
895 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
896 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
898 Define ship_recreate for process after shipping exception
899 param order: sale order to which the order lines belong
900 param line: sale order line records to procure
901 param move_id: the ID of stock move
902 param proc_id: the ID of procurement
904 move_obj = self.pool.get('stock.move')
905 if order.state == 'shipping_except':
906 for pick in order.picking_ids:
907 for move in pick.move_lines:
908 if move.state == 'cancel':
909 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
911 for mov in move_obj.browse(cr, uid, mov_ids):
912 # 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?
913 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
914 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
917 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
918 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
919 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
922 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
923 """Create the required procurements to supply sale order lines, also connecting
924 the procurements to appropriate stock moves in order to bring the goods to the
925 sale order's requested location.
927 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
928 a standard outgoing picking will be created to wrap the stock moves, as returned
929 by :meth:`~._prepare_order_picking`.
931 Modules that wish to customize the procurements or partition the stock moves over
932 multiple stock pickings may override this method and call ``super()`` with
933 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
935 :param browse_record order: sale order to which the order lines belong
936 :param list(browse_record) order_lines: sale order line records to procure
937 :param int picking_id: optional ID of a stock picking to which the created stock moves
938 will be added. A new picking will be created if ommitted.
941 move_obj = self.pool.get('stock.move')
942 picking_obj = self.pool.get('stock.picking')
943 procurement_obj = self.pool.get('procurement.order')
946 for line in order_lines:
947 if line.state == 'done':
950 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
953 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
955 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
956 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
958 # a service has no stock move
961 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
962 proc_ids.append(proc_id)
963 line.write({'procurement_id': proc_id})
964 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
966 wf_service = netsvc.LocalService("workflow")
968 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
969 self.delivery_send_note(cr, uid, [order.id], picking_id, context)
972 for proc_id in proc_ids:
973 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
976 if order.state == 'shipping_except':
977 val['state'] = 'progress'
978 val['shipped'] = False
980 if (order.order_policy == 'manual'):
981 for line in order.order_line:
982 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
983 val['state'] = 'manual'
988 def action_ship_create(self, cr, uid, ids, context=None):
989 for order in self.browse(cr, uid, ids, context=context):
990 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
993 def action_ship_end(self, cr, uid, ids, context=None):
994 for order in self.browse(cr, uid, ids, context=context):
995 val = {'shipped': True}
996 if order.state == 'shipping_except':
997 val['state'] = 'progress'
998 if (order.order_policy == 'manual'):
999 for line in order.order_line:
1000 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
1001 val['state'] = 'manual'
1003 for line in order.order_line:
1005 if line.state == 'exception':
1006 towrite.append(line.id)
1008 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
1009 res = self.write(cr, uid, [order.id], val)
1011 self.delivery_end_send_note(cr, uid, [order.id], context=context)
1014 def has_stockable_products(self, cr, uid, ids, *args):
1015 for order in self.browse(cr, uid, ids):
1016 for order_line in order.order_line:
1017 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
1021 # ------------------------------------------------
1022 # OpenChatter methods and notifications
1023 # ------------------------------------------------
1025 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1026 result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
1027 for obj in self.browse(cr, uid, ids, context=context):
1028 if (obj.state == 'manual' or obj.state == 'progress'):
1029 result[obj.id].append(obj.user_id.id)
1032 def create_send_note(self, cr, uid, ids, context=None):
1033 for obj in self.browse(cr, uid, ids, context=context):
1034 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name), context=context)
1036 def confirm_send_note(self, cr, uid, ids, context=None):
1037 for obj in self.browse(cr, uid, ids, context=context):
1038 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
1040 def cancel_send_note(self, cr, uid, ids, context=None):
1041 for obj in self.browse(cr, uid, ids, context=context):
1042 self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
1044 def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
1045 for order in self.browse(cr, uid, ids, context=context):
1046 for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
1047 # convert datetime field to a datetime, using server format, then
1048 # convert it to the user TZ and re-render it with %Z to add the timezone
1049 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
1050 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
1051 self.message_append_note(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
1053 def delivery_end_send_note(self, cr, uid, ids, context=None):
1054 self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
1056 def invoice_paid_send_note(self, cr, uid, ids, context=None):
1057 self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
1059 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
1060 for order in self.browse(cr, uid, ids, context=context):
1061 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
1062 self.message_append_note(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
1064 def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
1065 return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
1070 # TODO add a field price_unit_uos
1071 # - update it on change product and unit price
1072 # - use it in report if there is a uos
1073 class sale_order_line(osv.osv):
1075 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
1076 tax_obj = self.pool.get('account.tax')
1077 cur_obj = self.pool.get('res.currency')
1081 for line in self.browse(cr, uid, ids, context=context):
1082 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1083 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
1084 cur = line.order_id.pricelist_id.currency_id
1085 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
1088 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
1090 for line in self.browse(cr, uid, ids, context=context):
1092 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
1097 def _get_uom_id(self, cr, uid, *args):
1099 proxy = self.pool.get('ir.model.data')
1100 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
1102 except Exception, ex:
1105 _name = 'sale.order.line'
1106 _description = 'Sales Order Line'
1108 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1109 'name': fields.text('Product Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1110 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1111 '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)]}),
1112 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1113 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1114 'invoiced': fields.boolean('Invoiced', readonly=True),
1115 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1116 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
1117 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
1118 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1119 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1120 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."),
1121 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1122 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1123 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1124 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1125 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1126 'product_uos': fields.many2one('product.uom', 'Product UoS'),
1127 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1128 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1129 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
1130 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1131 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1132 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
1133 help='* The \'Draft\' state is set when the related sales order in draft state. \
1134 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1135 \n* The \'Exception\' state is set when the related sales order is set as exception. \
1136 \n* The \'Done\' state is set when the sales order line has been picked. \
1137 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1138 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1139 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
1140 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1142 _order = 'sequence, id'
1144 'product_uom' : _get_uom_id,
1147 'product_uom_qty': 1,
1148 'product_uos_qty': 1,
1152 'type': 'make_to_stock',
1153 'product_packaging': False,
1157 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1158 """Prepare the dict of values to create the new invoice line for a
1159 sale order line. This method may be overridden to implement custom
1160 invoice generation (making sure to call super() to establish
1161 a clean extension chain).
1163 :param browse_record line: sale.order.line record to invoice
1164 :param int account_id: optional ID of a G/L account to force
1165 (this is used for returning products including service)
1166 :return: dict of values to create() the invoice line
1169 def _get_line_qty(line):
1170 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1171 if line.product_uos:
1172 return line.product_uos_qty or 0.0
1173 return line.product_uom_qty
1175 return self.pool.get('procurement.order').quantity_get(cr, uid,
1176 line.procurement_id.id, context=context)
1178 def _get_line_uom(line):
1179 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1180 if line.product_uos:
1181 return line.product_uos.id
1182 return line.product_uom.id
1184 return self.pool.get('procurement.order').uom_get(cr, uid,
1185 line.procurement_id.id, context=context)
1187 if not line.invoiced:
1190 account_id = line.product_id.product_tmpl_id.property_account_income.id
1192 account_id = line.product_id.categ_id.property_account_income_categ.id
1194 raise osv.except_osv(_('Error!'),
1195 _('Please define income account for this product: "%s" (id:%d).') % \
1196 (line.product_id.name, line.product_id.id,))
1198 prop = self.pool.get('ir.property').get(cr, uid,
1199 'property_account_income_categ', 'product.category',
1201 account_id = prop and prop.id or False
1202 uosqty = _get_line_qty(line)
1203 uos_id = _get_line_uom(line)
1206 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1207 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
1208 fpos = line.order_id.fiscal_position or False
1209 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1211 raise osv.except_osv(_('Error!'),
1212 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
1215 'origin': line.order_id.name,
1216 'account_id': account_id,
1219 'discount': line.discount,
1221 'product_id': line.product_id.id or False,
1222 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1223 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1228 def invoice_line_create(self, cr, uid, ids, context=None):
1234 for line in self.browse(cr, uid, ids, context=context):
1235 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1237 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1238 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1239 self.write(cr, uid, [line.id], {'invoiced': True})
1240 sales.add(line.order_id.id)
1241 create_ids.append(inv_id)
1242 # Trigger workflow events
1243 wf_service = netsvc.LocalService("workflow")
1244 for sale_id in sales:
1245 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1248 def button_cancel(self, cr, uid, ids, context=None):
1249 for line in self.browse(cr, uid, ids, context=context):
1251 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
1252 for move_line in line.move_ids:
1253 if move_line.state != 'cancel':
1254 raise osv.except_osv(
1255 _('Cannot cancel sales order line!'),
1256 _('You must first cancel stock moves attached to this sales order line.'))
1257 return self.write(cr, uid, ids, {'state': 'cancel'})
1259 def button_confirm(self, cr, uid, ids, context=None):
1260 return self.write(cr, uid, ids, {'state': 'confirmed'})
1262 def button_done(self, cr, uid, ids, context=None):
1263 wf_service = netsvc.LocalService("workflow")
1264 res = self.write(cr, uid, ids, {'state': 'done'})
1265 for line in self.browse(cr, uid, ids, context=context):
1266 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1269 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1270 product_obj = self.pool.get('product.product')
1272 return {'value': {'product_uom': product_uos,
1273 'product_uom_qty': product_uos_qty}, 'domain': {}}
1275 product = product_obj.browse(cr, uid, product_id)
1277 'product_uom': product.uom_id.id,
1279 # FIXME must depend on uos/uom of the product and not only of the coeff.
1282 'product_uom_qty': product_uos_qty / product.uos_coeff,
1283 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1285 except ZeroDivisionError:
1287 return {'value': value}
1289 def copy_data(self, cr, uid, id, default=None, context=None):
1292 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1293 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1295 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1296 partner_id=False, packaging=False, flag=False, context=None):
1298 return {'value': {'product_packaging': False}}
1299 product_obj = self.pool.get('product.product')
1300 product_uom_obj = self.pool.get('product.uom')
1301 pack_obj = self.pool.get('product.packaging')
1306 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1307 product=product, qty=qty, uom=uom, partner_id=partner_id,
1308 packaging=packaging, flag=False, context=context)
1309 warning_msgs = res.get('warning') and res['warning']['message']
1311 products = product_obj.browse(cr, uid, product, context=context)
1312 if not products.packaging:
1313 packaging = result['product_packaging'] = False
1314 elif not packaging and products.packaging and not flag:
1315 packaging = products.packaging[0].id
1316 result['product_packaging'] = packaging
1319 default_uom = products.uom_id and products.uom_id.id
1320 pack = pack_obj.browse(cr, uid, packaging, context=context)
1321 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1322 # qty = qty - qty % q + q
1323 if qty and (q and not (qty % q) == 0):
1324 ean = pack.ean or _('(n/a)')
1327 if not warning_msgs:
1328 warn_msg = _("You selected a quantity of %d Units.\n"
1329 "But it's not compatible with the selected packaging.\n"
1330 "Here is a proposition of quantities according to the packaging:\n"
1331 "EAN: %s Quantity: %s Type of ul: %s") % \
1332 (qty, ean, qty_pack, type_ul.name)
1333 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1335 'title': _('Configuration Error!'),
1336 'message': warning_msgs
1338 result['product_uom_qty'] = qty
1340 return {'value': result, 'warning': warning}
1342 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1343 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1344 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1345 context = context or {}
1346 lang = lang or context.get('lang',False)
1348 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
1350 product_uom_obj = self.pool.get('product.uom')
1351 partner_obj = self.pool.get('res.partner')
1352 product_obj = self.pool.get('product.product')
1353 context = {'lang': lang, 'partner_id': partner_id}
1355 lang = partner_obj.browse(cr, uid, partner_id).lang
1356 context_partner = {'lang': lang, 'partner_id': partner_id}
1359 return {'value': {'th_weight': 0, 'product_packaging': False,
1360 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1363 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1365 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1366 result = res.get('value', {})
1367 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1368 product_obj = product_obj.browse(cr, uid, product, context=context)
1372 uom2 = product_uom_obj.browse(cr, uid, uom)
1373 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1376 if product_obj.uos_id:
1377 uos2 = product_uom_obj.browse(cr, uid, uos)
1378 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1382 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1383 if update_tax: #The quantity only have changed
1384 result['delay'] = (product_obj.sale_delay or 0.0)
1385 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1386 result.update({'type': product_obj.procure_method})
1389 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1390 if product_obj.description_sale:
1391 result['name'] += '\n'+product_obj.description_sale
1393 if (not uom) and (not uos):
1394 result['product_uom'] = product_obj.uom_id.id
1395 if product_obj.uos_id:
1396 result['product_uos'] = product_obj.uos_id.id
1397 result['product_uos_qty'] = qty * product_obj.uos_coeff
1398 uos_category_id = product_obj.uos_id.category_id.id
1400 result['product_uos'] = False
1401 result['product_uos_qty'] = qty
1402 uos_category_id = False
1403 result['th_weight'] = qty * product_obj.weight
1404 domain = {'product_uom':
1405 [('category_id', '=', product_obj.uom_id.category_id.id)],
1407 [('category_id', '=', uos_category_id)]}
1408 elif uos and not uom: # only happens if uom is False
1409 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1410 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1411 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1412 elif uom: # whether uos is set or not
1413 default_uom = product_obj.uom_id and product_obj.uom_id.id
1414 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1415 if product_obj.uos_id:
1416 result['product_uos'] = product_obj.uos_id.id
1417 result['product_uos_qty'] = qty * product_obj.uos_coeff
1419 result['product_uos'] = False
1420 result['product_uos_qty'] = qty
1421 result['th_weight'] = q * product_obj.weight # Round the quantity up
1424 uom2 = product_obj.uom_id
1425 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1426 if (product_obj.type=='product') and int(compare_qty) == -1 \
1427 and (product_obj.procure_method=='make_to_stock'):
1428 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1429 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1430 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1431 max(0,product_obj.qty_available), product_obj.uom_id.name)
1432 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1436 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1437 'Please set one before choosing a product.')
1438 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1440 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1441 product, qty or 1.0, partner_id, {
1442 'uom': uom or result.get('product_uom'),
1446 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1447 "You have to change either the product, the quantity or the pricelist.")
1449 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1451 result.update({'price_unit': price})
1454 'title': _('Configuration Error!'),
1455 'message' : warning_msgs
1457 return {'value': result, 'domain': domain, 'warning': warning}
1459 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1460 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1461 lang=False, update_tax=True, date_order=False, context=None):
1462 context = context or {}
1463 lang = lang or ('lang' in context and context['lang'])
1465 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1466 return self.product_id_change(cursor, user, ids, pricelist, product,
1467 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1468 partner_id=partner_id, lang=lang, update_tax=update_tax,
1469 date_order=date_order, context=context)
1471 def unlink(self, cr, uid, ids, context=None):
1474 """Allows to delete sales order lines in draft,cancel states"""
1475 for rec in self.browse(cr, uid, ids, context=context):
1476 if rec.state not in ['draft', 'cancel']:
1477 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1478 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1482 class mail_message(osv.osv):
1483 _inherit = 'mail.message'
1485 def _postprocess_sent_message(self, cr, uid, message, context=None):
1486 if message.model == 'sale.order':
1487 wf_service = netsvc.LocalService("workflow")
1488 wf_service.trg_validate(uid, 'sale.order', message.res_id, 'quotation_sent', cr)
1489 return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1493 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: