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 res_company(osv.Model):
31 _inherit = "res.company"
33 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
36 class sale_order(osv.osv):
38 _inherit = ['mail.thread', 'ir.needaction_mixin']
39 _description = "Sales Order"
42 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
43 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
47 def _amount_line_tax(self, cr, uid, line, context=None):
49 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']:
50 val += c.get('amount', 0.0)
53 def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
54 """ Wrapper because of direct method passing as parameter for function fields """
55 return self._amount_all(cr, uid, ids, field_name, arg, context=context)
57 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
58 cur_obj = self.pool.get('res.currency')
60 for order in self.browse(cr, uid, ids, context=context):
62 'amount_untaxed': 0.0,
67 cur = order.pricelist_id.currency_id
68 for line in order.order_line:
69 val1 += line.price_subtotal
70 val += self._amount_line_tax(cr, uid, line, context=context)
71 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
72 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
73 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
77 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
79 for sale in self.browse(cursor, user, ids, context=context):
84 for invoice in sale.invoice_ids:
85 if invoice.state not in ('draft', 'cancel'):
86 tot += invoice.amount_untaxed
88 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
93 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
95 for sale in self.browse(cursor, user, ids, context=context):
101 def _invoiced(self, cursor, user, ids, name, arg, context=None):
103 for sale in self.browse(cursor, user, ids, context=context):
105 invoice_existence = False
106 for invoice in sale.invoice_ids:
107 if invoice.state!='cancel':
108 invoice_existence = True
109 if invoice.state != 'paid':
112 if not invoice_existence or sale.state == 'manual':
116 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
123 if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
124 clause += 'AND inv.state = \'paid\''
126 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
127 sale_clause = ', sale_order AS sale '
130 cursor.execute('SELECT rel.order_id ' \
131 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
132 'WHERE rel.invoice_id = inv.id ' + clause)
133 res = cursor.fetchall()
135 cursor.execute('SELECT sale.id ' \
136 'FROM sale_order AS sale ' \
137 'WHERE sale.id NOT IN ' \
138 '(SELECT rel.order_id ' \
139 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
140 res.extend(cursor.fetchall())
142 return [('id', '=', 0)]
143 return [('id', 'in', [x[0] for x in res])]
145 def _get_order(self, cr, uid, ids, context=None):
147 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
148 result[line.order_id.id] = True
151 def _get_default_company(self, cr, uid, context=None):
152 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
154 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
157 def _get_default_section_id(self, cr, uid, context=None):
158 """ Gives default section by checking if present in the context """
159 section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
161 section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
164 def _resolve_section_id_from_context(self, cr, uid, context=None):
165 """ Returns ID of section based on the value of 'section_id'
166 context key, or None if it cannot be resolved to a single
171 if type(context.get('default_section_id')) in (int, long):
172 return context.get('default_section_id')
173 if isinstance(context.get('default_section_id'), basestring):
174 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
175 if len(section_ids) == 1:
176 return int(section_ids[0][0])
180 'name': fields.char('Order Reference', required=True, copy=False,
181 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
182 'origin': fields.char('Source Document', help="Reference of the document that generated this sales order request."),
183 'client_order_ref': fields.char('Customer Reference', copy=False),
184 'state': fields.selection([
185 ('draft', 'Draft Quotation'),
186 ('sent', 'Quotation Sent'),
187 ('cancel', 'Cancelled'),
188 ('waiting_date', 'Waiting Schedule'),
189 ('progress', 'Sales Order'),
190 ('manual', 'Sale to Invoice'),
191 ('shipping_except', 'Shipping Exception'),
192 ('invoice_except', 'Invoice Exception'),
194 ], 'Status', readonly=True, copy=False, help="Gives the status of the quotation or sales order.\
195 \nThe exception status is automatically set when a cancel operation occurs \
196 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\
197 but waiting for the scheduler to run on the order date.", select=True),
198 'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False),
199 'validity_date': fields.date('Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
200 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
201 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed.", copy=False),
202 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
203 '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'),
204 '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."),
205 '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."),
206 'order_policy': fields.selection([
207 ('manual', 'On Demand'),
208 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
209 help="""This field controls how invoice and delivery operations are synchronized."""),
210 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
211 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
212 '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."),
214 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True),
215 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, copy=False, 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)."),
216 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
217 'invoiced': fields.function(_invoiced, string='Paid',
218 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
219 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
220 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
221 'note': fields.text('Terms and conditions'),
223 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
225 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
226 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
228 multi='sums', help="The amount without tax.", track_visibility='always'),
229 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
231 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
232 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
234 multi='sums', help="The tax amount."),
235 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
237 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
238 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
240 multi='sums', help="The total amount."),
242 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
243 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
244 'company_id': fields.many2one('res.company', 'Company'),
245 'section_id': fields.many2one('crm.case.section', 'Sales Team', change_default=True),
246 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', copy=False),
247 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
251 'date_order': fields.datetime.now,
252 'order_policy': 'manual',
253 'company_id': _get_default_company,
255 'user_id': lambda obj, cr, uid, context: uid,
256 'name': lambda obj, cr, uid, context: '/',
257 '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'],
258 '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'],
259 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note,
260 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
263 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
265 _order = 'date_order desc, id desc'
268 def unlink(self, cr, uid, ids, context=None):
269 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
271 for s in sale_orders:
272 if s['state'] in ['draft', 'cancel']:
273 unlink_ids.append(s['id'])
275 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
277 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
279 def copy_quotation(self, cr, uid, ids, context=None):
280 id = self.copy(cr, uid, ids[0], context=context)
281 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
282 view_id = view_ref and view_ref[1] or False,
284 'type': 'ir.actions.act_window',
285 'name': _('Sales Order'),
286 'res_model': 'sale.order',
295 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
296 context = context or {}
300 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
302 if not order_lines or order_lines == [(6, 0, [])]:
303 return {'value': value}
305 'title': _('Pricelist Warning!'),
306 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
308 return {'warning': warning, 'value': value}
310 def get_salenote(self, cr, uid, ids, partner_id, context=None):
311 context_lang = context.copy()
313 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
314 context_lang.update({'lang': partner_lang})
315 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
317 def onchange_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position, context=None):
319 if not fiscal_position:
321 company_id = self._get_default_company(cr, uid, context=context)
322 fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, delivery_id, context=context)
324 r['value']['fiscal_position'] = fiscal_position
327 def onchange_partner_id(self, cr, uid, ids, part, context=None):
329 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
331 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
332 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
333 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
334 payment_term = part.property_payment_term and part.property_payment_term.id or False
335 dedicated_salesman = part.user_id and part.user_id.id or uid
337 'partner_invoice_id': addr['invoice'],
338 'partner_shipping_id': addr['delivery'],
339 'payment_term': payment_term,
340 'user_id': dedicated_salesman,
342 delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False, context=context)
343 val.update(delivery_onchange['value'])
345 val['pricelist_id'] = pricelist
346 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
347 if sale_note: val.update({'note': sale_note})
348 return {'value': val}
350 def create(self, cr, uid, vals, context=None):
353 if vals.get('name', '/') == '/':
354 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
355 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id', 'fiscal_position']):
356 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value']
357 if not vals.get('fiscal_position') and vals.get('partner_shipping_id'):
358 delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context)
359 defaults.update(delivery_onchange['value'])
360 vals = dict(defaults, **vals)
361 ctx = dict(context or {}, mail_create_nolog=True)
362 new_id = super(sale_order, self).create(cr, uid, vals, context=ctx)
363 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=ctx)
366 def button_dummy(self, cr, uid, ids, context=None):
369 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
370 # can be removed after 6.1.
371 def _inv_get(self, cr, uid, order, context=None):
374 def _prepare_invoice(self, cr, uid, order, lines, context=None):
375 """Prepare the dict of values to create the new invoice for a
376 sales order. This method may be overridden to implement custom
377 invoice generation (making sure to call super() to establish
378 a clean extension chain).
380 :param browse_record order: sale.order record to invoice
381 :param list(int) line: list of invoice line IDs that must be
382 attached to the invoice
383 :return: dict of value to create() the invoice
387 journal_ids = self.pool.get('account.journal').search(cr, uid,
388 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
391 raise osv.except_osv(_('Error!'),
392 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
394 'name': order.client_order_ref or '',
395 'origin': order.name,
396 'type': 'out_invoice',
397 'reference': order.client_order_ref or order.name,
398 'account_id': order.partner_id.property_account_receivable.id,
399 'partner_id': order.partner_invoice_id.id,
400 'journal_id': journal_ids[0],
401 'invoice_line': [(6, 0, lines)],
402 'currency_id': order.pricelist_id.currency_id.id,
403 'comment': order.note,
404 'payment_term': order.payment_term and order.payment_term.id or False,
405 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
406 'date_invoice': context.get('date_invoice', False),
407 'company_id': order.company_id.id,
408 'user_id': order.user_id and order.user_id.id or False,
409 'section_id' : order.section_id.id
412 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
413 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
416 def _make_invoice(self, cr, uid, order, lines, context=None):
417 inv_obj = self.pool.get('account.invoice')
418 obj_invoice_line = self.pool.get('account.invoice.line')
421 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
422 from_line_invoice_ids = []
423 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
424 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
425 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
426 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
427 for preinv in order.invoice_ids:
428 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
429 for preline in preinv.invoice_line:
430 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
431 lines.append(inv_line_id)
432 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
433 inv_id = inv_obj.create(cr, uid, inv, context=context)
434 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
435 if data.get('value', False):
436 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
437 inv_obj.button_compute(cr, uid, [inv_id])
440 def print_quotation(self, cr, uid, ids, context=None):
442 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
444 assert len(ids) == 1, 'This option should only be used for a single id at a time'
445 self.signal_workflow(cr, uid, ids, 'quotation_sent')
446 return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
448 def manual_invoice(self, cr, uid, ids, context=None):
449 """ create invoices for the given sales orders (ids), and open the form
450 view of one of the newly created invoices
452 mod_obj = self.pool.get('ir.model.data')
454 # create invoices through the sales orders' workflow
455 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
456 self.signal_workflow(cr, uid, ids, 'manual_invoice')
457 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
458 # determine newly created invoices
459 new_inv_ids = list(inv_ids1 - inv_ids0)
461 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
462 res_id = res and res[1] or False,
465 'name': _('Customer Invoices'),
469 'res_model': 'account.invoice',
470 'context': "{'type':'out_invoice'}",
471 'type': 'ir.actions.act_window',
474 'res_id': new_inv_ids and new_inv_ids[0] or False,
477 def action_view_invoice(self, cr, uid, ids, context=None):
479 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.
481 mod_obj = self.pool.get('ir.model.data')
482 act_obj = self.pool.get('ir.actions.act_window')
484 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
485 id = result and result[1] or False
486 result = act_obj.read(cr, uid, [id], context=context)[0]
487 #compute the number of invoices to display
489 for so in self.browse(cr, uid, ids, context=context):
490 inv_ids += [invoice.id for invoice in so.invoice_ids]
491 #choose the view_mode accordingly
493 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
495 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
496 result['views'] = [(res and res[1] or False, 'form')]
497 result['res_id'] = inv_ids and inv_ids[0] or False
500 def test_no_product(self, cr, uid, order, context):
501 for line in order.order_line:
502 if line.product_id and (line.product_id.type<>'service'):
506 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
508 states = ['confirmed', 'done', 'exception']
512 invoice = self.pool.get('account.invoice')
513 obj_sale_order_line = self.pool.get('sale.order.line')
514 partner_currency = {}
515 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
516 # last day of the last month as invoice date
518 context = dict(context or {}, date_invoice=date_invoice)
519 for o in self.browse(cr, uid, ids, context=context):
520 currency_id = o.pricelist_id.currency_id.id
521 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
522 raise osv.except_osv(
524 _('You cannot group sales having different currencies for the same partner.'))
526 partner_currency[o.partner_id.id] = currency_id
528 for line in o.order_line:
531 elif (line.state in states):
532 lines.append(line.id)
533 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
535 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
537 for o in self.browse(cr, uid, ids, context=context):
538 for i in o.invoice_ids:
539 if i.state == 'draft':
541 for val in invoices.values():
543 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
547 invoice_ref += (o.client_order_ref or o.name) + '|'
548 origin_ref += (o.origin or o.name) + '|'
549 self.write(cr, uid, [o.id], {'state': 'progress'})
550 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
551 self.invalidate_cache(cr, uid, ['invoice_ids'], [o.id], context=context)
552 #remove last '|' in invoice_ref
553 if len(invoice_ref) >= 1:
554 invoice_ref = invoice_ref[:-1]
555 if len(origin_ref) >= 1:
556 origin_ref = origin_ref[:-1]
557 invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
559 for order, il in val:
560 res = self._make_invoice(cr, uid, order, il, context=context)
561 invoice_ids.append(res)
562 self.write(cr, uid, [order.id], {'state': 'progress'})
563 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
564 self.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context)
567 def action_invoice_cancel(self, cr, uid, ids, context=None):
568 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
571 def action_invoice_end(self, cr, uid, ids, context=None):
572 for this in self.browse(cr, uid, ids, context=context):
573 for line in this.order_line:
574 if line.state == 'exception':
575 line.write({'state': 'confirmed'})
576 if this.state == 'invoice_except':
577 this.write({'state': 'progress'})
580 def action_cancel(self, cr, uid, ids, context=None):
583 sale_order_line_obj = self.pool.get('sale.order.line')
584 account_invoice_obj = self.pool.get('account.invoice')
585 for sale in self.browse(cr, uid, ids, context=context):
586 for inv in sale.invoice_ids:
587 if inv.state not in ('draft', 'cancel'):
588 raise osv.except_osv(
589 _('Cannot cancel this sales order!'),
590 _('First cancel all invoices attached to this sales order.'))
591 inv.signal_workflow('invoice_cancel')
592 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
594 self.write(cr, uid, ids, {'state': 'cancel'})
597 def action_button_confirm(self, cr, uid, ids, context=None):
598 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
599 self.signal_workflow(cr, uid, ids, 'order_confirm')
602 def action_wait(self, cr, uid, ids, context=None):
603 context = context or {}
604 for o in self.browse(cr, uid, ids):
606 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
607 noprod = self.test_no_product(cr, uid, o, context)
608 if (o.order_policy == 'manual') or noprod:
609 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
611 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
612 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
615 def action_quotation_send(self, cr, uid, ids, context=None):
617 This function opens a window to compose an email, with the edi sale template message loaded by default
619 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
620 ir_model_data = self.pool.get('ir.model.data')
622 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
626 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
628 compose_form_id = False
631 'default_model': 'sale.order',
632 'default_res_id': ids[0],
633 'default_use_template': bool(template_id),
634 'default_template_id': template_id,
635 'default_composition_mode': 'comment',
636 'mark_so_as_sent': True
639 'type': 'ir.actions.act_window',
642 'res_model': 'mail.compose.message',
643 'views': [(compose_form_id, 'form')],
644 'view_id': compose_form_id,
649 def action_done(self, cr, uid, ids, context=None):
650 for order in self.browse(cr, uid, ids, context=context):
651 self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
652 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
654 def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
655 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
658 'origin': order.name,
659 'date_planned': date_planned,
660 'product_id': line.product_id.id,
661 'product_qty': line.product_uom_qty,
662 'product_uom': line.product_uom.id,
663 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
664 'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
665 'company_id': order.company_id.id,
666 'group_id': group_id,
667 'invoice_state': (order.order_policy == 'picking') and '2binvoiced' or 'none',
668 'sale_line_id': line.id
671 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
672 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
675 def _prepare_procurement_group(self, cr, uid, order, context=None):
676 return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
678 def procurement_needed(self, cr, uid, ids, context=None):
679 #when sale is installed only, there is no need to create procurements, that's only
680 #further installed modules (sale_service, sale_stock) that will change this.
681 sale_line_obj = self.pool.get('sale.order.line')
683 for order in self.browse(cr, uid, ids, context=context):
684 res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
687 def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
688 for sale_order in self.browse(cr, uid, ids, context=context):
689 self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
692 def action_ship_create(self, cr, uid, ids, context=None):
693 """Create the required procurements to supply sales order lines, also connecting
694 the procurements to appropriate stock moves in order to bring the goods to the
695 sales order's requested location.
699 procurement_obj = self.pool.get('procurement.order')
700 sale_line_obj = self.pool.get('sale.order.line')
701 for order in self.browse(cr, uid, ids, context=context):
703 vals = self._prepare_procurement_group(cr, uid, order, context=context)
704 if not order.procurement_group_id:
705 group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
706 order.write({'procurement_group_id': group_id}, context=context)
708 for line in order.order_line:
709 #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
710 if line.procurement_ids:
711 #first check them to see if they are in exception or not (one of the related moves is cancelled)
712 procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
714 #run again procurement that are in exception in order to trigger another move
715 proc_ids += [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
716 elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
717 if (line.state == 'done') or not line.product_id:
719 vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
720 proc_id = procurement_obj.create(cr, uid, vals, context=context)
721 proc_ids.append(proc_id)
722 #Confirm procurement order such that rules will be applied on it
723 #note that the workflow normally ensure proc_ids isn't an empty list
724 procurement_obj.run(cr, uid, proc_ids, context=context)
726 #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
727 if order.state == 'shipping_except':
728 val = {'state': 'progress', 'shipped': False}
730 if (order.order_policy == 'manual'):
731 for line in order.order_line:
732 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
733 val['state'] = 'manual'
740 def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None):
741 '''Update taxes of order lines for each line where a product is defined
743 :param list ids: not used
744 :param int fiscal_position: sale order fiscal position
745 :param list order_lines: command list for one2many write method
748 fiscal_obj = self.pool.get('account.fiscal.position')
749 product_obj = self.pool.get('product.product')
750 line_obj = self.pool.get('sale.order.line')
754 fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context)
756 for line in order_lines:
757 # create (0, 0, { fields })
758 # update (1, ID, { fields })
759 if line[0] in [0, 1]:
761 if line[2].get('product_id'):
762 prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context)
764 prod = line_obj.browse(cr, uid, line[1], context=context).product_id
765 if prod and prod.taxes_id:
766 line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]
767 order_line.append(line)
770 # link all (6, 0, IDS)
771 elif line[0] in [4, 6]:
772 line_ids = line[0] == 4 and [line[1]] or line[2]
773 for line_id in line_ids:
774 prod = line_obj.browse(cr, uid, line_id, context=context).product_id
775 if prod and prod.taxes_id:
776 order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}])
778 order_line.append([4, line_id])
780 order_line.append(line)
781 return {'value': {'order_line': order_line}}
783 def test_procurements_done(self, cr, uid, ids, context=None):
784 for sale in self.browse(cr, uid, ids, context=context):
785 for line in sale.order_line:
786 if not all([x.state == 'done' for x in line.procurement_ids]):
790 def test_procurements_except(self, cr, uid, ids, context=None):
791 for sale in self.browse(cr, uid, ids, context=context):
792 for line in sale.order_line:
793 if any([x.state == 'cancel' for x in line.procurement_ids]):
798 # TODO add a field price_unit_uos
799 # - update it on change product and unit price
800 # - use it in report if there is a uos
801 class sale_order_line(osv.osv):
803 def need_procurement(self, cr, uid, ids, context=None):
804 #when sale is installed only, there is no need to create procurements, that's only
805 #further installed modules (sale_service, sale_stock) that will change this.
806 prod_obj = self.pool.get('product.product')
807 for line in self.browse(cr, uid, ids, context=context):
808 if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
812 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
813 tax_obj = self.pool.get('account.tax')
814 cur_obj = self.pool.get('res.currency')
818 for line in self.browse(cr, uid, ids, context=context):
819 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
820 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
821 cur = line.order_id.pricelist_id.currency_id
822 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
825 def _get_uom_id(self, cr, uid, *args):
827 proxy = self.pool.get('ir.model.data')
828 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
830 except Exception, ex:
833 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
834 res = dict.fromkeys(ids, False)
835 for this in self.browse(cr, uid, ids, context=context):
836 res[this.id] = this.invoice_lines and \
837 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
840 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
841 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
842 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
843 sale_order_line sol ON (sol.order_id = rel.order_id)
844 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
845 return [i[0] for i in cr.fetchall()]
848 _name = 'sale.order.line'
849 _description = 'Sales Order Line'
851 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
852 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
853 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
854 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'),
855 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True, copy=False),
856 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
858 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
859 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
861 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
862 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
863 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
864 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
865 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
866 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
867 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
868 'product_uos': fields.many2one('product.uom', 'Product UoS'),
869 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
870 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
871 'state': fields.selection(
872 [('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')],
873 'Status', required=True, readonly=True, copy=False,
874 help='* The \'Draft\' status is set when the related sales order in draft status. \
875 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
876 \n* The \'Exception\' status is set when the related sales order is set as exception. \
877 \n* The \'Done\' status is set when the sales order line has been picked. \
878 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
879 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
880 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
881 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
882 '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)]}),
883 'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
885 _order = 'order_id desc, sequence, id'
887 'product_uom' : _get_uom_id,
889 'product_uom_qty': 1,
890 'product_uos_qty': 1,
899 def _get_line_qty(self, cr, uid, line, context=None):
901 return line.product_uos_qty or 0.0
902 return line.product_uom_qty
904 def _get_line_uom(self, cr, uid, line, context=None):
906 return line.product_uos.id
907 return line.product_uom.id
909 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
910 """Prepare the dict of values to create the new invoice line for a
911 sales order line. This method may be overridden to implement custom
912 invoice generation (making sure to call super() to establish
913 a clean extension chain).
915 :param browse_record line: sale.order.line record to invoice
916 :param int account_id: optional ID of a G/L account to force
917 (this is used for returning products including service)
918 :return: dict of values to create() the invoice line
921 if not line.invoiced:
924 account_id = line.product_id.property_account_income.id
926 account_id = line.product_id.categ_id.property_account_income_categ.id
928 raise osv.except_osv(_('Error!'),
929 _('Please define income account for this product: "%s" (id:%d).') % \
930 (line.product_id.name, line.product_id.id,))
932 prop = self.pool.get('ir.property').get(cr, uid,
933 'property_account_income_categ', 'product.category',
935 account_id = prop and prop.id or False
936 uosqty = self._get_line_qty(cr, uid, line, context=context)
937 uos_id = self._get_line_uom(cr, uid, line, context=context)
940 pu = round(line.price_unit * line.product_uom_qty / uosqty,
941 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
942 fpos = line.order_id.fiscal_position or False
943 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
945 raise osv.except_osv(_('Error!'),
946 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
949 'sequence': line.sequence,
950 'origin': line.order_id.name,
951 'account_id': account_id,
954 'discount': line.discount,
956 'product_id': line.product_id.id or False,
957 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
958 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
963 def invoice_line_create(self, cr, uid, ids, context=None):
969 for line in self.browse(cr, uid, ids, context=context):
970 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
972 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
973 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
974 sales.add(line.order_id.id)
975 create_ids.append(inv_id)
976 # Trigger workflow events
977 for sale_id in sales:
978 workflow.trg_write(uid, 'sale.order', sale_id, cr)
981 def button_cancel(self, cr, uid, ids, context=None):
982 for line in self.browse(cr, uid, ids, context=context):
984 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
985 return self.write(cr, uid, ids, {'state': 'cancel'})
987 def button_confirm(self, cr, uid, ids, context=None):
988 return self.write(cr, uid, ids, {'state': 'confirmed'})
990 def button_done(self, cr, uid, ids, context=None):
991 res = self.write(cr, uid, ids, {'state': 'done'})
992 for line in self.browse(cr, uid, ids, context=context):
993 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
996 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
997 product_obj = self.pool.get('product.product')
999 return {'value': {'product_uom': product_uos,
1000 'product_uom_qty': product_uos_qty}, 'domain': {}}
1002 product = product_obj.browse(cr, uid, product_id)
1004 'product_uom': product.uom_id.id,
1006 # FIXME must depend on uos/uom of the product and not only of the coeff.
1009 'product_uom_qty': product_uos_qty / product.uos_coeff,
1010 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1012 except ZeroDivisionError:
1014 return {'value': value}
1016 def create(self, cr, uid, values, context=None):
1017 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']):
1018 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
1019 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
1020 qty=float(values.get('product_uom_qty', False)),
1021 uom=values.get('product_uom', False),
1022 qty_uos=float(values.get('product_uos_qty', False)),
1023 uos=values.get('product_uos', False),
1024 name=values.get('name', False),
1025 partner_id=order['partner_id'][0],
1026 date_order=order['date_order'],
1027 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
1028 flag=False, # Force name update
1031 if defaults.get('tax_id'):
1032 defaults['tax_id'] = [[6, 0, defaults['tax_id']]]
1033 values = dict(defaults, **values)
1034 return super(sale_order_line, self).create(cr, uid, values, context=context)
1036 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1037 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1038 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1041 Partner = self.pool['res.partner']
1042 ProductUom = self.pool['product.uom']
1043 Product = self.pool['product.product']
1044 ctx_product = dict(context)
1047 partner = Partner.browse(cr, uid, partner_id, context=context)
1048 ctx_product['lang'] = partner.lang
1049 ctx_product['partner_id'] = partner_id
1051 ctx_product['lang'] = lang
1054 return {'value': {'th_weight': 0,
1055 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1058 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1061 product_obj = Product.browse(cr, uid, product, context=ctx_product)
1065 uom2 = ProductUom.browse(cr, uid, uom, context=context)
1066 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1069 if product_obj.uos_id:
1070 uos2 = ProductUom.browse(cr, uid, uos, context=context)
1071 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1077 if not fiscal_position:
1078 fpos = partner and partner.property_account_position or False
1080 fpos = self.pool['account.fiscal.position'].browse(cr, uid, fiscal_position)
1081 if update_tax: # The quantity only have changed
1082 result['tax_id'] = self.pool['account.fiscal.position'].map_tax(cr, uid, fpos, product_obj.taxes_id)
1085 result['name'] = Product.name_get(cr, uid, [product_obj.id], context=ctx_product)[0][1]
1086 if product_obj.description_sale:
1087 result['name'] += '\n'+product_obj.description_sale
1089 if (not uom) and (not uos):
1090 result['product_uom'] = product_obj.uom_id.id
1091 if product_obj.uos_id:
1092 result['product_uos'] = product_obj.uos_id.id
1093 result['product_uos_qty'] = qty * product_obj.uos_coeff
1094 uos_category_id = product_obj.uos_id.category_id.id
1096 result['product_uos'] = False
1097 result['product_uos_qty'] = qty
1098 uos_category_id = False
1099 result['th_weight'] = qty * product_obj.weight
1100 domain = {'product_uom':
1101 [('category_id', '=', product_obj.uom_id.category_id.id)],
1103 [('category_id', '=', uos_category_id)]}
1104 elif uos and not uom: # only happens if uom is False
1105 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1106 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1107 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1108 elif uom: # whether uos is set or not
1109 default_uom = product_obj.uom_id and product_obj.uom_id.id
1110 q = ProductUom._compute_qty(cr, uid, uom, qty, default_uom)
1111 if product_obj.uos_id:
1112 result['product_uos'] = product_obj.uos_id.id
1113 result['product_uos_qty'] = qty * product_obj.uos_coeff
1115 result['product_uos'] = False
1116 result['product_uos_qty'] = qty
1117 result['th_weight'] = q * product_obj.weight # Round the quantity up
1120 uom2 = product_obj.uom_id
1122 if pricelist and partner_id:
1123 price = self.pool['product.pricelist'].price_get(cr, uid, [pricelist],
1124 product, qty or 1.0, partner_id, {
1125 'uom': uom or result.get('product_uom'),
1129 price = Product.price_get(cr, uid, [product], ptype='list_price', context=ctx_product)[product] or False
1130 result.update({'price_unit': price})
1131 return {'value': result, 'domain': domain}
1133 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1134 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1135 lang=False, update_tax=True, date_order=False, context=None):
1136 context = context or {}
1137 lang = lang or ('lang' in context and context['lang'])
1139 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1140 return self.product_id_change(cursor, user, ids, pricelist, product,
1141 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1142 partner_id=partner_id, lang=lang, update_tax=update_tax,
1143 date_order=date_order, context=context)
1145 def unlink(self, cr, uid, ids, context=None):
1148 """Allows to delete sales order lines in draft,cancel states"""
1149 for rec in self.browse(cr, uid, ids, context=context):
1150 if rec.state not in ['draft', 'cancel']:
1151 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1152 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1155 class mail_compose_message(osv.Model):
1156 _inherit = 'mail.compose.message'
1158 def send_mail(self, cr, uid, ids, context=None):
1159 context = context or {}
1160 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1161 context = dict(context, mail_post_autofollow=True)
1162 self.pool.get('sale.order').signal_workflow(cr, uid, [context['default_res_id']], 'quotation_sent')
1163 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1166 class account_invoice(osv.Model):
1167 _inherit = 'account.invoice'
1169 def _get_default_section_id(self, cr, uid, context=None):
1170 """ Gives default section by checking if present in the context """
1171 section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
1173 section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
1176 def _resolve_section_id_from_context(self, cr, uid, context=None):
1177 """ Returns ID of section based on the value of 'section_id'
1178 context key, or None if it cannot be resolved to a single
1183 if type(context.get('default_section_id')) in (int, long):
1184 return context.get('default_section_id')
1185 if isinstance(context.get('default_section_id'), basestring):
1186 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
1187 if len(section_ids) == 1:
1188 return int(section_ids[0][0])
1192 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
1196 'section_id': lambda self, cr, uid, c=None: self._get_default_section_id(cr, uid, context=c)
1199 def confirm_paid(self, cr, uid, ids, context=None):
1200 sale_order_obj = self.pool.get('sale.order')
1201 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1202 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1203 for so_id in so_ids:
1204 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1207 def unlink(self, cr, uid, ids, context=None):
1208 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1209 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1210 #if we can't cancel all invoices, do nothing
1211 if len(invoice_ids) == len(ids):
1212 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1213 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1215 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1216 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1219 class procurement_order(osv.osv):
1220 _inherit = 'procurement.order'
1222 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
1225 def write(self, cr, uid, ids, vals, context=None):
1226 if isinstance(ids, (int, long)):
1228 res = super(procurement_order, self).write(cr, uid, ids, vals, context=context)
1229 from openerp import workflow
1230 if vals.get('state') in ['done', 'cancel', 'exception']:
1231 for proc in self.browse(cr, uid, ids, context=context):
1232 if proc.sale_line_id and proc.sale_line_id.order_id:
1233 order_id = proc.sale_line_id.order_id.id
1234 if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context):
1235 workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr)
1236 if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context):
1237 workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr)
1240 class product_product(osv.Model):
1241 _inherit = 'product.product'
1243 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1244 SaleOrderLine = self.pool['sale.order.line']
1246 product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context)
1247 for product_id in ids
1251 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1255 class product_template(osv.Model):
1256 _inherit = 'product.template'
1258 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1259 res = dict.fromkeys(ids, 0)
1260 for template in self.browse(cr, uid, ids, context=context):
1261 res[template.id] = sum([p.sales_count for p in template.product_variant_ids])
1264 def action_view_sales(self, cr, uid, ids, context=None):
1265 act_obj = self.pool.get('ir.actions.act_window')
1266 mod_obj = self.pool.get('ir.model.data')
1268 for template in self.browse(cr, uid, ids, context=context):
1269 product_ids += [x.id for x in template.product_variant_ids]
1270 result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree',raise_if_not_found=True)
1271 result = act_obj.read(cr, uid, [result], context=context)[0]
1272 result['domain'] = "[('product_id','in',[" + ','.join(map(str, product_ids)) + "])]"
1277 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1281 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: