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')
556 act_obj = self.pool.get('ir.actions.act_window')
558 result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree')
559 id = result and result[1] or False
560 result = act_obj.read(cr, uid, [id], context=context)[0]
561 #compute the number of delivery orders to display
563 for so in self.browse(cr, uid, ids, context=context):
564 pick_ids += [picking.id for picking in so.picking_ids]
565 #choose the view_mode accordingly
566 if len(pick_ids) > 1:
567 result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
569 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
570 result['views'] = [(res and res[1] or False, 'form')]
571 result['res_id'] = pick_ids and pick_ids[0] or False
574 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
578 picking_obj = self.pool.get('stock.picking')
579 invoice = self.pool.get('account.invoice')
580 obj_sale_order_line = self.pool.get('sale.order.line')
581 partner_currency = {}
584 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
585 # last day of the last month as invoice date
587 context['date_inv'] = date_inv
588 for o in self.browse(cr, uid, ids, context=context):
589 currency_id = o.pricelist_id.currency_id.id
590 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
591 raise osv.except_osv(
593 _('You cannot group sales having different currencies for the same partner.'))
595 partner_currency[o.partner_id.id] = currency_id
597 for line in o.order_line:
600 elif (line.state in states):
601 lines.append(line.id)
602 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
604 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
606 for o in self.browse(cr, uid, ids, context=context):
607 for i in o.invoice_ids:
608 if i.state == 'draft':
610 for val in invoices.values():
612 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
615 invoice_ref += o.name + '|'
616 self.write(cr, uid, [o.id], {'state': 'progress'})
617 if o.order_policy == 'picking':
618 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
619 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
620 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
622 for order, il in val:
623 res = self._make_invoice(cr, uid, order, il, context=context)
624 invoice_ids.append(res)
625 self.write(cr, uid, [order.id], {'state': 'progress'})
626 if order.order_policy == 'picking':
627 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
628 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
630 self.invoice_send_note(cr, uid, ids, res, context)
633 def action_invoice_cancel(self, cr, uid, ids, context=None):
636 for sale in self.browse(cr, uid, ids, context=context):
637 for line in sale.order_line:
639 # Check if the line is invoiced (has asociated invoice
640 # lines from non-cancelled invoices).
643 for iline in line.invoice_lines:
644 if iline.invoice_id and iline.invoice_id.state != 'cancel':
647 # Update the line (only when needed)
648 if line.invoiced != invoiced:
649 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
650 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
653 def action_invoice_end(self, cr, uid, ids, context=None):
654 for order in self.browse(cr, uid, ids, context=context):
656 # Update the sale order lines state (and invoiced flag).
658 for line in order.order_line:
661 # Check if the line is invoiced (has asociated invoice
662 # lines from non-cancelled invoices).
665 for iline in line.invoice_lines:
666 if iline.invoice_id and iline.invoice_id.state != 'cancel':
669 if line.invoiced != invoiced:
670 vals['invoiced'] = invoiced
671 # If the line was in exception state, now it gets confirmed.
672 if line.state == 'exception':
673 vals['state'] = 'confirmed'
674 # Update the line (only when needed).
676 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
678 # Update the sales order state.
680 if order.state == 'invoice_except':
681 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
684 def action_cancel(self, cr, uid, ids, context=None):
685 wf_service = netsvc.LocalService("workflow")
688 sale_order_line_obj = self.pool.get('sale.order.line')
689 proc_obj = self.pool.get('procurement.order')
690 for sale in self.browse(cr, uid, ids, context=context):
691 for pick in sale.picking_ids:
692 if pick.state not in ('draft', 'cancel'):
693 raise osv.except_osv(
694 _('Cannot cancel sales order!'),
695 _('You must first cancel all delivery order(s) attached to this sales order.'))
696 if pick.state == 'cancel':
697 for mov in pick.move_lines:
698 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
700 for proc in proc_ids:
701 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
702 for r in self.read(cr, uid, ids, ['picking_ids']):
703 for pick in r['picking_ids']:
704 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
705 for inv in sale.invoice_ids:
706 if inv.state not in ('draft', 'cancel'):
707 raise osv.except_osv(
708 _('Cannot cancel this sales order!'),
709 _('First cancel all invoices attached to this sales order.'))
710 for r in self.read(cr, uid, ids, ['invoice_ids']):
711 for inv in r['invoice_ids']:
712 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
713 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
715 self.cancel_send_note(cr, uid, [sale.id], context=None)
716 self.write(cr, uid, ids, {'state': 'cancel'})
719 def action_button_confirm(self, cr, uid, ids, context=None):
720 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
721 wf_service = netsvc.LocalService('workflow')
722 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
724 # redisplay the record as a sale order
725 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
726 view_id = view_ref and view_ref[1] or False,
728 'type': 'ir.actions.act_window',
729 'name': _('Sales Order'),
730 'res_model': 'sale.order',
739 def action_wait(self, cr, uid, ids, context=None):
740 for o in self.browse(cr, uid, ids):
742 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
743 if (o.order_policy == 'manual'):
744 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
746 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
747 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
748 self.confirm_send_note(cr, uid, ids, context)
751 def action_quotation_send(self, cr, uid, ids, context=None):
753 This function opens a window to compose an email, with the edi sale template message loaded by default
755 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
756 mod_obj = self.pool.get('ir.model.data')
757 template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
758 template_id = template and template[1] or False
759 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
760 res_id = res and res[1] or False
761 ctx = dict(context, active_model='sale.order', active_id=ids[0])
762 ctx.update({'mail.compose.template_id': template_id})
766 'res_model': 'mail.compose.message',
767 'views': [(res_id,'form')],
769 'type': 'ir.actions.act_window',
775 def procurement_lines_get(self, cr, uid, ids, *args):
777 for order in self.browse(cr, uid, ids, context={}):
778 for line in order.order_line:
779 if line.procurement_id:
780 res.append(line.procurement_id.id)
783 # if mode == 'finished':
784 # returns True if all lines are done, False otherwise
785 # if mode == 'canceled':
786 # returns True if there is at least one canceled line, False otherwise
787 def test_state(self, cr, uid, ids, mode, *args):
788 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
793 write_cancel_ids = []
794 for order in self.browse(cr, uid, ids, context={}):
795 for line in order.order_line:
796 if (not line.procurement_id) or (line.procurement_id.state=='done'):
797 if line.state != 'done':
798 write_done_ids.append(line.id)
801 if line.procurement_id:
802 if (line.procurement_id.state == 'cancel'):
804 if line.state != 'exception':
805 write_cancel_ids.append(line.id)
809 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
811 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
813 if mode == 'finished':
815 elif mode == 'canceled':
821 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
823 'name': line.name.split('\n')[0],
824 'origin': order.name,
825 'date_planned': date_planned,
826 'product_id': line.product_id.id,
827 'product_qty': line.product_uom_qty,
828 'product_uom': line.product_uom.id,
829 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
830 or line.product_uom_qty,
831 'product_uos': (line.product_uos and line.product_uos.id)\
832 or line.product_uom.id,
833 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
834 'procure_method': line.type,
836 'company_id': order.company_id.id,
837 'note': '\n'.join(line.name.split('\n')[1:]),
838 'property_ids': [(6, 0, [x.id for x in line.property_ids])]
841 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
842 location_id = order.shop_id.warehouse_id.lot_stock_id.id
843 output_id = order.shop_id.warehouse_id.lot_output_id.id
845 'name': line.name.split('\n')[0][:250],
846 'picking_id': picking_id,
847 'product_id': line.product_id.id,
848 'date': date_planned,
849 'date_expected': date_planned,
850 'product_qty': line.product_uom_qty,
851 'product_uom': line.product_uom.id,
852 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
853 'product_uos': (line.product_uos and line.product_uos.id)\
854 or line.product_uom.id,
855 'product_packaging': line.product_packaging.id,
856 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
857 'location_id': location_id,
858 'location_dest_id': output_id,
859 'sale_line_id': line.id,
860 'tracking_id': False,
863 'note': '\n'.join(line.name.split('\n')[1:]),
864 'company_id': order.company_id.id,
865 'price_unit': line.product_id.standard_price or 0.0
868 def _prepare_order_picking(self, cr, uid, order, context=None):
869 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
872 'origin': order.name,
873 'date': order.date_order,
876 'move_type': order.picking_policy,
878 'partner_id': order.partner_shipping_id.id,
880 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
881 'company_id': order.company_id.id,
884 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
885 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
887 Define ship_recreate for process after shipping exception
888 param order: sale order to which the order lines belong
889 param line: sale order line records to procure
890 param move_id: the ID of stock move
891 param proc_id: the ID of procurement
893 move_obj = self.pool.get('stock.move')
894 if order.state == 'shipping_except':
895 for pick in order.picking_ids:
896 for move in pick.move_lines:
897 if move.state == 'cancel':
898 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
900 for mov in move_obj.browse(cr, uid, mov_ids):
901 # 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?
902 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
903 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
906 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
907 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
908 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
911 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
912 """Create the required procurements to supply sale order lines, also connecting
913 the procurements to appropriate stock moves in order to bring the goods to the
914 sale order's requested location.
916 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
917 a standard outgoing picking will be created to wrap the stock moves, as returned
918 by :meth:`~._prepare_order_picking`.
920 Modules that wish to customize the procurements or partition the stock moves over
921 multiple stock pickings may override this method and call ``super()`` with
922 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
924 :param browse_record order: sale order to which the order lines belong
925 :param list(browse_record) order_lines: sale order line records to procure
926 :param int picking_id: optional ID of a stock picking to which the created stock moves
927 will be added. A new picking will be created if ommitted.
930 move_obj = self.pool.get('stock.move')
931 picking_obj = self.pool.get('stock.picking')
932 procurement_obj = self.pool.get('procurement.order')
935 for line in order_lines:
936 if line.state == 'done':
939 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
942 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
944 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
945 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
947 # a service has no stock move
950 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
951 proc_ids.append(proc_id)
952 line.write({'procurement_id': proc_id})
953 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
955 wf_service = netsvc.LocalService("workflow")
957 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
958 self.delivery_send_note(cr, uid, [order.id], picking_id, context)
961 for proc_id in proc_ids:
962 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
965 if order.state == 'shipping_except':
966 val['state'] = 'progress'
967 val['shipped'] = False
969 if (order.order_policy == 'manual'):
970 for line in order.order_line:
971 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
972 val['state'] = 'manual'
977 def action_ship_create(self, cr, uid, ids, context=None):
978 for order in self.browse(cr, uid, ids, context=context):
979 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
982 def action_ship_end(self, cr, uid, ids, context=None):
983 for order in self.browse(cr, uid, ids, context=context):
984 val = {'shipped': True}
985 if order.state == 'shipping_except':
986 val['state'] = 'progress'
987 if (order.order_policy == 'manual'):
988 for line in order.order_line:
989 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
990 val['state'] = 'manual'
992 for line in order.order_line:
994 if line.state == 'exception':
995 towrite.append(line.id)
997 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
998 res = self.write(cr, uid, [order.id], val)
1000 self.delivery_end_send_note(cr, uid, [order.id], context=context)
1003 def has_stockable_products(self, cr, uid, ids, *args):
1004 for order in self.browse(cr, uid, ids):
1005 for order_line in order.order_line:
1006 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
1010 # ------------------------------------------------
1011 # OpenChatter methods and notifications
1012 # ------------------------------------------------
1014 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1015 result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
1016 for obj in self.browse(cr, uid, ids, context=context):
1017 if (obj.state == 'manual' or obj.state == 'progress'):
1018 result[obj.id].append(obj.user_id.id)
1021 def create_send_note(self, cr, uid, ids, context=None):
1022 for obj in self.browse(cr, uid, ids, context=context):
1023 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)
1025 def confirm_send_note(self, cr, uid, ids, context=None):
1026 for obj in self.browse(cr, uid, ids, context=context):
1027 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)
1029 def cancel_send_note(self, cr, uid, ids, context=None):
1030 for obj in self.browse(cr, uid, ids, context=context):
1031 self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
1033 def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
1034 for order in self.browse(cr, uid, ids, context=context):
1035 for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
1036 # convert datetime field to a datetime, using server format, then
1037 # convert it to the user TZ and re-render it with %Z to add the timezone
1038 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
1039 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
1040 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)
1042 def delivery_end_send_note(self, cr, uid, ids, context=None):
1043 self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
1045 def invoice_paid_send_note(self, cr, uid, ids, context=None):
1046 self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
1048 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
1049 for order in self.browse(cr, uid, ids, context=context):
1050 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
1051 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)
1053 def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
1054 return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
1059 # TODO add a field price_unit_uos
1060 # - update it on change product and unit price
1061 # - use it in report if there is a uos
1062 class sale_order_line(osv.osv):
1064 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
1065 tax_obj = self.pool.get('account.tax')
1066 cur_obj = self.pool.get('res.currency')
1070 for line in self.browse(cr, uid, ids, context=context):
1071 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1072 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
1073 cur = line.order_id.pricelist_id.currency_id
1074 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
1077 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
1079 for line in self.browse(cr, uid, ids, context=context):
1081 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
1086 def _get_uom_id(self, cr, uid, *args):
1088 proxy = self.pool.get('ir.model.data')
1089 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
1091 except Exception, ex:
1094 _name = 'sale.order.line'
1095 _description = 'Sales Order Line'
1097 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1098 'name': fields.text('Product Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1099 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1100 '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)]}),
1101 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1102 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1103 'invoiced': fields.boolean('Invoiced', readonly=True),
1104 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1105 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
1106 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
1107 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1108 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1109 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."),
1110 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1111 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1112 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1113 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1114 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1115 'product_uos': fields.many2one('product.uom', 'Product UoS'),
1116 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1117 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1118 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
1119 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1120 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1121 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
1122 help='* The \'Draft\' state is set when the related sales order in draft state. \
1123 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1124 \n* The \'Exception\' state is set when the related sales order is set as exception. \
1125 \n* The \'Done\' state is set when the sales order line has been picked. \
1126 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1127 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1128 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
1129 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1131 _order = 'sequence, id'
1133 'product_uom' : _get_uom_id,
1136 'product_uom_qty': 1,
1137 'product_uos_qty': 1,
1141 'type': 'make_to_stock',
1142 'product_packaging': False,
1146 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1147 """Prepare the dict of values to create the new invoice line for a
1148 sale order line. This method may be overridden to implement custom
1149 invoice generation (making sure to call super() to establish
1150 a clean extension chain).
1152 :param browse_record line: sale.order.line record to invoice
1153 :param int account_id: optional ID of a G/L account to force
1154 (this is used for returning products including service)
1155 :return: dict of values to create() the invoice line
1158 def _get_line_qty(line):
1159 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1160 if line.product_uos:
1161 return line.product_uos_qty or 0.0
1162 return line.product_uom_qty
1164 return self.pool.get('procurement.order').quantity_get(cr, uid,
1165 line.procurement_id.id, context=context)
1167 def _get_line_uom(line):
1168 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1169 if line.product_uos:
1170 return line.product_uos.id
1171 return line.product_uom.id
1173 return self.pool.get('procurement.order').uom_get(cr, uid,
1174 line.procurement_id.id, context=context)
1176 if not line.invoiced:
1179 account_id = line.product_id.product_tmpl_id.property_account_income.id
1181 account_id = line.product_id.categ_id.property_account_income_categ.id
1183 raise osv.except_osv(_('Error!'),
1184 _('Please define income account for this product: "%s" (id:%d).') % \
1185 (line.product_id.name, line.product_id.id,))
1187 prop = self.pool.get('ir.property').get(cr, uid,
1188 'property_account_income_categ', 'product.category',
1190 account_id = prop and prop.id or False
1191 uosqty = _get_line_qty(line)
1192 uos_id = _get_line_uom(line)
1195 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1196 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
1197 fpos = line.order_id.fiscal_position or False
1198 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1200 raise osv.except_osv(_('Error!'),
1201 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
1204 'origin': line.order_id.name,
1205 'account_id': account_id,
1208 'discount': line.discount,
1210 'product_id': line.product_id.id or False,
1211 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1212 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1217 def invoice_line_create(self, cr, uid, ids, context=None):
1223 for line in self.browse(cr, uid, ids, context=context):
1224 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1226 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1227 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1228 self.write(cr, uid, [line.id], {'invoiced': True})
1229 sales.add(line.order_id.id)
1230 create_ids.append(inv_id)
1231 # Trigger workflow events
1232 wf_service = netsvc.LocalService("workflow")
1233 for sale_id in sales:
1234 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1237 def button_cancel(self, cr, uid, ids, context=None):
1238 for line in self.browse(cr, uid, ids, context=context):
1240 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
1241 for move_line in line.move_ids:
1242 if move_line.state != 'cancel':
1243 raise osv.except_osv(
1244 _('Cannot cancel sales order line!'),
1245 _('You must first cancel stock moves attached to this sales order line.'))
1246 return self.write(cr, uid, ids, {'state': 'cancel'})
1248 def button_confirm(self, cr, uid, ids, context=None):
1249 return self.write(cr, uid, ids, {'state': 'confirmed'})
1251 def button_done(self, cr, uid, ids, context=None):
1252 wf_service = netsvc.LocalService("workflow")
1253 res = self.write(cr, uid, ids, {'state': 'done'})
1254 for line in self.browse(cr, uid, ids, context=context):
1255 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1258 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1259 product_obj = self.pool.get('product.product')
1261 return {'value': {'product_uom': product_uos,
1262 'product_uom_qty': product_uos_qty}, 'domain': {}}
1264 product = product_obj.browse(cr, uid, product_id)
1266 'product_uom': product.uom_id.id,
1268 # FIXME must depend on uos/uom of the product and not only of the coeff.
1271 'product_uom_qty': product_uos_qty / product.uos_coeff,
1272 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1274 except ZeroDivisionError:
1276 return {'value': value}
1278 def copy_data(self, cr, uid, id, default=None, context=None):
1281 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1282 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1284 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1285 partner_id=False, packaging=False, flag=False, context=None):
1287 return {'value': {'product_packaging': False}}
1288 product_obj = self.pool.get('product.product')
1289 product_uom_obj = self.pool.get('product.uom')
1290 pack_obj = self.pool.get('product.packaging')
1295 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1296 product=product, qty=qty, uom=uom, partner_id=partner_id,
1297 packaging=packaging, flag=False, context=context)
1298 warning_msgs = res.get('warning') and res['warning']['message']
1300 products = product_obj.browse(cr, uid, product, context=context)
1301 if not products.packaging:
1302 packaging = result['product_packaging'] = False
1303 elif not packaging and products.packaging and not flag:
1304 packaging = products.packaging[0].id
1305 result['product_packaging'] = packaging
1308 default_uom = products.uom_id and products.uom_id.id
1309 pack = pack_obj.browse(cr, uid, packaging, context=context)
1310 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1311 # qty = qty - qty % q + q
1312 if qty and (q and not (qty % q) == 0):
1313 ean = pack.ean or _('(n/a)')
1316 if not warning_msgs:
1317 warn_msg = _("You selected a quantity of %d Units.\n"
1318 "But it's not compatible with the selected packaging.\n"
1319 "Here is a proposition of quantities according to the packaging:\n"
1320 "EAN: %s Quantity: %s Type of ul: %s") % \
1321 (qty, ean, qty_pack, type_ul.name)
1322 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1324 'title': _('Configuration Error!'),
1325 'message': warning_msgs
1327 result['product_uom_qty'] = qty
1329 return {'value': result, 'warning': warning}
1331 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1332 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1333 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1334 context = context or {}
1335 lang = lang or context.get('lang',False)
1337 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
1339 product_uom_obj = self.pool.get('product.uom')
1340 partner_obj = self.pool.get('res.partner')
1341 product_obj = self.pool.get('product.product')
1342 context = {'lang': lang, 'partner_id': partner_id}
1344 lang = partner_obj.browse(cr, uid, partner_id).lang
1345 context_partner = {'lang': lang, 'partner_id': partner_id}
1348 return {'value': {'th_weight': 0, 'product_packaging': False,
1349 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1352 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1354 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1355 result = res.get('value', {})
1356 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1357 product_obj = product_obj.browse(cr, uid, product, context=context)
1361 uom2 = product_uom_obj.browse(cr, uid, uom)
1362 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1365 if product_obj.uos_id:
1366 uos2 = product_uom_obj.browse(cr, uid, uos)
1367 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1371 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1372 if update_tax: #The quantity only have changed
1373 result['delay'] = (product_obj.sale_delay or 0.0)
1374 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1375 result.update({'type': product_obj.procure_method})
1378 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1379 if product_obj.description_sale:
1380 result['name'] += '\n'+product_obj.description_sale
1382 if (not uom) and (not uos):
1383 result['product_uom'] = product_obj.uom_id.id
1384 if product_obj.uos_id:
1385 result['product_uos'] = product_obj.uos_id.id
1386 result['product_uos_qty'] = qty * product_obj.uos_coeff
1387 uos_category_id = product_obj.uos_id.category_id.id
1389 result['product_uos'] = False
1390 result['product_uos_qty'] = qty
1391 uos_category_id = False
1392 result['th_weight'] = qty * product_obj.weight
1393 domain = {'product_uom':
1394 [('category_id', '=', product_obj.uom_id.category_id.id)],
1396 [('category_id', '=', uos_category_id)]}
1397 elif uos and not uom: # only happens if uom is False
1398 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1399 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1400 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1401 elif uom: # whether uos is set or not
1402 default_uom = product_obj.uom_id and product_obj.uom_id.id
1403 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1404 if product_obj.uos_id:
1405 result['product_uos'] = product_obj.uos_id.id
1406 result['product_uos_qty'] = qty * product_obj.uos_coeff
1408 result['product_uos'] = False
1409 result['product_uos_qty'] = qty
1410 result['th_weight'] = q * product_obj.weight # Round the quantity up
1413 uom2 = product_obj.uom_id
1414 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1415 if (product_obj.type=='product') and int(compare_qty) == -1 \
1416 and (product_obj.procure_method=='make_to_stock'):
1417 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1418 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1419 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1420 max(0,product_obj.qty_available), product_obj.uom_id.name)
1421 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1425 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1426 'Please set one before choosing a product.')
1427 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1429 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1430 product, qty or 1.0, partner_id, {
1431 'uom': uom or result.get('product_uom'),
1435 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1436 "You have to change either the product, the quantity or the pricelist.")
1438 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1440 result.update({'price_unit': price})
1443 'title': _('Configuration Error!'),
1444 'message' : warning_msgs
1446 return {'value': result, 'domain': domain, 'warning': warning}
1448 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1449 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1450 lang=False, update_tax=True, date_order=False, context=None):
1451 context = context or {}
1452 lang = lang or ('lang' in context and context['lang'])
1454 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1455 return self.product_id_change(cursor, user, ids, pricelist, product,
1456 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1457 partner_id=partner_id, lang=lang, update_tax=update_tax,
1458 date_order=date_order, context=context)
1460 def unlink(self, cr, uid, ids, context=None):
1463 """Allows to delete sales order lines in draft,cancel states"""
1464 for rec in self.browse(cr, uid, ids, context=context):
1465 if rec.state not in ['draft', 'cancel']:
1466 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1467 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1471 class mail_message(osv.osv):
1472 _inherit = 'mail.message'
1474 def _postprocess_sent_message(self, cr, uid, message, context=None):
1475 if message.model == 'sale.order':
1476 wf_service = netsvc.LocalService("workflow")
1477 wf_service.trg_validate(uid, 'sale.order', message.res_id, 'quotation_sent', cr)
1478 return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1482 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: