# -*- coding: utf-8 -*- ############################################################################## # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2010 Tiny SPRL (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################## from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta import time from openerp.osv import fields, osv from openerp.tools.translate import _ from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare import openerp.addons.decimal_precision as dp from openerp import workflow class sale_order(osv.osv): _name = "sale.order" _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Sales Order" _track = { 'state': { 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'], 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent'] }, } def copy(self, cr, uid, id, default=None, context=None): if not default: default = {} default.update({ 'date_order': fields.date.context_today(self, cr, uid, context=context), 'state': 'draft', 'invoice_ids': [], 'date_confirm': False, 'client_order_ref': '', 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'), }) return super(sale_order, self).copy(cr, uid, id, default, context=context) def _amount_line_tax(self, cr, uid, line, context=None): val = 0.0 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']: val += c.get('amount', 0.0) return val def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None): """ Wrapper because of direct method passing as parameter for function fields """ return self._amount_all(cr, uid, ids, field_name, arg, context=context) def _amount_all(self, cr, uid, ids, field_name, arg, context=None): cur_obj = self.pool.get('res.currency') res = {} for order in self.browse(cr, uid, ids, context=context): res[order.id] = { 'amount_untaxed': 0.0, 'amount_tax': 0.0, 'amount_total': 0.0, } val = val1 = 0.0 cur = order.pricelist_id.currency_id for line in order.order_line: val1 += line.price_subtotal val += self._amount_line_tax(cr, uid, line, context=context) res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val) res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1) res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax'] return res def _invoiced_rate(self, cursor, user, ids, name, arg, context=None): res = {} for sale in self.browse(cursor, user, ids, context=context): if sale.invoiced: res[sale.id] = 100.0 continue tot = 0.0 for invoice in sale.invoice_ids: if invoice.state not in ('draft', 'cancel'): tot += invoice.amount_untaxed if tot: res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00)) else: res[sale.id] = 0.0 return res def _invoice_exists(self, cursor, user, ids, name, arg, context=None): res = {} for sale in self.browse(cursor, user, ids, context=context): res[sale.id] = False if sale.invoice_ids: res[sale.id] = True return res def _invoiced(self, cursor, user, ids, name, arg, context=None): res = {} for sale in self.browse(cursor, user, ids, context=context): res[sale.id] = True invoice_existence = False for invoice in sale.invoice_ids: if invoice.state!='cancel': invoice_existence = True if invoice.state != 'paid': res[sale.id] = False break if not invoice_existence or sale.state == 'manual': res[sale.id] = False return res def _invoiced_search(self, cursor, user, obj, name, args, context=None): if not len(args): return [] clause = '' sale_clause = '' no_invoiced = False for arg in args: if arg[1] == '=': if arg[2]: clause += 'AND inv.state = \'paid\'' else: clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id ' sale_clause = ', sale_order AS sale ' no_invoiced = True cursor.execute('SELECT rel.order_id ' \ 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \ 'WHERE rel.invoice_id = inv.id ' + clause) res = cursor.fetchall() if no_invoiced: cursor.execute('SELECT sale.id ' \ 'FROM sale_order AS sale ' \ 'WHERE sale.id NOT IN ' \ '(SELECT rel.order_id ' \ 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'') res.extend(cursor.fetchall()) if not res: return [('id', '=', 0)] return [('id', 'in', [x[0] for x in res])] def _get_order(self, cr, uid, ids, context=None): result = {} for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context): result[line.order_id.id] = True return result.keys() def _get_default_company(self, cr, uid, context=None): company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) if not company_id: raise osv.except_osv(_('Error!'), _('There is no default company for the current user!')) return company_id _columns = { 'name': fields.char('Order Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True), 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."), 'client_order_ref': fields.char('Reference/Description', size=64), 'state': fields.selection([ ('draft', 'Draft Quotation'), ('sent', 'Quotation Sent'), ('cancel', 'Cancelled'), ('waiting_date', 'Waiting Schedule'), ('progress', 'Sales Order'), ('manual', 'Sale to Invoice'), ('invoice_except', 'Invoice Exception'), ('done', 'Done'), ], 'Status', readonly=True, track_visibility='onchange', help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sales order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True), 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."), 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."), 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'), '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'), '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."), '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."), 'order_policy': fields.selection([ ('manual', 'On Demand'), ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="""This field controls how invoice and delivery operations are synchronized."""), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."), 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True), '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."), 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), '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)."), 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'), 'invoiced': fields.function(_invoiced, string='Paid', fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."), 'invoice_exists': fields.function(_invoice_exists, string='Invoiced', fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."), 'note': fields.text('Terms and conditions'), 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount', store={ 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), }, multi='sums', help="The amount without tax.", track_visibility='always'), 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes', store={ 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), }, multi='sums', help="The tax amount."), 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total', store={ 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), }, multi='sums', help="The total amount."), 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sales order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}), 'payment_term': fields.many2one('account.payment.term', 'Payment Term'), 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'), 'company_id': fields.many2one('res.company', 'Company'), } _defaults = { 'date_order': fields.date.context_today, 'order_policy': 'manual', 'company_id': _get_default_company, 'state': 'draft', 'user_id': lambda obj, cr, uid, context: uid, 'name': lambda obj, cr, uid, context: '/', 'invoice_quantity': 'order', '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'], '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'], 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note } _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'), ] _order = 'date_order desc, id desc' # Form filling def unlink(self, cr, uid, ids, context=None): sale_orders = self.read(cr, uid, ids, ['state'], context=context) unlink_ids = [] for s in sale_orders: if s['state'] in ['draft', 'cancel']: unlink_ids.append(s['id']) else: raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!')) return osv.osv.unlink(self, cr, uid, unlink_ids, context=context) def copy_quotation(self, cr, uid, ids, context=None): id = self.copy(cr, uid, ids[0], context=None) view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form') view_id = view_ref and view_ref[1] or False, return { 'type': 'ir.actions.act_window', 'name': _('Sales Order'), 'res_model': 'sale.order', 'res_id': id, 'view_type': 'form', 'view_mode': 'form', 'view_id': view_id, 'target': 'current', 'nodestroy': True, } def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None): context = context or {} if not pricelist_id: return {} value = { 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id } if not order_lines: return {'value': value} warning = { 'title': _('Pricelist Warning!'), 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.') } return {'warning': warning, 'value': value} def get_salenote(self, cr, uid, ids, partner_id, context=None): context_lang = context.copy() if partner_id: partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang context_lang.update({'lang': partner_lang}) return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note def onchange_partner_id(self, cr, uid, ids, part, context=None): if not part: return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}} part = self.pool.get('res.partner').browse(cr, uid, part, context=context) addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact']) pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False payment_term = part.property_payment_term and part.property_payment_term.id or False fiscal_position = part.property_account_position and part.property_account_position.id or False dedicated_salesman = part.user_id and part.user_id.id or uid val = { 'partner_invoice_id': addr['invoice'], 'partner_shipping_id': addr['delivery'], 'payment_term': payment_term, 'fiscal_position': fiscal_position, 'user_id': dedicated_salesman, } if pricelist: val['pricelist_id'] = pricelist sale_note = self.get_salenote(cr, uid, ids, part.id, context=context) if sale_note: val.update({'note': sale_note}) return {'value': val} def create(self, cr, uid, vals, context=None): if context is None: context = {} if vals.get('name', '/') == '/': vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/' if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']): defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value'] vals = dict(defaults, **vals) context.update({'mail_create_nolog': True}) new_id = super(sale_order, self).create(cr, uid, vals, context=context) self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context) return new_id def button_dummy(self, cr, uid, ids, context=None): return True # FIXME: deprecated method, overriders should be using _prepare_invoice() instead. # can be removed after 6.1. def _inv_get(self, cr, uid, order, context=None): return {} def _prepare_invoice(self, cr, uid, order, lines, context=None): """Prepare the dict of values to create the new invoice for a sales order. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). :param browse_record order: sale.order record to invoice :param list(int) line: list of invoice line IDs that must be attached to the invoice :return: dict of value to create() the invoice """ if context is None: context = {} journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)], limit=1) if not journal_ids: raise osv.except_osv(_('Error!'), _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id)) invoice_vals = { 'name': order.client_order_ref or '', 'origin': order.name, 'type': 'out_invoice', 'reference': order.client_order_ref or order.name, 'account_id': order.partner_id.property_account_receivable.id, 'partner_id': order.partner_invoice_id.id, 'journal_id': journal_ids[0], 'invoice_line': [(6, 0, lines)], 'currency_id': order.pricelist_id.currency_id.id, 'comment': order.note, 'payment_term': order.payment_term and order.payment_term.id or False, 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id, 'date_invoice': context.get('date_invoice', False), 'company_id': order.company_id.id, 'user_id': order.user_id and order.user_id.id or False } # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1 invoice_vals.update(self._inv_get(cr, uid, order, context=context)) return invoice_vals def _make_invoice(self, cr, uid, order, lines, context=None): inv_obj = self.pool.get('account.invoice') obj_invoice_line = self.pool.get('account.invoice.line') if context is None: context = {} invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context) from_line_invoice_ids = [] for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context): for invoice_line_id in invoiced_sale_line_id.invoice_lines: if invoice_line_id.invoice_id.id not in from_line_invoice_ids: from_line_invoice_ids.append(invoice_line_id.invoice_id.id) for preinv in order.invoice_ids: if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids: for preline in preinv.invoice_line: inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit}) lines.append(inv_line_id) inv = self._prepare_invoice(cr, uid, order, lines, context=context) inv_id = inv_obj.create(cr, uid, inv, context=context) data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT)) if data.get('value', False): inv_obj.write(cr, uid, [inv_id], data['value'], context=context) inv_obj.button_compute(cr, uid, [inv_id]) return inv_id def print_quotation(self, cr, uid, ids, context=None): ''' This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow ''' assert len(ids) == 1, 'This option should only be used for a single id at a time' self.signal_quotation_sent(cr, uid, ids) return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context) def manual_invoice(self, cr, uid, ids, context=None): """ create invoices for the given sales orders (ids), and open the form view of one of the newly created invoices """ mod_obj = self.pool.get('ir.model.data') # create invoices through the sales orders' workflow inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids) self.signal_manual_invoice(cr, uid, ids) inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids) # determine newly created invoices new_inv_ids = list(inv_ids1 - inv_ids0) res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') res_id = res and res[1] or False, return { 'name': _('Customer Invoices'), 'view_type': 'form', 'view_mode': 'form', 'view_id': [res_id], 'res_model': 'account.invoice', 'context': "{'type':'out_invoice'}", 'type': 'ir.actions.act_window', 'nodestroy': True, 'target': 'current', 'res_id': new_inv_ids and new_inv_ids[0] or False, } def action_view_invoice(self, cr, uid, ids, context=None): ''' 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. ''' mod_obj = self.pool.get('ir.model.data') act_obj = self.pool.get('ir.actions.act_window') result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1') id = result and result[1] or False result = act_obj.read(cr, uid, [id], context=context)[0] #compute the number of invoices to display inv_ids = [] for so in self.browse(cr, uid, ids, context=context): inv_ids += [invoice.id for invoice in so.invoice_ids] #choose the view_mode accordingly if len(inv_ids)>1: result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]" else: res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') result['views'] = [(res and res[1] or False, 'form')] result['res_id'] = inv_ids and inv_ids[0] or False return result def test_no_product(self, cr, uid, order, context): for line in order.order_line: if line.product_id and (line.product_id.type<>'service'): return False return True def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None): if states is None: states = ['confirmed', 'done', 'exception'] res = False invoices = {} invoice_ids = [] invoice = self.pool.get('account.invoice') obj_sale_order_line = self.pool.get('sale.order.line') partner_currency = {} if context is None: context = {} # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the # last day of the last month as invoice date if date_invoice: context['date_invoice'] = date_invoice for o in self.browse(cr, uid, ids, context=context): currency_id = o.pricelist_id.currency_id.id if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id): raise osv.except_osv( _('Error!'), _('You cannot group sales having different currencies for the same partner.')) partner_currency[o.partner_id.id] = currency_id lines = [] for line in o.order_line: if line.invoiced: continue elif (line.state in states): lines.append(line.id) created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines) if created_lines: invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines)) if not invoices: for o in self.browse(cr, uid, ids, context=context): for i in o.invoice_ids: if i.state == 'draft': return i.id for val in invoices.values(): if grouped: res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context) invoice_ref = '' for o, l in val: invoice_ref += o.name + '|' self.write(cr, uid, [o.id], {'state': 'progress'}) cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res)) #remove last '|' in invoice_ref if len(invoice_ref) >= 1: invoice_ref = invoice_ref[:-1] invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref}) else: for order, il in val: res = self._make_invoice(cr, uid, order, il, context=context) invoice_ids.append(res) self.write(cr, uid, [order.id], {'state': 'progress'}) cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res)) return res def action_invoice_cancel(self, cr, uid, ids, context=None): self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context) return True def action_invoice_end(self, cr, uid, ids, context=None): for this in self.browse(cr, uid, ids, context=context): for line in this.order_line: if line.state == 'exception': line.write({'state': 'confirmed'}) if this.state == 'invoice_except': this.write({'state': 'progress'}) return True def action_cancel(self, cr, uid, ids, context=None): if context is None: context = {} sale_order_line_obj = self.pool.get('sale.order.line') account_invoice_obj = self.pool.get('account.invoice') for sale in self.browse(cr, uid, ids, context=context): for inv in sale.invoice_ids: if inv.state not in ('draft', 'cancel'): raise osv.except_osv( _('Cannot cancel this sales order!'), _('First cancel all invoices attached to this sales order.')) for r in self.read(cr, uid, ids, ['invoice_ids']): account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids']) sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line], {'state': 'cancel'}) self.write(cr, uid, ids, {'state': 'cancel'}) return True def action_button_confirm(self, cr, uid, ids, context=None): assert len(ids) == 1, 'This option should only be used for a single id at a time.' self.signal_order_confirm(cr, uid, ids) # redisplay the record as a sales order view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form') view_id = view_ref and view_ref[1] or False, return { 'type': 'ir.actions.act_window', 'name': _('Sales Order'), 'res_model': 'sale.order', 'res_id': ids[0], 'view_type': 'form', 'view_mode': 'form', 'view_id': view_id, 'target': 'current', 'nodestroy': True, } def action_wait(self, cr, uid, ids, context=None): context = context or {} for o in self.browse(cr, uid, ids): if not o.order_line: raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.')) noprod = self.test_no_product(cr, uid, o, context) if (o.order_policy == 'manual') or noprod: self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) else: self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line]) return True def action_quotation_send(self, cr, uid, ids, context=None): ''' This function opens a window to compose an email, with the edi sale template message loaded by default ''' assert len(ids) == 1, 'This option should only be used for a single id at a time.' ir_model_data = self.pool.get('ir.model.data') try: template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1] except ValueError: template_id = False try: compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1] except ValueError: compose_form_id = False ctx = dict(context) ctx.update({ 'default_model': 'sale.order', 'default_res_id': ids[0], 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_composition_mode': 'comment', 'mark_so_as_sent': True }) return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form_id, 'form')], 'view_id': compose_form_id, 'target': 'new', 'context': ctx, } def action_done(self, cr, uid, ids, context=None): return self.write(cr, uid, ids, {'state': 'done'}, context=context) def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None): '''Update taxes of order lines for each line where a product is defined :param list ids: not used :param int fiscal_position: sale order fiscal position :param list order_lines: command list for one2many write method ''' order_line = [] fiscal_obj = self.pool.get('account.fiscal.position') product_obj = self.pool.get('product.product') line_obj = self.pool.get('sale.order.line') fpos = False if fiscal_position: fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context) for line in order_lines: # create (0, 0, { fields }) # update (1, ID, { fields }) if line[0] in [0, 1]: prod = None if line[2].get('product_id'): prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context) elif line[1]: prod = line_obj.browse(cr, uid, line[1], context=context).product_id if prod and prod.taxes_id: line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]] order_line.append(line) # link (4, ID) # link all (6, 0, IDS) elif line[0] in [4, 6]: line_ids = line[0] == 4 and [line[1]] or line[2] for line_id in line_ids: prod = line_obj.browse(cr, uid, line_id, context=context).product_id if prod and prod.taxes_id: order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}]) else: order_line.append([4, line_id]) else: order_line.append(line) return {'value': {'order_line': order_line}} # TODO add a field price_unit_uos # - update it on change product and unit price # - use it in report if there is a uos class sale_order_line(osv.osv): def _amount_line(self, cr, uid, ids, field_name, arg, context=None): tax_obj = self.pool.get('account.tax') cur_obj = self.pool.get('res.currency') res = {} if context is None: context = {} for line in self.browse(cr, uid, ids, context=context): price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id) cur = line.order_id.pricelist_id.currency_id res[line.id] = cur_obj.round(cr, uid, cur, taxes['total']) return res def _get_uom_id(self, cr, uid, *args): try: proxy = self.pool.get('ir.model.data') result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit') return result[1] except Exception, ex: return False def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None): res = dict.fromkeys(ids, False) for this in self.browse(cr, uid, ids, context=context): res[this.id] = this.invoice_lines and \ all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines) return res def _order_lines_from_invoice(self, cr, uid, ids, context=None): # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise) cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN sale_order_line sol ON (sol.order_id = rel.order_id) WHERE rel.invoice_id = ANY(%s)""", (list(ids),)) return [i[0] for i in cr.fetchall()] _name = 'sale.order.line' _description = 'Sales Order Line' _columns = { 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}), 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."), 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}), 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True), 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean', store={ 'account.invoice': (_order_lines_from_invoice, ['state'], 10), 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}), 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}), 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="From stock: When needed, the product is taken from the stock or we wait for replenishment.\nOn order: When needed, the product is purchased or produced."), 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')), 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}), 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."), 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}), 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}), 'product_uos': fields.many2one('product.uom', 'Product UoS'), 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}), 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}), 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True, help='* The \'Draft\' status is set when the related sales order in draft status. \ \n* The \'Confirmed\' status is set when the related sales order is confirmed. \ \n* The \'Exception\' status is set when the related sales order is set as exception. \ \n* The \'Done\' status is set when the sales order line has been picked. \ \n* The \'Cancelled\' status is set when a user cancel the sales order related.'), 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'), 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'), 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), } _order = 'order_id desc, sequence, id' _defaults = { 'product_uom' : _get_uom_id, 'discount': 0.0, 'product_uom_qty': 1, 'product_uos_qty': 1, 'sequence': 10, 'state': 'draft', 'type': 'make_to_stock', 'price_unit': 0.0, } def _get_line_qty(self, cr, uid, line, context=None): if (line.order_id.invoice_quantity=='order'): if line.product_uos: return line.product_uos_qty or 0.0 return line.product_uom_qty def _get_line_uom(self, cr, uid, line, context=None): if (line.order_id.invoice_quantity=='order'): if line.product_uos: return line.product_uos.id return line.product_uom.id def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None): """Prepare the dict of values to create the new invoice line for a sales order line. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). :param browse_record line: sale.order.line record to invoice :param int account_id: optional ID of a G/L account to force (this is used for returning products including service) :return: dict of values to create() the invoice line """ res = {} if not line.invoiced: if not account_id: if line.product_id: account_id = line.product_id.property_account_income.id if not account_id: account_id = line.product_id.categ_id.property_account_income_categ.id if not account_id: raise osv.except_osv(_('Error!'), _('Please define income account for this product: "%s" (id:%d).') % \ (line.product_id.name, line.product_id.id,)) else: prop = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context) account_id = prop and prop.id or False uosqty = self._get_line_qty(cr, uid, line, context=context) uos_id = self._get_line_uom(cr, uid, line, context=context) pu = 0.0 if uosqty: pu = round(line.price_unit * line.product_uom_qty / uosqty, self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price')) fpos = line.order_id.fiscal_position or False account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id) if not account_id: raise osv.except_osv(_('Error!'), _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.')) res = { 'name': line.name, 'sequence': line.sequence, 'origin': line.order_id.name, 'account_id': account_id, 'price_unit': pu, 'quantity': uosqty, 'discount': line.discount, 'uos_id': uos_id, 'product_id': line.product_id.id or False, 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])], 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False, } return res def invoice_line_create(self, cr, uid, ids, context=None): if context is None: context = {} create_ids = [] sales = set() for line in self.browse(cr, uid, ids, context=context): vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context) if vals: inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context) self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context) sales.add(line.order_id.id) create_ids.append(inv_id) # Trigger workflow events for sale_id in sales: workflow.trg_write(uid, 'sale.order', sale_id, cr) return create_ids def button_cancel(self, cr, uid, ids, context=None): for line in self.browse(cr, uid, ids, context=context): if line.invoiced: raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.')) return self.write(cr, uid, ids, {'state': 'cancel'}) def button_confirm(self, cr, uid, ids, context=None): return self.write(cr, uid, ids, {'state': 'confirmed'}) def button_done(self, cr, uid, ids, context=None): res = self.write(cr, uid, ids, {'state': 'done'}) for line in self.browse(cr, uid, ids, context=context): workflow.trg_write(uid, 'sale.order', line.order_id.id, cr) return res def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None): product_obj = self.pool.get('product.product') if not product_id: return {'value': {'product_uom': product_uos, 'product_uom_qty': product_uos_qty}, 'domain': {}} product = product_obj.browse(cr, uid, product_id) value = { 'product_uom': product.uom_id.id, } # FIXME must depend on uos/uom of the product and not only of the coeff. try: value.update({ 'product_uom_qty': product_uos_qty / product.uos_coeff, 'th_weight': product_uos_qty / product.uos_coeff * product.weight }) except ZeroDivisionError: pass return {'value': value} def create(self, cr, uid, values, context=None): 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']): order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context) defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'], qty=float(values.get('product_uom_qty', False)), uom=values.get('product_uom', False), qty_uos=float(values.get('product_uos_qty', False)), uos=values.get('product_uos', False), name=values.get('name', False), partner_id=order['partner_id'][0], date_order=order['date_order'], fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False, flag=False, # Force name update context=context )['value'] values = dict(defaults, **values) return super(sale_order_line, self).create(cr, uid, values, context=context) def copy_data(self, cr, uid, id, default=None, context=None): if not default: default = {} default.update({'state': 'draft', 'invoice_lines': []}) return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context) def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False, qty_uos=0, uos=False, name='', partner_id=False, lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None): context = context or {} lang = lang or context.get('lang',False) if not partner_id: raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.')) warning = {} product_uom_obj = self.pool.get('product.uom') partner_obj = self.pool.get('res.partner') product_obj = self.pool.get('product.product') context = {'lang': lang, 'partner_id': partner_id} partner = partner_obj.browse(cr, uid, partner_id) lang = partner.lang context_partner = {'lang': lang, 'partner_id': partner_id} if not product: return {'value': {'th_weight': 0, 'product_uos_qty': qty}, 'domain': {'product_uom': [], 'product_uos': []}} if not date_order: date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT) result = {} warning_msgs = '' product_obj = product_obj.browse(cr, uid, product, context=context_partner) uom2 = False if uom: uom2 = product_uom_obj.browse(cr, uid, uom) if product_obj.uom_id.category_id.id != uom2.category_id.id: uom = False if uos: if product_obj.uos_id: uos2 = product_uom_obj.browse(cr, uid, uos) if product_obj.uos_id.category_id.id != uos2.category_id.id: uos = False else: uos = False fpos = False if not fiscal_position: fpos = partner.property_account_position or False else: fpos = self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) if update_tax: #The quantity only have changed result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id) if not flag: result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1] if product_obj.description_sale: result['name'] += '\n'+product_obj.description_sale domain = {} if (not uom) and (not uos): result['product_uom'] = product_obj.uom_id.id if product_obj.uos_id: result['product_uos'] = product_obj.uos_id.id result['product_uos_qty'] = qty * product_obj.uos_coeff uos_category_id = product_obj.uos_id.category_id.id else: result['product_uos'] = False result['product_uos_qty'] = qty uos_category_id = False result['th_weight'] = qty * product_obj.weight domain = {'product_uom': [('category_id', '=', product_obj.uom_id.category_id.id)], 'product_uos': [('category_id', '=', uos_category_id)]} elif uos and not uom: # only happens if uom is False result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id result['product_uom_qty'] = qty_uos / product_obj.uos_coeff result['th_weight'] = result['product_uom_qty'] * product_obj.weight elif uom: # whether uos is set or not default_uom = product_obj.uom_id and product_obj.uom_id.id q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom) if product_obj.uos_id: result['product_uos'] = product_obj.uos_id.id result['product_uos_qty'] = qty * product_obj.uos_coeff else: result['product_uos'] = False result['product_uos_qty'] = qty result['th_weight'] = q * product_obj.weight # Round the quantity up if not uom2: uom2 = product_obj.uom_id # get unit price if not pricelist: warn_msg = _('You have to select a pricelist or a customer in the sales form !\n' 'Please set one before choosing a product.') warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n" else: price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist], product, qty or 1.0, partner_id, { 'uom': uom or result.get('product_uom'), 'date': date_order, })[pricelist] if price is False: warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n" "You have to change either the product, the quantity or the pricelist.") warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n" else: result.update({'price_unit': price}) if warning_msgs: warning = { 'title': _('Configuration Error!'), 'message' : warning_msgs } return {'value': result, 'domain': domain, 'warning': warning} def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0, uom=False, qty_uos=0, uos=False, name='', partner_id=False, lang=False, update_tax=True, date_order=False, context=None): context = context or {} lang = lang or ('lang' in context and context['lang']) if not uom: return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}} return self.product_id_change(cursor, user, ids, pricelist, product, qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id, lang=lang, update_tax=update_tax, date_order=date_order, context=context) def unlink(self, cr, uid, ids, context=None): if context is None: context = {} """Allows to delete sales order lines in draft,cancel states""" for rec in self.browse(cr, uid, ids, context=context): if rec.state not in ['draft', 'cancel']: raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,)) return super(sale_order_line, self).unlink(cr, uid, ids, context=context) class res_company(osv.Model): _inherit = "res.company" _columns = { 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."), } class mail_compose_message(osv.Model): _inherit = 'mail.compose.message' def send_mail(self, cr, uid, ids, context=None): context = context or {} if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'): context = dict(context, mail_post_autofollow=True) self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']]) return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context) class account_invoice(osv.Model): _inherit = 'account.invoice' def confirm_paid(self, cr, uid, ids, context=None): sale_order_obj = self.pool.get('sale.order') res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context) so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context) for so_id in so_ids: sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context) return res def unlink(self, cr, uid, ids, context=None): """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """ invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context) #if we can't cancel all invoices, do nothing if len(invoice_ids) == len(ids): #Cancel invoice(s) first before deleting them so that if any sale order is associated with them #it will trigger the workflow to put the sale order in an 'invoice exception' state for id in ids: workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr) return super(account_invoice, self).unlink(cr, uid, ids, context=context) class product_product(osv.Model): _inherit = 'product.product' def _sales_count(self, cr, uid, ids, field_name, arg, context=None): SaleOrderLine = self.pool['sale.order.line'] return { product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context) for product_id in ids } _columns = { 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'), } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: