# -*- 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 import pooler from osv import fields, osv from tools.translate import _ from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare import decimal_precision as dp import netsvc class sale_shop(osv.osv): _name = "sale.shop" _description = "Sales Shop" _columns = { 'name': fields.char('Shop Name', size=64, required=True), 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'), 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]), 'company_id': fields.many2one('res.company', 'Company', required=False), } _defaults = { 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c), } sale_shop() class sale_order(osv.osv): _name = "sale.order" _inherit = ['ir.needaction_mixin', 'mail.thread'] _description = "Sales Order" def copy(self, cr, uid, id, default=None, context=None): if not default: default = {} default.update({ 'state': 'draft', 'invoice_ids': [], 'date_confirm': False, '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(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 _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: 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() _columns = { 'name': fields.char('Order Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True), 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."), 'client_order_ref': fields.char('Customer Reference', size=64), 'state': fields.selection([ ('draft', 'Draft Quotation'), ('sent', 'Quotation Sent'), ('cancel', 'Cancelled'), ('waiting_date', 'Waiting Schedule'), ('progress', 'Sale Order'), ('invoice_except', 'Invoice Exception'), ('done', 'Done'), ], 'Status', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception). \nThe 'Waiting Schedule' state 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), 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True), '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', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Shipping address for current sales order."), 'order_policy': fields.selection([ ('prepaid', 'Before Delivery'), ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="""This field controls how invoice and delivery operations are synchronized. - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."), 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."), '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', type='float'), 'invoiced': fields.function(_invoiced, string='Paid', fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."), 'note': fields.text('Terms and conditions'), 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), 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."), 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), 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, digits_compute= dp.get_precision('Sale Price'), 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 sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True, readonly=True, states={'draft': [('readonly', False)]}), 'payment_term': fields.many2one('account.payment.term', 'Payment Term'), 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'), 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True) } _defaults = { 'date_order': fields.date.context_today, 'order_policy': 'prepaid', 'state': 'draft', 'user_id': lambda obj, cr, uid, context: uid, 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'), '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'], } _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'), ] _order = 'name 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 sale order, you must cancel it before !')) return osv.osv.unlink(self, cr, uid, unlink_ids, context=context) def onchange_shop_id(self, cr, uid, ids, shop_id): v = {} if shop_id: shop = self.pool.get('sale.shop').browse(cr, uid, shop_id) v['project_id'] = shop.project_id.id # Que faire si le client a une pricelist a lui ? if shop.pricelist_id.id: v['pricelist_id'] = shop.pricelist_id.id return {'value': v} def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}): if (not pricelist_id) or (not order_lines): return {} 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} def onchange_partner_id(self, cr, uid, ids, part): if not part: return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}} addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact']) part = self.pool.get('res.partner').browse(cr, uid, part) 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 return {'value': val} def create(self, cr, uid, vals, context=None): order = super(sale_order, self).create(cr, uid, vals, context=context) if order: self.create_send_note(cr, uid, [order], context=context) return order 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 sale 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 !'), _('There is no sales journal defined 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_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 sale 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' wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr) datas = { 'model': 'sale.order', 'ids': ids, 'form': self.read(cr, uid, ids[0], context=context), } return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True} def manual_invoice(self, cr, uid, ids, context=None): """ create invoices for the given sale orders (ids), and open the form view of one of the newly created invoices """ mod_obj = self.pool.get('ir.model.data') wf_service = netsvc.LocalService("workflow") # create invoices through the sale orders' workflow inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids) for id in ids: wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr) 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 sale 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') result = { 'name': _('Cutomer Invoice'), 'view_type': 'form', 'res_model': 'account.invoice', 'context': "{'type':'out_invoice', 'journal_type': 'sale'}", 'type': 'ir.actions.act_window', 'nodestroy': True, 'target': 'current', } #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: res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_tree') result.update({ 'view_mode': 'tree,form', 'res_id': inv_ids or False }) else: res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') result.update({ 'view_mode': 'form', 'res_id': inv_ids and inv_ids[0] or False, }) result.update(view_id = res and res[1] or False) return result def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None): 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_inv: context['date_inv'] = date_inv 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_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)) 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)) if res: self.invoice_send_note(cr, uid, ids, res, context) return res def action_invoice_cancel(self, cr, uid, ids, context=None): if context is None: context = {} for sale in self.browse(cr, uid, ids, context=context): for line in sale.order_line: # # Check if the line is invoiced (has asociated invoice # lines from non-cancelled invoices). # invoiced = False for iline in line.invoice_lines: if iline.invoice_id and iline.invoice_id.state != 'cancel': invoiced = True break # Update the line (only when needed) if line.invoiced != invoiced: self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context) self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context) return True def action_invoice_end(self, cr, uid, ids, context=None): for order in self.browse(cr, uid, ids, context=context): # # Update the sale order lines state (and invoiced flag). # for line in order.order_line: vals = {} # # Check if the line is invoiced (has asociated invoice # lines from non-cancelled invoices). # invoiced = False for iline in line.invoice_lines: if iline.invoice_id and iline.invoice_id.state != 'cancel': invoiced = True break if line.invoiced != invoiced: vals['invoiced'] = invoiced # If the line was in exception state, now it gets confirmed. if line.state == 'exception': vals['state'] = 'confirmed' # Update the line (only when needed). if vals: self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context) # # Update the sales order state. # if order.state == 'invoice_except': self.write(cr, uid, [order.id], {'state': 'progress'}, context=context) return True def action_cancel(self, cr, uid, ids, context=None): wf_service = netsvc.LocalService("workflow") if context is None: context = {} sale_order_line_obj = self.pool.get('sale.order.line') 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( _('Could not cancel this sales order !'), _('You must first cancel all invoices attached to this sales order.')) for r in self.read(cr, uid, ids, ['invoice_ids']): for inv in r['invoice_ids']: wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr) sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line], {'state': 'cancel'}) self.cancel_send_note(cr, uid, [sale.id], context=None) 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' wf_service = netsvc.LocalService('workflow') wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr) # redisplay the record as a sale 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): for o in self.browse(cr, uid, ids): if not o.order_line: raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.')) 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]) self.confirm_send_note(cr, uid, ids, context) 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' mod_obj = self.pool.get('ir.model.data') template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale') template_id = template and template[1] or False res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form') res_id = res and res[1] or False ctx = dict(context, active_model='sale.order', active_id=ids[0]) ctx.update({'mail.compose.template_id': template_id}) return { 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(res_id,'form')], 'view_id': res_id, 'type': 'ir.actions.act_window', 'target': 'new', 'context': ctx, 'nodestroy': True, } # ------------------------------------------------ # OpenChatter methods and notifications # ------------------------------------------------ def get_needaction_user_ids(self, cr, uid, ids, context=None): result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context) for obj in self.browse(cr, uid, ids, context=context): if (obj.state == 'progress'): result[obj.id].append(obj.user_id.id) return result def create_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context) self.message_append_note(cr, uid, [obj.id], body=_("Quotation for %s has been created.") % (obj.partner_id.name), context=context) def confirm_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): self.message_append_note(cr, uid, [obj.id], body=_("Quotation for %s converted to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context) def cancel_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for %s cancelled.") % (obj.partner_id.name), context=context) def invoice_paid_send_note(self, cr, uid, ids, context=None): self.message_append_note(cr, uid, ids, body=_("Invoice paid."), context=context) def invoice_send_note(self, cr, uid, ids, invoice_id, context=None): for order in self.browse(cr, uid, ids, context=context): for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id): self.message_append_note(cr, uid, [order.id], body=_("Draft Invoice of %s %s waiting for validation.") % (invoice.amount_total, invoice.currency_id.symbol), context=context) sale_order() # 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 _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('Product Description', size=256, required=True, select=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), 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True), 'invoiced': fields.boolean('Invoiced', readonly=True), 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}), 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')), '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'), '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=(16, 2), 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\' state is set when the related sales order in draft state. \ \n* The \'Confirmed\' state is set when the related sales order is confirmed. \ \n* The \'Exception\' state is set when the related sales order is set as exception. \ \n* The \'Done\' state is set when the sales order line has been picked. \ \n* The \'Cancelled\' state 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 = 'sequence, id' _defaults = { 'product_uom' : _get_uom_id, 'discount': 0.0, 'product_uom_qty': 1, 'product_uos_qty': 1, 'sequence': 10, 'invoiced': 0, 'state': 'draft', 'price_unit': 0.0, } 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 sale 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 """ def _get_line_qty(line): 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(line): if (line.order_id.invoice_quantity=='order'): if line.product_uos: return line.product_uos.id return line.product_uom.id if not line.invoiced: if not account_id: if line.product_id: account_id = line.product_id.product_tmpl_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 !'), _('There is no income account defined 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 = _get_line_qty(line) uos_id = _get_line_uom(line) pu = 0.0 if uosqty: pu = round(line.price_unit * line.product_uom_qty / uosqty, self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale 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 income category account defined in default Properties for Product Category or Fiscal Position is not defined !')) return { 'name': line.name, '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 False 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) cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id)) self.write(cr, uid, [line.id], {'invoiced': True}) sales.add(line.order_id.id) create_ids.append(inv_id) # Trigger workflow events wf_service = netsvc.LocalService("workflow") for sale_id in sales: wf_service.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 sale order line that has already been invoiced!')) for move_line in line.move_ids: if move_line.state != 'cancel': raise osv.except_osv( _('Could not cancel sales order line!'), _('You must first cancel stock moves attached to this sales order line.')) 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): wf_service = netsvc.LocalService("workflow") res = self.write(cr, uid, ids, {'state': 'done'}) for line in self.browse(cr, uid, ids, context=context): wf_service.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 copy_data(self, cr, uid, id, default=None, context=None): if not default: default = {} default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, '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 !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.')) 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} if partner_id: lang = partner_obj.browse(cr, uid, partner_id).lang context_partner = {'lang': lang, 'partner_id': partner_id} if not product: return {'value': {'th_weight': 0, 'product_packaging': False, 'product_uos_qty': qty}, 'domain': {'product_uom': [], 'product_uos': []}} if not date_order: date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT) res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context) result = res.get('value', {}) warning_msgs = res.get('warning') and res['warning']['message'] or '' product_obj = product_obj.browse(cr, uid, product, context=context) 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 = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False if update_tax: #The quantity only have changed result['delay'] = (product_obj.sale_delay or 0.0) result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id) result.update({'type': product_obj.procure_method}) 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 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding) if (product_obj.type=='product') and int(compare_qty) == -1 \ and (product_obj.procure_method=='make_to_stock'): warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \ (qty, uom2 and uom2.name or product_obj.uom_id.name, max(0,product_obj.virtual_available), product_obj.uom_id.name, max(0,product_obj.qty_available), product_obj.uom_id.name) warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n" # 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 = _("Couldn't 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']) res = 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) if 'product_uom' in res['value']: del res['value']['product_uom'] if not uom: res['value']['price_unit'] = 0.0 return res 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) sale_order_line() class mail_message(osv.osv): _inherit = 'mail.message' def _postprocess_sent_message(self, cr, uid, message, context=None): if message.model == 'sale.order': wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'sale.order', message.res_id, 'quotation_sent', cr) return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context) mail_message() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: