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
24 from openerp.osv import fields, osv
25 from openerp.tools.translate import _
26 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
27 import openerp.addons.decimal_precision as dp
28 from openerp import workflow
30 class sale_order(osv.osv):
32 _inherit = ['mail.thread', 'ir.needaction_mixin']
33 _description = "Sales Order"
36 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
37 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
41 def copy(self, cr, uid, id, default=None, context=None):
45 'date_order': fields.datetime.now(),
48 'date_confirm': False,
49 'client_order_ref': '',
50 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
51 'procurement_group_id': False,
53 return super(sale_order, self).copy(cr, uid, id, default, context=context)
55 def _amount_line_tax(self, cr, uid, line, context=None):
57 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']:
58 val += c.get('amount', 0.0)
61 def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
62 """ Wrapper because of direct method passing as parameter for function fields """
63 return self._amount_all(cr, uid, ids, field_name, arg, context=context)
65 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
66 cur_obj = self.pool.get('res.currency')
68 for order in self.browse(cr, uid, ids, context=context):
70 'amount_untaxed': 0.0,
75 cur = order.pricelist_id.currency_id
76 for line in order.order_line:
77 val1 += line.price_subtotal
78 val += self._amount_line_tax(cr, uid, line, context=context)
79 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
80 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
81 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
85 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
87 for sale in self.browse(cursor, user, ids, context=context):
92 for invoice in sale.invoice_ids:
93 if invoice.state not in ('draft', 'cancel'):
94 tot += invoice.amount_untaxed
96 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
101 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
103 for sale in self.browse(cursor, user, ids, context=context):
109 def _invoiced(self, cursor, user, ids, name, arg, context=None):
111 for sale in self.browse(cursor, user, ids, context=context):
113 invoice_existence = False
114 for invoice in sale.invoice_ids:
115 if invoice.state!='cancel':
116 invoice_existence = True
117 if invoice.state != 'paid':
120 if not invoice_existence or sale.state == 'manual':
124 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
131 if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
132 clause += 'AND inv.state = \'paid\''
134 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
135 sale_clause = ', sale_order AS sale '
138 cursor.execute('SELECT rel.order_id ' \
139 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
140 'WHERE rel.invoice_id = inv.id ' + clause)
141 res = cursor.fetchall()
143 cursor.execute('SELECT sale.id ' \
144 'FROM sale_order AS sale ' \
145 'WHERE sale.id NOT IN ' \
146 '(SELECT rel.order_id ' \
147 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
148 res.extend(cursor.fetchall())
150 return [('id', '=', 0)]
151 return [('id', 'in', [x[0] for x in res])]
153 def _get_order(self, cr, uid, ids, context=None):
155 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
156 result[line.order_id.id] = True
159 def _get_default_company(self, cr, uid, context=None):
160 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
162 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
165 def _get_default_section_id(self, cr, uid, context=None):
166 """ Gives default section by checking if present in the context """
167 section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
169 section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
172 def _resolve_section_id_from_context(self, cr, uid, context=None):
173 """ Returns ID of section based on the value of 'section_id'
174 context key, or None if it cannot be resolved to a single
179 if type(context.get('default_section_id')) in (int, long):
180 return context.get('default_section_id')
181 if isinstance(context.get('default_section_id'), basestring):
182 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
183 if len(section_ids) == 1:
184 return int(section_ids[0][0])
188 'name': fields.char('Order Reference', size=64, required=True,
189 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
190 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
191 'client_order_ref': fields.char('Reference/Description', size=64),
192 'state': fields.selection([
193 ('draft', 'Draft Quotation'),
194 ('sent', 'Quotation Sent'),
195 ('cancel', 'Cancelled'),
196 ('waiting_date', 'Waiting Schedule'),
197 ('progress', 'Sales Order'),
198 ('manual', 'Sale to Invoice'),
199 ('shipping_except', 'Shipping Exception'),
200 ('invoice_except', 'Invoice Exception'),
202 ], 'Status', readonly=True, help="Gives the status of the quotation or sales order.\
203 \nThe exception status is automatically set when a cancel operation occurs \
204 in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
205 but waiting for the scheduler to run on the order date.", select=True),
206 'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
207 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
208 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
209 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
210 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'),
211 '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."),
212 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."),
213 'order_policy': fields.selection([
214 ('manual', 'On Demand'),
215 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
216 help="""This field controls how invoice and delivery operations are synchronized."""),
217 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
218 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
219 'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
221 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
222 '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)."),
223 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
224 'invoiced': fields.function(_invoiced, string='Paid',
225 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
226 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
227 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
228 'note': fields.text('Terms and conditions'),
230 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
232 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
233 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
235 multi='sums', help="The amount without tax.", track_visibility='always'),
236 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
238 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
239 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
241 multi='sums', help="The tax amount."),
242 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
244 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
245 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
247 multi='sums', help="The total amount."),
249 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
250 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
251 'company_id': fields.many2one('res.company', 'Company'),
252 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
253 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group'),
257 'date_order': fields.datetime.now,
258 'order_policy': 'manual',
259 'company_id': _get_default_company,
261 'user_id': lambda obj, cr, uid, context: uid,
262 'name': lambda obj, cr, uid, context: '/',
263 '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'],
264 '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'],
265 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note,
266 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
269 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
271 _order = 'date_order desc, id desc'
274 def unlink(self, cr, uid, ids, context=None):
275 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
277 for s in sale_orders:
278 if s['state'] in ['draft', 'cancel']:
279 unlink_ids.append(s['id'])
281 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
283 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
285 def copy_quotation(self, cr, uid, ids, context=None):
286 id = self.copy(cr, uid, ids[0], context=context)
287 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
288 view_id = view_ref and view_ref[1] or False,
290 'type': 'ir.actions.act_window',
291 'name': _('Sales Order'),
292 'res_model': 'sale.order',
301 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
302 context = context or {}
306 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
309 return {'value': value}
311 'title': _('Pricelist Warning!'),
312 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
314 return {'warning': warning, 'value': value}
316 def get_salenote(self, cr, uid, ids, partner_id, context=None):
317 context_lang = context.copy()
319 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
320 context_lang.update({'lang': partner_lang})
321 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
323 def onchange_partner_id(self, cr, uid, ids, part, context=None):
325 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
327 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
328 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
329 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
330 payment_term = part.property_payment_term and part.property_payment_term.id or False
331 fiscal_position = part.property_account_position and part.property_account_position.id or False
332 dedicated_salesman = part.user_id and part.user_id.id or uid
334 'partner_invoice_id': addr['invoice'],
335 'partner_shipping_id': addr['delivery'],
336 'payment_term': payment_term,
337 'fiscal_position': fiscal_position,
338 'user_id': dedicated_salesman,
341 val['pricelist_id'] = pricelist
342 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
343 if sale_note: val.update({'note': sale_note})
344 return {'value': val}
346 def create(self, cr, uid, vals, context=None):
349 if vals.get('name', '/') == '/':
350 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
351 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
352 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value']
353 vals = dict(defaults, **vals)
354 context.update({'mail_create_nolog': True})
355 new_id = super(sale_order, self).create(cr, uid, vals, context=context)
356 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context)
359 def button_dummy(self, cr, uid, ids, context=None):
362 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
363 # can be removed after 6.1.
364 def _inv_get(self, cr, uid, order, context=None):
367 def _prepare_invoice(self, cr, uid, order, lines, context=None):
368 """Prepare the dict of values to create the new invoice for a
369 sales order. This method may be overridden to implement custom
370 invoice generation (making sure to call super() to establish
371 a clean extension chain).
373 :param browse_record order: sale.order record to invoice
374 :param list(int) line: list of invoice line IDs that must be
375 attached to the invoice
376 :return: dict of value to create() the invoice
380 journal_ids = self.pool.get('account.journal').search(cr, uid,
381 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
384 raise osv.except_osv(_('Error!'),
385 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
387 'name': order.client_order_ref or '',
388 'origin': order.name,
389 'type': 'out_invoice',
390 'reference': order.client_order_ref or order.name,
391 'account_id': order.partner_id.property_account_receivable.id,
392 'partner_id': order.partner_invoice_id.id,
393 'journal_id': journal_ids[0],
394 'invoice_line': [(6, 0, lines)],
395 'currency_id': order.pricelist_id.currency_id.id,
396 'comment': order.note,
397 'payment_term': order.payment_term and order.payment_term.id or False,
398 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
399 'date_invoice': context.get('date_invoice', False),
400 'company_id': order.company_id.id,
401 'user_id': order.user_id and order.user_id.id or False,
402 'section_id' : order.section_id.id
405 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
406 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
409 def _make_invoice(self, cr, uid, order, lines, context=None):
410 inv_obj = self.pool.get('account.invoice')
411 obj_invoice_line = self.pool.get('account.invoice.line')
414 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
415 from_line_invoice_ids = []
416 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
417 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
418 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
419 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
420 for preinv in order.invoice_ids:
421 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
422 for preline in preinv.invoice_line:
423 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
424 lines.append(inv_line_id)
425 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
426 inv_id = inv_obj.create(cr, uid, inv, context=context)
427 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
428 if data.get('value', False):
429 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
430 inv_obj.button_compute(cr, uid, [inv_id])
433 def print_quotation(self, cr, uid, ids, context=None):
435 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
437 assert len(ids) == 1, 'This option should only be used for a single id at a time'
438 self.signal_quotation_sent(cr, uid, ids)
439 return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
441 def manual_invoice(self, cr, uid, ids, context=None):
442 """ create invoices for the given sales orders (ids), and open the form
443 view of one of the newly created invoices
445 mod_obj = self.pool.get('ir.model.data')
447 # create invoices through the sales orders' workflow
448 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
449 self.signal_manual_invoice(cr, uid, ids)
450 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
451 # determine newly created invoices
452 new_inv_ids = list(inv_ids1 - inv_ids0)
454 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
455 res_id = res and res[1] or False,
458 'name': _('Customer Invoices'),
462 'res_model': 'account.invoice',
463 'context': "{'type':'out_invoice'}",
464 'type': 'ir.actions.act_window',
467 'res_id': new_inv_ids and new_inv_ids[0] or False,
470 def action_view_invoice(self, cr, uid, ids, context=None):
472 This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
474 mod_obj = self.pool.get('ir.model.data')
475 act_obj = self.pool.get('ir.actions.act_window')
477 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
478 id = result and result[1] or False
479 result = act_obj.read(cr, uid, [id], context=context)[0]
480 #compute the number of invoices to display
482 for so in self.browse(cr, uid, ids, context=context):
483 inv_ids += [invoice.id for invoice in so.invoice_ids]
484 #choose the view_mode accordingly
486 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
488 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
489 result['views'] = [(res and res[1] or False, 'form')]
490 result['res_id'] = inv_ids and inv_ids[0] or False
493 def test_no_product(self, cr, uid, order, context):
494 for line in order.order_line:
495 if line.product_id and (line.product_id.type<>'service'):
499 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
501 states = ['confirmed', 'done', 'exception']
505 invoice = self.pool.get('account.invoice')
506 obj_sale_order_line = self.pool.get('sale.order.line')
507 partner_currency = {}
510 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
511 # last day of the last month as invoice date
513 context['date_invoice'] = date_invoice
514 for o in self.browse(cr, uid, ids, context=context):
515 currency_id = o.pricelist_id.currency_id.id
516 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
517 raise osv.except_osv(
519 _('You cannot group sales having different currencies for the same partner.'))
521 partner_currency[o.partner_id.id] = currency_id
523 for line in o.order_line:
526 elif (line.state in states):
527 lines.append(line.id)
528 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
530 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
532 for o in self.browse(cr, uid, ids, context=context):
533 for i in o.invoice_ids:
534 if i.state == 'draft':
536 for val in invoices.values():
538 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
542 invoice_ref += (o.client_order_ref or o.name) + '|'
543 origin_ref += (o.origin or o.name) + '|'
544 self.write(cr, uid, [o.id], {'state': 'progress'})
545 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
546 #remove last '|' in invoice_ref
547 if len(invoice_ref) >= 1:
548 invoice_ref = invoice_ref[:-1]
549 if len(origin_ref) >= 1:
550 origin_ref = origin_ref[:-1]
551 invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
553 for order, il in val:
554 res = self._make_invoice(cr, uid, order, il, context=context)
555 invoice_ids.append(res)
556 self.write(cr, uid, [order.id], {'state': 'progress'})
557 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
560 def action_invoice_cancel(self, cr, uid, ids, context=None):
561 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
564 def action_invoice_end(self, cr, uid, ids, context=None):
565 for this in self.browse(cr, uid, ids, context=context):
566 for line in this.order_line:
567 if line.state == 'exception':
568 line.write({'state': 'confirmed'})
569 if this.state == 'invoice_except':
570 this.write({'state': 'progress'})
573 def action_cancel(self, cr, uid, ids, context=None):
576 sale_order_line_obj = self.pool.get('sale.order.line')
577 account_invoice_obj = self.pool.get('account.invoice')
578 for sale in self.browse(cr, uid, ids, context=context):
579 for inv in sale.invoice_ids:
580 if inv.state not in ('draft', 'cancel'):
581 raise osv.except_osv(
582 _('Cannot cancel this sales order!'),
583 _('First cancel all invoices attached to this sales order.'))
584 for r in self.read(cr, uid, ids, ['invoice_ids']):
585 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
586 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
588 self.write(cr, uid, ids, {'state': 'cancel'})
591 def action_button_confirm(self, cr, uid, ids, context=None):
592 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
593 self.signal_order_confirm(cr, uid, ids)
595 # redisplay the record as a sales order
596 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
597 view_id = view_ref and view_ref[1] or False,
599 'type': 'ir.actions.act_window',
600 'name': _('Sales Order'),
601 'res_model': 'sale.order',
610 def action_wait(self, cr, uid, ids, context=None):
611 context = context or {}
612 for o in self.browse(cr, uid, ids):
614 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
615 noprod = self.test_no_product(cr, uid, o, context)
616 if (o.order_policy == 'manual') or noprod:
617 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
619 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
620 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
623 def action_quotation_send(self, cr, uid, ids, context=None):
625 This function opens a window to compose an email, with the edi sale template message loaded by default
627 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
628 ir_model_data = self.pool.get('ir.model.data')
630 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
634 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
636 compose_form_id = False
639 'default_model': 'sale.order',
640 'default_res_id': ids[0],
641 'default_use_template': bool(template_id),
642 'default_template_id': template_id,
643 'default_composition_mode': 'comment',
644 'mark_so_as_sent': True
647 'type': 'ir.actions.act_window',
650 'res_model': 'mail.compose.message',
651 'views': [(compose_form_id, 'form')],
652 'view_id': compose_form_id,
657 def action_done(self, cr, uid, ids, context=None):
658 for order in self.browse(cr, uid, ids, context=context):
659 self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
660 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
662 def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
663 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
666 'origin': order.name,
667 'date_planned': date_planned,
668 'product_id': line.product_id.id,
669 'product_qty': line.product_uom_qty,
670 'product_uom': line.product_uom.id,
671 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
672 'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
673 'company_id': order.company_id.id,
674 'group_id': group_id,
675 'invoice_state': (order.order_policy == 'picking') and '2binvoiced' or 'none',
676 'sale_line_id': line.id
679 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
680 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
683 def _prepare_procurement_group(self, cr, uid, order, context=None):
684 return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
686 def procurement_needed(self, cr, uid, ids, context=None):
687 #when sale is installed only, there is no need to create procurements, that's only
688 #further installed modules (project_mrp, sale_stock) that will change this.
689 sale_line_obj = self.pool.get('sale.order.line')
691 for order in self.browse(cr, uid, ids, context=context):
692 res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
695 def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
696 for sale_order in self.browse(cr, uid, ids, context=context):
697 self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
700 def action_ship_create(self, cr, uid, ids, context=None):
701 """Create the required procurements to supply sales order lines, also connecting
702 the procurements to appropriate stock moves in order to bring the goods to the
703 sales order's requested location.
707 procurement_obj = self.pool.get('procurement.order')
708 sale_line_obj = self.pool.get('sale.order.line')
709 for order in self.browse(cr, uid, ids, context=context):
711 vals = self._prepare_procurement_group(cr, uid, order, context=context)
712 if not order.procurement_group_id:
713 group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
714 order.write({'procurement_group_id': group_id}, context=context)
716 for line in order.order_line:
717 #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
718 if line.procurement_ids:
719 #first check them to see if they are in exception or not (one of the related moves is cancelled)
720 procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
722 #run again procurement that are in exception in order to trigger another move
723 proc_ids += [x.id for x in line.procurement_ids if x.state == 'exception']
724 elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
725 if (line.state == 'done') or not line.product_id:
727 vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
728 proc_id = procurement_obj.create(cr, uid, vals, context=context)
729 proc_ids.append(proc_id)
730 #Confirm procurement order such that rules will be applied on it
731 #note that the workflow normally ensure proc_ids isn't an empty list
732 procurement_obj.run(cr, uid, proc_ids, context=context)
734 #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
735 if order.state == 'shipping_except':
736 val = {'state': 'progress', 'shipped': False}
738 if (order.order_policy == 'manual'):
739 for line in order.order_line:
740 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
741 val['state'] = 'manual'
746 # if mode == 'finished':
747 # returns True if all lines are done, False otherwise
748 # if mode == 'canceled':
749 # returns True if there is at least one canceled line, False otherwise
750 def test_state(self, cr, uid, ids, mode, *args):
751 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
755 write_cancel_ids = []
756 for order in self.browse(cr, uid, ids, context={}):
758 #TODO: Need to rethink what happens when cancelling
759 for line in order.order_line:
760 states = [x.state for x in line.procurement_ids]
761 cancel = states and all([x == 'cancel' for x in states])
762 doneorcancel = all([x in ('done', 'cancel') for x in states])
765 if line.state != 'exception':
766 write_cancel_ids.append(line.id)
769 if doneorcancel and not cancel:
770 write_done_ids.append(line.id)
773 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
775 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
777 if mode == 'finished':
779 elif mode == 'canceled':
783 def procurement_lines_get(self, cr, uid, ids, *args):
785 for order in self.browse(cr, uid, ids, context={}):
786 for line in order.order_line:
787 res += [x.id for x in line.procurement_ids]
790 def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None):
791 '''Update taxes of order lines for each line where a product is defined
793 :param list ids: not used
794 :param int fiscal_position: sale order fiscal position
795 :param list order_lines: command list for one2many write method
798 fiscal_obj = self.pool.get('account.fiscal.position')
799 product_obj = self.pool.get('product.product')
800 line_obj = self.pool.get('sale.order.line')
804 fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context)
806 for line in order_lines:
807 # create (0, 0, { fields })
808 # update (1, ID, { fields })
809 if line[0] in [0, 1]:
811 if line[2].get('product_id'):
812 prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context)
814 prod = line_obj.browse(cr, uid, line[1], context=context).product_id
815 if prod and prod.taxes_id:
816 line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]
817 order_line.append(line)
820 # link all (6, 0, IDS)
821 elif line[0] in [4, 6]:
822 line_ids = line[0] == 4 and [line[1]] or line[2]
823 for line_id in line_ids:
824 prod = line_obj.browse(cr, uid, line_id, context=context).product_id
825 if prod and prod.taxes_id:
826 order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}])
828 order_line.append([4, line_id])
830 order_line.append(line)
831 return {'value': {'order_line': order_line}}
834 # TODO add a field price_unit_uos
835 # - update it on change product and unit price
836 # - use it in report if there is a uos
837 class sale_order_line(osv.osv):
839 def need_procurement(self, cr, uid, ids, context=None):
840 #when sale is installed only, there is no need to create procurements, that's only
841 #further installed modules (project_mrp, sale_stock) that will change this.
844 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
845 tax_obj = self.pool.get('account.tax')
846 cur_obj = self.pool.get('res.currency')
850 for line in self.browse(cr, uid, ids, context=context):
851 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
852 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
853 cur = line.order_id.pricelist_id.currency_id
854 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
857 def _get_uom_id(self, cr, uid, *args):
859 proxy = self.pool.get('ir.model.data')
860 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
862 except Exception, ex:
865 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
866 res = dict.fromkeys(ids, False)
867 for this in self.browse(cr, uid, ids, context=context):
868 res[this.id] = this.invoice_lines and \
869 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
872 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
873 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
874 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
875 sale_order_line sol ON (sol.order_id = rel.order_id)
876 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
877 return [i[0] for i in cr.fetchall()]
880 _name = 'sale.order.line'
881 _description = 'Sales Order Line'
883 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
884 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
885 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
886 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'),
887 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
888 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
890 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
891 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
893 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
894 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
895 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
896 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
897 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
898 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
899 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
900 'product_uos': fields.many2one('product.uom', 'Product UoS'),
901 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
902 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
903 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
904 help='* The \'Draft\' status is set when the related sales order in draft status. \
905 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
906 \n* The \'Exception\' status is set when the related sales order is set as exception. \
907 \n* The \'Done\' status is set when the sales order line has been picked. \
908 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
909 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
910 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
911 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
912 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
913 'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
915 _order = 'order_id desc, sequence, id'
917 'product_uom' : _get_uom_id,
919 'product_uom_qty': 1,
920 'product_uos_qty': 1,
927 def _get_line_qty(self, cr, uid, line, context=None):
929 return line.product_uos_qty or 0.0
930 return line.product_uom_qty
932 def _get_line_uom(self, cr, uid, line, context=None):
934 return line.product_uos.id
935 return line.product_uom.id
937 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
938 """Prepare the dict of values to create the new invoice line for a
939 sales order line. This method may be overridden to implement custom
940 invoice generation (making sure to call super() to establish
941 a clean extension chain).
943 :param browse_record line: sale.order.line record to invoice
944 :param int account_id: optional ID of a G/L account to force
945 (this is used for returning products including service)
946 :return: dict of values to create() the invoice line
949 if not line.invoiced:
952 account_id = line.product_id.property_account_income.id
954 account_id = line.product_id.categ_id.property_account_income_categ.id
956 raise osv.except_osv(_('Error!'),
957 _('Please define income account for this product: "%s" (id:%d).') % \
958 (line.product_id.name, line.product_id.id,))
960 prop = self.pool.get('ir.property').get(cr, uid,
961 'property_account_income_categ', 'product.category',
963 account_id = prop and prop.id or False
964 uosqty = self._get_line_qty(cr, uid, line, context=context)
965 uos_id = self._get_line_uom(cr, uid, line, context=context)
968 pu = round(line.price_unit * line.product_uom_qty / uosqty,
969 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
970 fpos = line.order_id.fiscal_position or False
971 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
973 raise osv.except_osv(_('Error!'),
974 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
977 'sequence': line.sequence,
978 'origin': line.order_id.name,
979 'account_id': account_id,
982 'discount': line.discount,
984 'product_id': line.product_id.id or False,
985 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
986 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
991 def invoice_line_create(self, cr, uid, ids, context=None):
997 for line in self.browse(cr, uid, ids, context=context):
998 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1000 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1001 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
1002 sales.add(line.order_id.id)
1003 create_ids.append(inv_id)
1004 # Trigger workflow events
1005 for sale_id in sales:
1006 workflow.trg_write(uid, 'sale.order', sale_id, cr)
1009 def button_cancel(self, cr, uid, ids, context=None):
1010 for line in self.browse(cr, uid, ids, context=context):
1012 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
1013 return self.write(cr, uid, ids, {'state': 'cancel'})
1015 def button_confirm(self, cr, uid, ids, context=None):
1016 return self.write(cr, uid, ids, {'state': 'confirmed'})
1018 def button_done(self, cr, uid, ids, context=None):
1019 res = self.write(cr, uid, ids, {'state': 'done'})
1020 for line in self.browse(cr, uid, ids, context=context):
1021 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
1024 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1025 product_obj = self.pool.get('product.product')
1027 return {'value': {'product_uom': product_uos,
1028 'product_uom_qty': product_uos_qty}, 'domain': {}}
1030 product = product_obj.browse(cr, uid, product_id)
1032 'product_uom': product.uom_id.id,
1034 # FIXME must depend on uos/uom of the product and not only of the coeff.
1037 'product_uom_qty': product_uos_qty / product.uos_coeff,
1038 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1040 except ZeroDivisionError:
1042 return {'value': value}
1044 def create(self, cr, uid, values, context=None):
1045 if values.get('order_id') and values.get('product_id') and any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']):
1046 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
1047 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
1048 qty=float(values.get('product_uom_qty', False)),
1049 uom=values.get('product_uom', False),
1050 qty_uos=float(values.get('product_uos_qty', False)),
1051 uos=values.get('product_uos', False),
1052 name=values.get('name', False),
1053 partner_id=order['partner_id'][0],
1054 date_order=order['date_order'],
1055 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
1056 flag=False, # Force name update
1059 values = dict(defaults, **values)
1060 return super(sale_order_line, self).create(cr, uid, values, context=context)
1062 def copy_data(self, cr, uid, id, default=None, context=None):
1065 default.update({'state': 'draft', 'invoice_lines': [], 'procurement_ids': []})
1066 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1068 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1069 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1070 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1071 context = context or {}
1072 lang = lang or context.get('lang', False)
1074 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
1076 product_uom_obj = self.pool.get('product.uom')
1077 partner_obj = self.pool.get('res.partner')
1078 product_obj = self.pool.get('product.product')
1079 context = {'lang': lang, 'partner_id': partner_id}
1080 partner = partner_obj.browse(cr, uid, partner_id)
1082 context_partner = {'lang': lang, 'partner_id': partner_id}
1085 return {'value': {'th_weight': 0,
1086 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1089 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1093 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
1097 uom2 = product_uom_obj.browse(cr, uid, uom)
1098 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1101 if product_obj.uos_id:
1102 uos2 = product_uom_obj.browse(cr, uid, uos)
1103 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1109 if not fiscal_position:
1110 fpos = partner.property_account_position or False
1112 fpos = self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position)
1113 if update_tax: #The quantity only have changed
1114 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1117 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1118 if product_obj.description_sale:
1119 result['name'] += '\n'+product_obj.description_sale
1121 if (not uom) and (not uos):
1122 result['product_uom'] = product_obj.uom_id.id
1123 if product_obj.uos_id:
1124 result['product_uos'] = product_obj.uos_id.id
1125 result['product_uos_qty'] = qty * product_obj.uos_coeff
1126 uos_category_id = product_obj.uos_id.category_id.id
1128 result['product_uos'] = False
1129 result['product_uos_qty'] = qty
1130 uos_category_id = False
1131 result['th_weight'] = qty * product_obj.weight
1132 domain = {'product_uom':
1133 [('category_id', '=', product_obj.uom_id.category_id.id)],
1135 [('category_id', '=', uos_category_id)]}
1136 elif uos and not uom: # only happens if uom is False
1137 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1138 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1139 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1140 elif uom: # whether uos is set or not
1141 default_uom = product_obj.uom_id and product_obj.uom_id.id
1142 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1143 if product_obj.uos_id:
1144 result['product_uos'] = product_obj.uos_id.id
1145 result['product_uos_qty'] = qty * product_obj.uos_coeff
1147 result['product_uos'] = False
1148 result['product_uos_qty'] = qty
1149 result['th_weight'] = q * product_obj.weight # Round the quantity up
1152 uom2 = product_obj.uom_id
1156 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1157 'Please set one before choosing a product.')
1158 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1160 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1161 product, qty or 1.0, partner_id, {
1162 'uom': uom or result.get('product_uom'),
1166 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1167 "You have to change either the product, the quantity or the pricelist.")
1169 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1171 result.update({'price_unit': price})
1174 'title': _('Configuration Error!'),
1175 'message' : warning_msgs
1177 return {'value': result, 'domain': domain, 'warning': warning}
1179 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1180 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1181 lang=False, update_tax=True, date_order=False, context=None):
1182 context = context or {}
1183 lang = lang or ('lang' in context and context['lang'])
1185 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1186 return self.product_id_change(cursor, user, ids, pricelist, product,
1187 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1188 partner_id=partner_id, lang=lang, update_tax=update_tax,
1189 date_order=date_order, context=context)
1191 def unlink(self, cr, uid, ids, context=None):
1194 """Allows to delete sales order lines in draft,cancel states"""
1195 for rec in self.browse(cr, uid, ids, context=context):
1196 if rec.state not in ['draft', 'cancel']:
1197 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1198 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1200 class res_company(osv.Model):
1201 _inherit = "res.company"
1203 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
1207 class mail_compose_message(osv.Model):
1208 _inherit = 'mail.compose.message'
1210 def send_mail(self, cr, uid, ids, context=None):
1211 context = context or {}
1212 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1213 context = dict(context, mail_post_autofollow=True)
1214 self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
1215 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1218 class account_invoice(osv.Model):
1219 _inherit = 'account.invoice'
1221 def _get_default_section_id(self, cr, uid, context=None):
1222 """ Gives default section by checking if present in the context """
1223 section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
1225 section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
1228 def _resolve_section_id_from_context(self, cr, uid, context=None):
1229 """ Returns ID of section based on the value of 'section_id'
1230 context key, or None if it cannot be resolved to a single
1235 if type(context.get('default_section_id')) in (int, long):
1236 return context.get('default_section_id')
1237 if isinstance(context.get('default_section_id'), basestring):
1238 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
1239 if len(section_ids) == 1:
1240 return int(section_ids[0][0])
1244 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
1248 'section_id': lambda self, cr, uid, c=None: self._get_default_section_id(cr, uid, context=c)
1251 def confirm_paid(self, cr, uid, ids, context=None):
1252 sale_order_obj = self.pool.get('sale.order')
1253 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1254 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1255 for so_id in so_ids:
1256 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1259 def unlink(self, cr, uid, ids, context=None):
1260 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1261 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1262 #if we can't cancel all invoices, do nothing
1263 if len(invoice_ids) == len(ids):
1264 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1265 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1267 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1268 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1271 class procurement_order(osv.osv):
1272 _inherit = 'procurement.order'
1274 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
1278 class product_product(osv.Model):
1279 _inherit = 'product.product'
1281 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1282 SaleOrderLine = self.pool['sale.order.line']
1284 product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context)
1285 for product_id in ids
1288 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1293 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: