1302 lines
68 KiB
Python
1302 lines
68 KiB
Python
# -*- coding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# OpenERP, Open Source Management Solution
|
|
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
|
|
from datetime import datetime, timedelta
|
|
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
|
|
import openerp.addons.decimal_precision as dp
|
|
from openerp import workflow
|
|
|
|
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 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 _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] == '=' and arg[2]) or (arg[1] == '!=' and not 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
|
|
|
|
def _get_default_section_id(self, cr, uid, context=None):
|
|
""" Gives default section by checking if present in the context """
|
|
section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
|
|
if not section_id:
|
|
section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
|
|
return section_id
|
|
|
|
def _resolve_section_id_from_context(self, cr, uid, context=None):
|
|
""" Returns ID of section based on the value of 'section_id'
|
|
context key, or None if it cannot be resolved to a single
|
|
Sales Team.
|
|
"""
|
|
if context is None:
|
|
context = {}
|
|
if type(context.get('default_section_id')) in (int, long):
|
|
return context.get('default_section_id')
|
|
if isinstance(context.get('default_section_id'), basestring):
|
|
section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
|
|
if len(section_ids) == 1:
|
|
return int(section_ids[0][0])
|
|
return None
|
|
|
|
_columns = {
|
|
'name': fields.char('Order Reference', required=True, copy=False,
|
|
readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
|
|
'origin': fields.char('Source Document', help="Reference of the document that generated this sales order request."),
|
|
'client_order_ref': fields.char('Reference/Description', copy=False),
|
|
'state': fields.selection([
|
|
('draft', 'Draft Quotation'),
|
|
('sent', 'Quotation Sent'),
|
|
('cancel', 'Cancelled'),
|
|
('waiting_date', 'Waiting Schedule'),
|
|
('progress', 'Sales Order'),
|
|
('manual', 'Sale to Invoice'),
|
|
('shipping_except', 'Shipping Exception'),
|
|
('invoice_except', 'Invoice Exception'),
|
|
('done', 'Done'),
|
|
], 'Status', readonly=True, copy=False, help="Gives the status of the quotation or sales order.\
|
|
\nThe exception status is automatically set when a cancel operation occurs \
|
|
in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
|
|
but waiting for the scheduler to run on the order date.", select=True),
|
|
'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=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.", copy=False),
|
|
'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)]}, copy=True),
|
|
'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)."),
|
|
'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."),
|
|
|
|
'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'),
|
|
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
|
|
'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', copy=False),
|
|
'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
|
|
}
|
|
|
|
_defaults = {
|
|
'date_order': fields.datetime.now,
|
|
'order_policy': 'manual',
|
|
'company_id': _get_default_company,
|
|
'state': 'draft',
|
|
'user_id': lambda obj, cr, uid, context: uid,
|
|
'name': lambda obj, cr, uid, context: '/',
|
|
'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,
|
|
'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
|
|
}
|
|
_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=context)
|
|
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 or order_lines == [(6, 0, [])]:
|
|
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_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position, context=None):
|
|
r = {'value': {}}
|
|
if not fiscal_position:
|
|
if not company_id:
|
|
company_id = self._get_default_company(cr, uid, context=context)
|
|
fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, delivery_id, context=context)
|
|
if fiscal_position:
|
|
r['value']['fiscal_position'] = fiscal_position
|
|
return r
|
|
|
|
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
|
|
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,
|
|
'user_id': dedicated_salesman,
|
|
}
|
|
delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False, context=context)
|
|
val.update(delivery_onchange['value'])
|
|
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', 'fiscal_position']):
|
|
defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value']
|
|
if not vals.get('fiscal_position') and vals.get('partner_shipping_id'):
|
|
delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context)
|
|
defaults.update(delivery_onchange['value'])
|
|
vals = dict(defaults, **vals)
|
|
ctx = dict(context or {}, mail_create_nolog=True)
|
|
new_id = super(sale_order, self).create(cr, uid, vals, context=ctx)
|
|
self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=ctx)
|
|
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,
|
|
'section_id' : order.section_id.id
|
|
}
|
|
|
|
# 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_workflow(cr, uid, ids, 'quotation_sent')
|
|
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_workflow(cr, uid, ids, 'manual_invoice')
|
|
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 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 = dict(context or {}, 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 = ''
|
|
origin_ref = ''
|
|
for o, l in val:
|
|
invoice_ref += (o.client_order_ref or o.name) + '|'
|
|
origin_ref += (o.origin or 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))
|
|
self.invalidate_cache(cr, uid, ['invoice_ids'], [o.id], context=context)
|
|
#remove last '|' in invoice_ref
|
|
if len(invoice_ref) >= 1:
|
|
invoice_ref = invoice_ref[:-1]
|
|
if len(origin_ref) >= 1:
|
|
origin_ref = origin_ref[:-1]
|
|
invoice.write(cr, uid, [res], {'origin': origin_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))
|
|
self.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context)
|
|
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.'))
|
|
inv.signal_workflow('invoice_cancel')
|
|
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_workflow(cr, uid, ids, 'order_confirm')
|
|
return 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()
|
|
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):
|
|
for order in self.browse(cr, uid, ids, context=context):
|
|
self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
|
|
return self.write(cr, uid, ids, {'state': 'done'}, context=context)
|
|
|
|
def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
|
|
date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
|
|
return {
|
|
'name': line.name,
|
|
'origin': order.name,
|
|
'date_planned': date_planned,
|
|
'product_id': line.product_id.id,
|
|
'product_qty': line.product_uom_qty,
|
|
'product_uom': line.product_uom.id,
|
|
'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
|
|
'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
|
|
'company_id': order.company_id.id,
|
|
'group_id': group_id,
|
|
'invoice_state': (order.order_policy == 'picking') and '2binvoiced' or 'none',
|
|
'sale_line_id': line.id
|
|
}
|
|
|
|
def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
|
|
date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
|
|
return date_planned
|
|
|
|
def _prepare_procurement_group(self, cr, uid, order, context=None):
|
|
return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
|
|
|
|
def procurement_needed(self, cr, uid, ids, context=None):
|
|
#when sale is installed only, there is no need to create procurements, that's only
|
|
#further installed modules (sale_service, sale_stock) that will change this.
|
|
sale_line_obj = self.pool.get('sale.order.line')
|
|
res = []
|
|
for order in self.browse(cr, uid, ids, context=context):
|
|
res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
|
|
return any(res)
|
|
|
|
def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
|
|
for sale_order in self.browse(cr, uid, ids, context=context):
|
|
self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
|
|
return True
|
|
|
|
def action_ship_create(self, cr, uid, ids, context=None):
|
|
"""Create the required procurements to supply sales order lines, also connecting
|
|
the procurements to appropriate stock moves in order to bring the goods to the
|
|
sales order's requested location.
|
|
|
|
:return: True
|
|
"""
|
|
procurement_obj = self.pool.get('procurement.order')
|
|
sale_line_obj = self.pool.get('sale.order.line')
|
|
for order in self.browse(cr, uid, ids, context=context):
|
|
proc_ids = []
|
|
vals = self._prepare_procurement_group(cr, uid, order, context=context)
|
|
if not order.procurement_group_id:
|
|
group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
|
|
order.write({'procurement_group_id': group_id})
|
|
|
|
for line in order.order_line:
|
|
#Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
|
|
if line.procurement_ids:
|
|
#first check them to see if they are in exception or not (one of the related moves is cancelled)
|
|
procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
|
|
line.refresh()
|
|
#run again procurement that are in exception in order to trigger another move
|
|
proc_ids += [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
|
|
procurement_obj.reset_to_confirmed(cr, uid, proc_ids, context=context)
|
|
elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
|
|
if (line.state == 'done') or not line.product_id:
|
|
continue
|
|
vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=order.procurement_group_id.id, context=context)
|
|
proc_id = procurement_obj.create(cr, uid, vals, context=context)
|
|
proc_ids.append(proc_id)
|
|
#Confirm procurement order such that rules will be applied on it
|
|
#note that the workflow normally ensure proc_ids isn't an empty list
|
|
procurement_obj.run(cr, uid, proc_ids, context=context)
|
|
|
|
#if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
|
|
if order.state == 'shipping_except':
|
|
val = {'state': 'progress', 'shipped': False}
|
|
|
|
if (order.order_policy == 'manual'):
|
|
for line in order.order_line:
|
|
if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
|
|
val['state'] = 'manual'
|
|
break
|
|
order.write(val)
|
|
return True
|
|
|
|
|
|
|
|
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}}
|
|
|
|
def test_procurements_done(self, cr, uid, ids, context=None):
|
|
for sale in self.browse(cr, uid, ids, context=context):
|
|
for line in sale.order_line:
|
|
if not all([x.state == 'done' for x in line.procurement_ids]):
|
|
return False
|
|
return True
|
|
|
|
def test_procurements_except(self, cr, uid, ids, context=None):
|
|
for sale in self.browse(cr, uid, ids, context=context):
|
|
for line in sale.order_line:
|
|
if any([x.state == 'cancel' for x in line.procurement_ids]):
|
|
return True
|
|
return False
|
|
|
|
|
|
# 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 need_procurement(self, cr, uid, ids, context=None):
|
|
#when sale is installed only, there is no need to create procurements, that's only
|
|
#further installed modules (sale_service, sale_stock) that will change this.
|
|
prod_obj = self.pool.get('product.product')
|
|
for line in self.browse(cr, uid, ids, context=context):
|
|
if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
|
|
return True
|
|
return False
|
|
|
|
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()]
|
|
|
|
def _get_price_reduce(self, cr, uid, ids, field_name, arg, context=None):
|
|
res = dict.fromkeys(ids, 0.0)
|
|
for line in self.browse(cr, uid, ids, context=context):
|
|
res[line.id] = line.price_subtotal / line.product_uom_qty
|
|
return res
|
|
|
|
_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)]}, ondelete='restrict'),
|
|
'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True, copy=False),
|
|
'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)]}),
|
|
'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
|
|
'price_reduce': fields.function(_get_price_reduce, type='float', string='Price Reduce', digits_compute=dp.get_precision('Product 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',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, copy=False,
|
|
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),
|
|
'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)]}),
|
|
'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
|
|
}
|
|
_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',
|
|
'price_unit': 0.0,
|
|
'delay': 0.0,
|
|
}
|
|
|
|
|
|
|
|
def _get_line_qty(self, cr, uid, line, context=None):
|
|
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.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']
|
|
if defaults.get('tax_id'):
|
|
defaults['tax_id'] = [[6, 0, defaults['tax_id']]]
|
|
values = dict(defaults, **values)
|
|
return super(sale_order_line, self).create(cr, uid, values, 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 = False
|
|
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 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_workflow(cr, uid, [context['default_res_id']], 'quotation_sent')
|
|
return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
|
|
|
|
|
|
class account_invoice(osv.Model):
|
|
_inherit = 'account.invoice'
|
|
|
|
def _get_default_section_id(self, cr, uid, context=None):
|
|
""" Gives default section by checking if present in the context """
|
|
section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
|
|
if not section_id:
|
|
section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
|
|
return section_id
|
|
|
|
def _resolve_section_id_from_context(self, cr, uid, context=None):
|
|
""" Returns ID of section based on the value of 'section_id'
|
|
context key, or None if it cannot be resolved to a single
|
|
Sales Team.
|
|
"""
|
|
if context is None:
|
|
context = {}
|
|
if type(context.get('default_section_id')) in (int, long):
|
|
return context.get('default_section_id')
|
|
if isinstance(context.get('default_section_id'), basestring):
|
|
section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
|
|
if len(section_ids) == 1:
|
|
return int(section_ids[0][0])
|
|
return None
|
|
|
|
_columns = {
|
|
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
|
|
}
|
|
|
|
_defaults = {
|
|
'section_id': lambda self, cr, uid, c=None: self._get_default_section_id(cr, uid, context=c)
|
|
}
|
|
|
|
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 procurement_order(osv.osv):
|
|
_inherit = 'procurement.order'
|
|
_columns = {
|
|
'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
|
|
}
|
|
|
|
def write(self, cr, uid, ids, vals, context=None):
|
|
if isinstance(ids, (int, long)):
|
|
ids = [ids]
|
|
res = super(procurement_order, self).write(cr, uid, ids, vals, context=context)
|
|
from openerp import workflow
|
|
if vals.get('state') in ['done', 'cancel', 'exception']:
|
|
for proc in self.browse(cr, uid, ids, context=context):
|
|
if proc.sale_line_id and proc.sale_line_id.order_id:
|
|
order_id = proc.sale_line_id.order_id.id
|
|
if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context):
|
|
workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr)
|
|
if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context):
|
|
workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr)
|
|
return res
|
|
|
|
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'),
|
|
|
|
}
|
|
|
|
class product_template(osv.Model):
|
|
_inherit = 'product.template'
|
|
|
|
def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
|
|
res = dict.fromkeys(ids, 0)
|
|
for template in self.browse(cr, uid, ids, context=context):
|
|
res[template.id] = sum([p.sales_count for p in template.product_variant_ids])
|
|
return res
|
|
|
|
def action_view_sales(self, cr, uid, ids, context=None):
|
|
act_obj = self.pool.get('ir.actions.act_window')
|
|
mod_obj = self.pool.get('ir.model.data')
|
|
product_ids = []
|
|
for template in self.browse(cr, uid, ids, context=context):
|
|
product_ids += [x.id for x in template.product_variant_ids]
|
|
result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree',raise_if_not_found=True)
|
|
result = act_obj.read(cr, uid, [result], context=context)[0]
|
|
result['domain'] = "[('product_id','in',[" + ','.join(map(str, product_ids)) + "])]"
|
|
return result
|
|
|
|
|
|
_columns = {
|
|
'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
|
|
|
|
}
|
|
|
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|