1030 lines
51 KiB
Python
1030 lines
51 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from itertools import groupby
|
|
from datetime import datetime, timedelta
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import float_is_zero, float_compare, DEFAULT_SERVER_DATETIME_FORMAT
|
|
from odoo.tools.misc import formatLang
|
|
|
|
import odoo.addons.decimal_precision as dp
|
|
|
|
|
|
class SaleOrder(models.Model):
|
|
_name = "sale.order"
|
|
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
|
_description = "Sales Order"
|
|
_order = 'date_order desc, id desc'
|
|
|
|
@api.depends('order_line.price_total')
|
|
def _amount_all(self):
|
|
"""
|
|
Compute the total amounts of the SO.
|
|
"""
|
|
for order in self:
|
|
amount_untaxed = amount_tax = 0.0
|
|
for line in order.order_line:
|
|
amount_untaxed += line.price_subtotal
|
|
# FORWARDPORT UP TO 10.0
|
|
if order.company_id.tax_calculation_rounding_method == 'round_globally':
|
|
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
|
taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=order.partner_shipping_id)
|
|
amount_tax += sum(t.get('amount', 0.0) for t in taxes.get('taxes', []))
|
|
else:
|
|
amount_tax += line.price_tax
|
|
order.update({
|
|
'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed),
|
|
'amount_tax': order.pricelist_id.currency_id.round(amount_tax),
|
|
'amount_total': amount_untaxed + amount_tax,
|
|
})
|
|
|
|
@api.depends('state', 'order_line.invoice_status')
|
|
def _get_invoiced(self):
|
|
"""
|
|
Compute the invoice status of a SO. Possible statuses:
|
|
- no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to
|
|
invoice. This is also hte default value if the conditions of no other status is met.
|
|
- to invoice: if any SO line is 'to invoice', the whole SO is 'to invoice'
|
|
- invoiced: if all SO lines are invoiced, the SO is invoiced.
|
|
- upselling: if all SO lines are invoiced or upselling, the status is upselling.
|
|
|
|
The invoice_ids are obtained thanks to the invoice lines of the SO lines, and we also search
|
|
for possible refunds created directly from existing invoices. This is necessary since such a
|
|
refund is not directly linked to the SO.
|
|
"""
|
|
for order in self:
|
|
invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
|
|
# Search for invoices which have been 'cancelled' (filter_refund = 'modify' in
|
|
# 'account.invoice.refund')
|
|
# use like as origin may contains multiple references (e.g. 'SO01, SO02')
|
|
refunds = invoice_ids.search([('origin', 'like', order.name), ('company_id', '=', order.company_id.id)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
|
|
invoice_ids |= refunds.filtered(lambda r: order.name in [origin.strip() for origin in r.origin.split(',')])
|
|
# Search for refunds as well
|
|
refund_ids = self.env['account.invoice'].browse()
|
|
if invoice_ids:
|
|
for inv in invoice_ids:
|
|
refund_ids += refund_ids.search([('type', '=', 'out_refund'), ('origin', '=', inv.number), ('origin', '!=', False), ('journal_id', '=', inv.journal_id.id)])
|
|
|
|
# Ignore the status of the deposit product
|
|
deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
|
|
line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id]
|
|
|
|
if order.state not in ('sale', 'done'):
|
|
invoice_status = 'no'
|
|
elif any(invoice_status == 'to invoice' for invoice_status in line_invoice_status):
|
|
invoice_status = 'to invoice'
|
|
elif all(invoice_status == 'invoiced' for invoice_status in line_invoice_status):
|
|
invoice_status = 'invoiced'
|
|
elif all(invoice_status in ['invoiced', 'upselling'] for invoice_status in line_invoice_status):
|
|
invoice_status = 'upselling'
|
|
else:
|
|
invoice_status = 'no'
|
|
|
|
order.update({
|
|
'invoice_count': len(set(invoice_ids.ids + refund_ids.ids)),
|
|
'invoice_ids': invoice_ids.ids + refund_ids.ids,
|
|
'invoice_status': invoice_status
|
|
})
|
|
|
|
@api.model
|
|
def _default_note(self):
|
|
return self.env.user.company_id.sale_note
|
|
|
|
@api.model
|
|
def _get_default_team(self):
|
|
return self.env['crm.team']._get_default_team_id()
|
|
|
|
@api.onchange('fiscal_position_id')
|
|
def _compute_tax_id(self):
|
|
"""
|
|
Trigger the recompute of the taxes if the fiscal position is changed on the SO.
|
|
"""
|
|
for order in self:
|
|
order.order_line._compute_tax_id()
|
|
|
|
def _inverse_project_id(self):
|
|
self.project_id = self.related_project_id
|
|
|
|
name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New'))
|
|
origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.")
|
|
client_order_ref = fields.Char(string='Customer Reference', copy=False)
|
|
|
|
state = fields.Selection([
|
|
('draft', 'Quotation'),
|
|
('sent', 'Quotation Sent'),
|
|
('sale', 'Sales Order'),
|
|
('done', 'Locked'),
|
|
('cancel', 'Cancelled'),
|
|
], string='Status', readonly=True, copy=False, index=True, track_visibility='onchange', default='draft')
|
|
date_order = fields.Datetime(string='Order Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Datetime.now)
|
|
validity_date = fields.Date(string='Expiration Date', readonly=True, copy=False, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
|
help="Manually set the expiration date of your quotation (offer), or it will set the date automatically based on the template if online quotation is installed.")
|
|
create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.")
|
|
confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the sale order is confirmed.", oldname="date_confirm", copy=False)
|
|
user_id = fields.Many2one('res.users', string='Salesperson', index=True, track_visibility='onchange', default=lambda self: self.env.user)
|
|
partner_id = fields.Many2one('res.partner', string='Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, index=True, track_visibility='always')
|
|
partner_invoice_id = fields.Many2one('res.partner', string='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', string='Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order.")
|
|
|
|
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order.")
|
|
currency_id = fields.Many2one("res.currency", related='pricelist_id.currency_id', string="Currency", readonly=True, required=True)
|
|
project_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order.", copy=False)
|
|
related_project_id = fields.Many2one('account.analytic.account', inverse='_inverse_project_id', related='project_id', string='Analytic Account', help="The analytic account related to a sales order.")
|
|
|
|
order_line = fields.One2many('sale.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
|
|
|
|
invoice_count = fields.Integer(string='# of Invoices', compute='_get_invoiced', readonly=True)
|
|
invoice_ids = fields.Many2many("account.invoice", string='Invoices', compute="_get_invoiced", readonly=True, copy=False)
|
|
invoice_status = fields.Selection([
|
|
('upselling', 'Upselling Opportunity'),
|
|
('invoiced', 'Fully Invoiced'),
|
|
('to invoice', 'To Invoice'),
|
|
('no', 'Nothing to Invoice')
|
|
], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True)
|
|
|
|
note = fields.Text('Terms and conditions', default=_default_note)
|
|
|
|
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always')
|
|
amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all', track_visibility='always')
|
|
amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all', track_visibility='always')
|
|
|
|
payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term')
|
|
fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position')
|
|
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('sale.order'))
|
|
team_id = fields.Many2one('crm.team', 'Sales Team', change_default=True, default=_get_default_team, oldname='section_id')
|
|
procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False)
|
|
|
|
product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
|
|
|
|
@api.model
|
|
def _get_customer_lead(self, product_tmpl_id):
|
|
return False
|
|
|
|
@api.multi
|
|
def button_dummy(self):
|
|
return True
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
for order in self:
|
|
if order.state not in ('draft', 'cancel'):
|
|
raise UserError(_('You can not delete a sent quotation or a sales order! Try to cancel it before.'))
|
|
return super(SaleOrder, self).unlink()
|
|
|
|
@api.multi
|
|
def _track_subtype(self, init_values):
|
|
self.ensure_one()
|
|
if 'state' in init_values and self.state == 'sale':
|
|
return 'sale.mt_order_confirmed'
|
|
elif 'state' in init_values and self.state == 'sent':
|
|
return 'sale.mt_order_sent'
|
|
return super(SaleOrder, self)._track_subtype(init_values)
|
|
|
|
@api.multi
|
|
@api.onchange('partner_shipping_id', 'partner_id')
|
|
def onchange_partner_shipping_id(self):
|
|
"""
|
|
Trigger the change of fiscal position when the shipping address is modified.
|
|
"""
|
|
self.fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, self.partner_shipping_id.id)
|
|
return {}
|
|
|
|
@api.multi
|
|
@api.onchange('partner_id')
|
|
def onchange_partner_id(self):
|
|
"""
|
|
Update the following fields when the partner is changed:
|
|
- Pricelist
|
|
- Payment term
|
|
- Invoice address
|
|
- Delivery address
|
|
"""
|
|
if not self.partner_id:
|
|
self.update({
|
|
'partner_invoice_id': False,
|
|
'partner_shipping_id': False,
|
|
'payment_term_id': False,
|
|
'fiscal_position_id': False,
|
|
})
|
|
return
|
|
|
|
addr = self.partner_id.address_get(['delivery', 'invoice'])
|
|
values = {
|
|
'pricelist_id': self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False,
|
|
'payment_term_id': self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False,
|
|
'partner_invoice_id': addr['invoice'],
|
|
'partner_shipping_id': addr['delivery'],
|
|
}
|
|
if self.env.user.company_id.sale_note:
|
|
values['note'] = self.with_context(lang=self.partner_id.lang).env.user.company_id.sale_note
|
|
|
|
if self.partner_id.user_id:
|
|
values['user_id'] = self.partner_id.user_id.id
|
|
if self.partner_id.team_id:
|
|
values['team_id'] = self.partner_id.team_id.id
|
|
self.update(values)
|
|
|
|
@api.onchange('partner_id')
|
|
def onchange_partner_id_warning(self):
|
|
if not self.partner_id:
|
|
return
|
|
warning = {}
|
|
title = False
|
|
message = False
|
|
partner = self.partner_id
|
|
|
|
# If partner has no warning, check its company
|
|
if partner.sale_warn == 'no-message' and partner.parent_id:
|
|
partner = partner.parent_id
|
|
|
|
if partner.sale_warn != 'no-message':
|
|
# Block if partner only has warning but parent company is blocked
|
|
if partner.sale_warn != 'block' and partner.parent_id and partner.parent_id.sale_warn == 'block':
|
|
partner = partner.parent_id
|
|
title = ("Warning for %s") % partner.name
|
|
message = partner.sale_warn_msg
|
|
warning = {
|
|
'title': title,
|
|
'message': message,
|
|
}
|
|
if partner.sale_warn == 'block':
|
|
self.update({'partner_id': False, 'partner_invoice_id': False, 'partner_shipping_id': False, 'pricelist_id': False})
|
|
return {'warning': warning}
|
|
|
|
if warning:
|
|
return {'warning': warning}
|
|
|
|
@api.model
|
|
def create(self, vals):
|
|
if vals.get('name', _('New')) == _('New'):
|
|
if 'company_id' in vals:
|
|
vals['name'] = self.env['ir.sequence'].with_context(force_company=vals['company_id']).next_by_code('sale.order') or _('New')
|
|
else:
|
|
vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or _('New')
|
|
|
|
# Makes sure partner_invoice_id', 'partner_shipping_id' and 'pricelist_id' are defined
|
|
if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
|
|
partner = self.env['res.partner'].browse(vals.get('partner_id'))
|
|
addr = partner.address_get(['delivery', 'invoice'])
|
|
vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice'])
|
|
vals['partner_shipping_id'] = vals.setdefault('partner_shipping_id', addr['delivery'])
|
|
vals['pricelist_id'] = vals.setdefault('pricelist_id', partner.property_product_pricelist and partner.property_product_pricelist.id)
|
|
result = super(SaleOrder, self).create(vals)
|
|
return result
|
|
|
|
@api.multi
|
|
def _prepare_invoice(self):
|
|
"""
|
|
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).
|
|
"""
|
|
self.ensure_one()
|
|
journal_id = self.env['account.invoice'].default_get(['journal_id'])['journal_id']
|
|
if not journal_id:
|
|
raise UserError(_('Please define an accounting sale journal for this company.'))
|
|
invoice_vals = {
|
|
'name': self.client_order_ref or '',
|
|
'origin': self.name,
|
|
'type': 'out_invoice',
|
|
'account_id': self.partner_invoice_id.property_account_receivable_id.id,
|
|
'partner_id': self.partner_invoice_id.id,
|
|
'partner_shipping_id': self.partner_shipping_id.id,
|
|
'journal_id': journal_id,
|
|
'currency_id': self.pricelist_id.currency_id.id,
|
|
'comment': self.note,
|
|
'payment_term_id': self.payment_term_id.id,
|
|
'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id,
|
|
'company_id': self.company_id.id,
|
|
'user_id': self.user_id and self.user_id.id,
|
|
'team_id': self.team_id.id
|
|
}
|
|
return invoice_vals
|
|
|
|
@api.multi
|
|
def print_quotation(self):
|
|
self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'})
|
|
return self.env['report'].get_action(self, 'sale.report_saleorder')
|
|
|
|
@api.multi
|
|
def action_view_invoice(self):
|
|
invoices = self.mapped('invoice_ids')
|
|
action = self.env.ref('account.action_invoice_tree1').read()[0]
|
|
if len(invoices) > 1:
|
|
action['domain'] = [('id', 'in', invoices.ids)]
|
|
elif len(invoices) == 1:
|
|
action['views'] = [(self.env.ref('account.invoice_form').id, 'form')]
|
|
action['res_id'] = invoices.ids[0]
|
|
else:
|
|
action = {'type': 'ir.actions.act_window_close'}
|
|
return action
|
|
|
|
@api.multi
|
|
def action_invoice_create(self, grouped=False, final=False):
|
|
"""
|
|
Create the invoice associated to the SO.
|
|
:param grouped: if True, invoices are grouped by SO id. If False, invoices are grouped by
|
|
(partner_invoice_id, currency)
|
|
:param final: if True, refunds will be generated if necessary
|
|
:returns: list of created invoices
|
|
"""
|
|
inv_obj = self.env['account.invoice']
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
invoices = {}
|
|
references = {}
|
|
invoices_origin = {}
|
|
invoices_name = {}
|
|
|
|
for order in self:
|
|
group_key = order.id if grouped else (order.partner_invoice_id.id, order.currency_id.id)
|
|
for line in order.order_line.sorted(key=lambda l: l.qty_to_invoice < 0):
|
|
if float_is_zero(line.qty_to_invoice, precision_digits=precision):
|
|
continue
|
|
if group_key not in invoices:
|
|
inv_data = order._prepare_invoice()
|
|
invoice = inv_obj.create(inv_data)
|
|
references[invoice] = order
|
|
invoices[group_key] = invoice
|
|
invoices_origin[group_key] = [invoice.origin]
|
|
invoices_name[group_key] = [invoice.name]
|
|
elif group_key in invoices:
|
|
if order.name not in invoices_origin[group_key]:
|
|
invoices_origin[group_key].append(order.name)
|
|
if order.client_order_ref and order.client_order_ref not in invoices_name[group_key]:
|
|
invoices_name[group_key].append(order.client_order_ref)
|
|
|
|
if line.qty_to_invoice > 0:
|
|
line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice)
|
|
elif line.qty_to_invoice < 0 and final:
|
|
line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice)
|
|
|
|
if references.get(invoices.get(group_key)):
|
|
if order not in references[invoices[group_key]]:
|
|
references[invoice] = references[invoice] | order
|
|
|
|
for group_key in invoices:
|
|
invoices[group_key].write({'name': ', '.join(invoices_name[group_key]),
|
|
'origin': ', '.join(invoices_origin[group_key])})
|
|
|
|
if not invoices:
|
|
raise UserError(_('There is no invoicable line.'))
|
|
|
|
for invoice in invoices.values():
|
|
if not invoice.invoice_line_ids:
|
|
raise UserError(_('There is no invoicable line.'))
|
|
# If invoice is negative, do a refund invoice instead
|
|
if invoice.amount_untaxed < 0:
|
|
invoice.type = 'out_refund'
|
|
for line in invoice.invoice_line_ids:
|
|
line.quantity = -line.quantity
|
|
# Use additional field helper function (for account extensions)
|
|
for line in invoice.invoice_line_ids:
|
|
line._set_additional_fields(invoice)
|
|
# Necessary to force computation of taxes. In account_invoice, they are triggered
|
|
# by onchanges, which are not triggered when doing a create.
|
|
invoice.compute_taxes()
|
|
invoice.message_post_with_view('mail.message_origin_link',
|
|
values={'self': invoice, 'origin': references[invoice]},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
return [inv.id for inv in invoices.values()]
|
|
|
|
@api.multi
|
|
def action_draft(self):
|
|
orders = self.filtered(lambda s: s.state in ['cancel', 'sent'])
|
|
orders.write({
|
|
'state': 'draft',
|
|
'procurement_group_id': False,
|
|
})
|
|
return orders.mapped('order_line').mapped('procurement_ids').write({'sale_line_id': False})
|
|
|
|
@api.multi
|
|
def action_cancel(self):
|
|
return self.write({'state': 'cancel'})
|
|
|
|
@api.multi
|
|
def action_quotation_send(self):
|
|
'''
|
|
This function opens a window to compose an email, with the edi sale template message loaded by default
|
|
'''
|
|
self.ensure_one()
|
|
ir_model_data = self.env['ir.model.data']
|
|
try:
|
|
template_id = ir_model_data.get_object_reference('sale', 'email_template_edi_sale')[1]
|
|
except ValueError:
|
|
template_id = False
|
|
try:
|
|
compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1]
|
|
except ValueError:
|
|
compose_form_id = False
|
|
ctx = dict()
|
|
ctx.update({
|
|
'default_model': 'sale.order',
|
|
'default_res_id': self.ids[0],
|
|
'default_use_template': bool(template_id),
|
|
'default_template_id': template_id,
|
|
'default_composition_mode': 'comment',
|
|
'mark_so_as_sent': True,
|
|
'custom_layout': "sale.mail_template_data_notification_email_sale_order"
|
|
})
|
|
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,
|
|
}
|
|
|
|
@api.multi
|
|
def force_quotation_send(self):
|
|
for order in self:
|
|
email_act = order.action_quotation_send()
|
|
if email_act and email_act.get('context'):
|
|
email_ctx = email_act['context']
|
|
email_ctx.update(default_email_from=order.company_id.email)
|
|
order.with_context(email_ctx).message_post_with_template(email_ctx.get('default_template_id'))
|
|
return True
|
|
|
|
@api.multi
|
|
def action_done(self):
|
|
return self.write({'state': 'done'})
|
|
|
|
def _prepare_procurement_group(self):
|
|
return {'name': self.name}
|
|
|
|
@api.multi
|
|
def action_confirm(self):
|
|
for order in self:
|
|
order.state = 'sale'
|
|
order.confirmation_date = fields.Datetime.now()
|
|
if self.env.context.get('send_email'):
|
|
self.force_quotation_send()
|
|
order.order_line._action_procurement_create()
|
|
if self.env['ir.values'].get_default('sale.config.settings', 'auto_done_setting'):
|
|
self.action_done()
|
|
return True
|
|
|
|
@api.multi
|
|
def _create_analytic_account(self, prefix=None):
|
|
for order in self:
|
|
name = order.name
|
|
if prefix:
|
|
name = prefix + ": " + order.name
|
|
analytic = self.env['account.analytic.account'].create({
|
|
'name': name,
|
|
'code': order.client_order_ref,
|
|
'company_id': order.company_id.id,
|
|
'partner_id': order.partner_id.id
|
|
})
|
|
order.project_id = analytic
|
|
|
|
@api.multi
|
|
def order_lines_layouted(self):
|
|
"""
|
|
Returns this order lines classified by sale_layout_category and separated in
|
|
pages according to the category pagebreaks. Used to render the report.
|
|
"""
|
|
self.ensure_one()
|
|
report_pages = [[]]
|
|
for category, lines in groupby(self.order_line, lambda l: l.layout_category_id):
|
|
# If last added category induced a pagebreak, this one will be on a new page
|
|
if report_pages[-1] and report_pages[-1][-1]['pagebreak']:
|
|
report_pages.append([])
|
|
# Append category to current report page
|
|
report_pages[-1].append({
|
|
'name': category and category.name or _('Uncategorized'),
|
|
'subtotal': category and category.subtotal,
|
|
'pagebreak': category and category.pagebreak,
|
|
'lines': list(lines)
|
|
})
|
|
|
|
return report_pages
|
|
|
|
@api.multi
|
|
def _get_tax_amount_by_group(self):
|
|
self.ensure_one()
|
|
res = {}
|
|
currency = self.currency_id or self.company_id.currency_id
|
|
for line in self.order_line:
|
|
price_reduce = line.price_unit * (1.0 - line.discount / 100.0)
|
|
taxes = line.tax_id.compute_all(price_reduce, quantity=line.product_uom_qty, product=line.product_id, partner=self.partner_shipping_id)['taxes']
|
|
for tax in line.tax_id:
|
|
group = tax.tax_group_id
|
|
res.setdefault(group, 0.0)
|
|
for t in taxes:
|
|
if t['id'] == tax.id or t['id'] in tax.children_tax_ids.ids:
|
|
res[group] += t['amount']
|
|
res = sorted(res.items(), key=lambda l: l[0].sequence)
|
|
res = map(lambda l: (l[0].name, l[1]), res)
|
|
return res
|
|
|
|
|
|
class SaleOrderLine(models.Model):
|
|
_name = 'sale.order.line'
|
|
_description = 'Sales Order Line'
|
|
_order = 'order_id, layout_category_id, sequence, id'
|
|
|
|
@api.depends('state', 'product_uom_qty', 'qty_delivered', 'qty_to_invoice', 'qty_invoiced')
|
|
def _compute_invoice_status(self):
|
|
"""
|
|
Compute the invoice status of a SO line. Possible statuses:
|
|
- no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to
|
|
invoice. This is also hte default value if the conditions of no other status is met.
|
|
- to invoice: we refer to the quantity to invoice of the line. Refer to method
|
|
`_get_to_invoice_qty()` for more information on how this quantity is calculated.
|
|
- upselling: this is possible only for a product invoiced on ordered quantities for which
|
|
we delivered more than expected. The could arise if, for example, a project took more
|
|
time than expected but we decided not to invoice the extra cost to the client. This
|
|
occurs onyl in state 'sale', so that when a SO is set to done, the upselling opportunity
|
|
is removed from the list.
|
|
- invoiced: the quantity invoiced is larger or equal to the quantity ordered.
|
|
"""
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
for line in self:
|
|
if line.state not in ('sale', 'done'):
|
|
line.invoice_status = 'no'
|
|
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
|
|
line.invoice_status = 'to invoice'
|
|
elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\
|
|
float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
|
|
line.invoice_status = 'upselling'
|
|
elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0:
|
|
line.invoice_status = 'invoiced'
|
|
else:
|
|
line.invoice_status = 'no'
|
|
|
|
@api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
|
|
def _compute_amount(self):
|
|
"""
|
|
Compute the amounts of the SO line.
|
|
"""
|
|
for line in self:
|
|
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
|
taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_shipping_id)
|
|
line.update({
|
|
'price_tax': taxes['total_included'] - taxes['total_excluded'],
|
|
'price_total': taxes['total_included'],
|
|
'price_subtotal': taxes['total_excluded'],
|
|
})
|
|
|
|
@api.depends('product_id.invoice_policy', 'order_id.state')
|
|
def _compute_qty_delivered_updateable(self):
|
|
for line in self:
|
|
line.qty_delivered_updateable = (line.order_id.state == 'sale') and (line.product_id.track_service == 'manual') and (line.product_id.expense_policy == 'no')
|
|
|
|
@api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state')
|
|
def _get_to_invoice_qty(self):
|
|
"""
|
|
Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is
|
|
calculated from the ordered quantity. Otherwise, the quantity delivered is used.
|
|
"""
|
|
for line in self:
|
|
if line.order_id.state in ['sale', 'done']:
|
|
if line.product_id.invoice_policy == 'order':
|
|
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
|
|
else:
|
|
line.qty_to_invoice = line.qty_delivered - line.qty_invoiced
|
|
else:
|
|
line.qty_to_invoice = 0
|
|
|
|
@api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity')
|
|
def _get_invoice_qty(self):
|
|
"""
|
|
Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. Note
|
|
that this is the case only if the refund is generated from the SO and that is intentional: if
|
|
a refund made would automatically decrease the invoiced quantity, then there is a risk of reinvoicing
|
|
it automatically, which may not be wanted at all. That's why the refund has to be created from the SO
|
|
"""
|
|
for line in self:
|
|
qty_invoiced = 0.0
|
|
for invoice_line in line.invoice_lines:
|
|
if invoice_line.invoice_id.state != 'cancel':
|
|
if invoice_line.invoice_id.type == 'out_invoice':
|
|
qty_invoiced += invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom)
|
|
elif invoice_line.invoice_id.type == 'out_refund':
|
|
qty_invoiced -= invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom)
|
|
line.qty_invoiced = qty_invoiced
|
|
|
|
@api.depends('price_unit', 'discount')
|
|
def _get_price_reduce(self):
|
|
for line in self:
|
|
line.price_reduce = line.price_unit * (1.0 - line.discount / 100.0)
|
|
|
|
@api.depends('price_total', 'product_uom_qty')
|
|
def _get_price_reduce_tax(self):
|
|
for line in self:
|
|
line.price_reduce_taxinc = line.price_total / line.product_uom_qty if line.product_uom_qty else 0.0
|
|
|
|
@api.depends('price_subtotal', 'product_uom_qty')
|
|
def _get_price_reduce_notax(self):
|
|
for line in self:
|
|
line.price_reduce_taxexcl = line.price_subtotal / line.product_uom_qty if line.product_uom_qty else 0.0
|
|
|
|
@api.multi
|
|
def _compute_tax_id(self):
|
|
for line in self:
|
|
fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id
|
|
# If company_id is set, always filter taxes by the company
|
|
taxes = line.product_id.taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
|
|
line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_shipping_id) if fpos else taxes
|
|
|
|
@api.multi
|
|
def _prepare_order_line_procurement(self, group_id=False):
|
|
self.ensure_one()
|
|
return {
|
|
'name': self.name,
|
|
'origin': self.order_id.name,
|
|
'date_planned': datetime.strptime(self.order_id.date_order, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=self.customer_lead),
|
|
'product_id': self.product_id.id,
|
|
'product_qty': self.product_uom_qty,
|
|
'product_uom': self.product_uom.id,
|
|
'company_id': self.order_id.company_id.id,
|
|
'group_id': group_id,
|
|
'sale_line_id': self.id
|
|
}
|
|
|
|
@api.multi
|
|
def _action_procurement_create(self):
|
|
"""
|
|
Create procurements based on quantity ordered. If the quantity is increased, new
|
|
procurements are created. If the quantity is decreased, no automated action is taken.
|
|
"""
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
new_procs = self.env['procurement.order'] # Empty recordset
|
|
for line in self:
|
|
if line.state != 'sale' or not line.product_id._need_procurement():
|
|
continue
|
|
qty = 0.0
|
|
for proc in line.procurement_ids.filtered(lambda r: r.state != 'cancel'):
|
|
qty += proc.product_qty
|
|
if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
|
|
continue
|
|
|
|
if not line.order_id.procurement_group_id:
|
|
vals = line.order_id._prepare_procurement_group()
|
|
line.order_id.procurement_group_id = self.env["procurement.group"].create(vals)
|
|
|
|
vals = line._prepare_order_line_procurement(group_id=line.order_id.procurement_group_id.id)
|
|
vals['product_qty'] = line.product_uom_qty - qty
|
|
new_proc = self.env["procurement.order"].with_context(procurement_autorun_defer=True).create(vals)
|
|
new_proc.message_post_with_view('mail.message_origin_link',
|
|
values={'self': new_proc, 'origin': line.order_id},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
new_procs += new_proc
|
|
new_procs.run()
|
|
return new_procs
|
|
|
|
@api.model
|
|
def _get_purchase_price(self, pricelist, product, product_uom, date):
|
|
return {}
|
|
|
|
@api.model
|
|
def _prepare_add_missing_fields(self, values):
|
|
""" Deduce missing required fields from the onchange """
|
|
res = {}
|
|
onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_id']
|
|
if values.get('order_id') and values.get('product_id') and any(f not in values for f in onchange_fields):
|
|
line = self.new(values)
|
|
line.product_id_change()
|
|
for field in onchange_fields:
|
|
if field not in values:
|
|
res[field] = line._fields[field].convert_to_write(line[field], line)
|
|
return res
|
|
|
|
@api.model
|
|
def create(self, values):
|
|
values.update(self._prepare_add_missing_fields(values))
|
|
line = super(SaleOrderLine, self).create(values)
|
|
if line.order_id.state == 'sale':
|
|
line._action_procurement_create()
|
|
msg = _("Extra line with %s ") % (line.product_id.display_name,)
|
|
line.order_id.message_post(body=msg)
|
|
|
|
return line
|
|
|
|
@api.multi
|
|
def write(self, values):
|
|
lines = False
|
|
changed_lines = False
|
|
if 'product_uom_qty' in values:
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
lines = self.filtered(
|
|
lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1)
|
|
changed_lines = self.filtered(
|
|
lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) != 0)
|
|
if changed_lines:
|
|
orders = self.mapped('order_id')
|
|
for order in orders:
|
|
order_lines = changed_lines.filtered(lambda x: x.order_id == order)
|
|
msg = ""
|
|
if any([values['product_uom_qty'] < x.product_uom_qty for x in order_lines]):
|
|
msg += "<b>" + _('The ordered quantity has been decreased. Do not forget to take it into account on your invoices and delivery orders.') + '</b>'
|
|
msg += "<ul>"
|
|
for line in order_lines:
|
|
msg += "<li> %s:" % (line.product_id.display_name,)
|
|
msg += "<br/>" + _("Ordered Quantity") + ": %s -> %s <br/>" % (line.product_uom_qty, float(values['product_uom_qty']),)
|
|
if line.product_id.type in ('consu', 'product'):
|
|
msg += _("Delivered Quantity") + ": %s <br/>" % (line.qty_delivered,)
|
|
msg += _("Invoiced Quantity") + ": %s <br/>" % (line.qty_invoiced,)
|
|
msg += "</ul>"
|
|
order.message_post(body=msg)
|
|
|
|
# Prevent writing on a locked SO.
|
|
protected_fields = self._get_protected_fields()
|
|
if 'done' in self.mapped('order_id.state') and any(f in values.keys() for f in protected_fields):
|
|
fields = self.env['ir.model.fields'].search([
|
|
('name', 'in', protected_fields), ('model', '=', self._name)
|
|
])
|
|
raise UserError(
|
|
_('It is forbidden to modify the following fields in a locked order:\n%s')
|
|
% '\n'.join(fields.mapped('field_description'))
|
|
)
|
|
|
|
result = super(SaleOrderLine, self).write(values)
|
|
if lines:
|
|
lines._action_procurement_create()
|
|
return result
|
|
|
|
order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False)
|
|
name = fields.Text(string='Description', required=True)
|
|
sequence = fields.Integer(string='Sequence', default=10)
|
|
|
|
invoice_lines = fields.Many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_line_id', string='Invoice Lines', copy=False)
|
|
invoice_status = fields.Selection([
|
|
('upselling', 'Upselling Opportunity'),
|
|
('invoiced', 'Fully Invoiced'),
|
|
('to invoice', 'To Invoice'),
|
|
('no', 'Nothing to Invoice')
|
|
], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no')
|
|
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'), default=0.0)
|
|
|
|
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', readonly=True, store=True)
|
|
price_tax = fields.Monetary(compute='_compute_amount', string='Taxes', readonly=True, store=True)
|
|
price_total = fields.Monetary(compute='_compute_amount', string='Total', readonly=True, store=True)
|
|
|
|
price_reduce = fields.Monetary(compute='_get_price_reduce', string='Price Reduce', readonly=True, store=True)
|
|
tax_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
|
|
price_reduce_taxinc = fields.Monetary(compute='_get_price_reduce_tax', string='Price Reduce Tax inc', readonly=True, store=True)
|
|
price_reduce_taxexcl = fields.Monetary(compute='_get_price_reduce_notax', string='Price Reduce Tax excl', readonly=True, store=True)
|
|
|
|
discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0)
|
|
|
|
product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict', required=True)
|
|
product_uom_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
|
|
product_uom = fields.Many2one('product.uom', string='Unit of Measure', required=True)
|
|
|
|
qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True)
|
|
qty_delivered = fields.Float(string='Delivered', copy=False, digits=dp.get_precision('Product Unit of Measure'), default=0.0)
|
|
qty_to_invoice = fields.Float(
|
|
compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True,
|
|
digits=dp.get_precision('Product Unit of Measure'))
|
|
qty_invoiced = fields.Float(
|
|
compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
|
|
digits=dp.get_precision('Product Unit of Measure'))
|
|
|
|
salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', readonly=True)
|
|
currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True)
|
|
company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, readonly=True)
|
|
order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer')
|
|
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
|
|
|
|
state = fields.Selection([
|
|
('draft', 'Quotation'),
|
|
('sent', 'Quotation Sent'),
|
|
('sale', 'Sale Order'),
|
|
('done', 'Done'),
|
|
('cancel', 'Cancelled'),
|
|
], related='order_id.state', string='Order Status', readonly=True, copy=False, store=True, default='draft')
|
|
|
|
customer_lead = fields.Float(
|
|
'Delivery Lead Time', required=True, default=0.0,
|
|
help="Number of days between the order confirmation and the shipping of the products to the customer", oldname="delay")
|
|
procurement_ids = fields.One2many('procurement.order', 'sale_line_id', string='Procurements')
|
|
|
|
layout_category_id = fields.Many2one('sale.layout_category', string='Section')
|
|
layout_category_sequence = fields.Integer(string='Layout Sequence')
|
|
# TODO: remove layout_category_sequence in master or make it work properly
|
|
|
|
@api.multi
|
|
def _prepare_invoice_line(self, qty):
|
|
"""
|
|
Prepare the dict of values to create the new invoice line for a sales order line.
|
|
|
|
:param qty: float quantity to invoice
|
|
"""
|
|
self.ensure_one()
|
|
res = {}
|
|
account = self.product_id.property_account_income_id or self.product_id.categ_id.property_account_income_categ_id
|
|
if not account:
|
|
raise UserError(_('Please define income account for this product: "%s" (id:%d) - or for its category: "%s".') %
|
|
(self.product_id.name, self.product_id.id, self.product_id.categ_id.name))
|
|
|
|
fpos = self.order_id.fiscal_position_id or self.order_id.partner_id.property_account_position_id
|
|
if fpos:
|
|
account = fpos.map_account(account)
|
|
|
|
res = {
|
|
'name': self.name,
|
|
'sequence': self.sequence,
|
|
'origin': self.order_id.name,
|
|
'account_id': account.id,
|
|
'price_unit': self.price_unit,
|
|
'quantity': qty,
|
|
'discount': self.discount,
|
|
'uom_id': self.product_uom.id,
|
|
'product_id': self.product_id.id or False,
|
|
'layout_category_id': self.layout_category_id and self.layout_category_id.id or False,
|
|
'invoice_line_tax_ids': [(6, 0, self.tax_id.ids)],
|
|
'account_analytic_id': self.order_id.project_id.id,
|
|
'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
|
|
}
|
|
return res
|
|
|
|
@api.multi
|
|
def invoice_line_create(self, invoice_id, qty):
|
|
"""
|
|
Create an invoice line. The quantity to invoice can be positive (invoice) or negative
|
|
(refund).
|
|
|
|
:param invoice_id: integer
|
|
:param qty: float quantity to invoice
|
|
"""
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
for line in self:
|
|
if not float_is_zero(qty, precision_digits=precision):
|
|
vals = line._prepare_invoice_line(qty=qty)
|
|
vals.update({'invoice_id': invoice_id, 'sale_line_ids': [(6, 0, [line.id])]})
|
|
self.env['account.invoice.line'].create(vals)
|
|
|
|
@api.multi
|
|
def _get_display_price(self, product):
|
|
# TO DO: move me in master/saas-16 on sale.order
|
|
if self.order_id.pricelist_id.discount_policy == 'with_discount':
|
|
return product.with_context(pricelist=self.order_id.pricelist_id.id).price
|
|
final_price, rule_id = self.order_id.pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id)
|
|
context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order)
|
|
base_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id)
|
|
if currency_id != self.order_id.pricelist_id.currency_id.id:
|
|
base_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(base_price, self.order_id.pricelist_id.currency_id)
|
|
# negative discounts (= surcharge) are included in the display price
|
|
return max(base_price, final_price)
|
|
|
|
@api.multi
|
|
@api.onchange('product_id')
|
|
def product_id_change(self):
|
|
if not self.product_id:
|
|
return {'domain': {'product_uom': []}}
|
|
|
|
vals = {}
|
|
domain = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
|
if not self.product_uom or (self.product_id.uom_id.id != self.product_uom.id):
|
|
vals['product_uom'] = self.product_id.uom_id
|
|
vals['product_uom_qty'] = 1.0
|
|
|
|
product = self.product_id.with_context(
|
|
lang=self.order_id.partner_id.lang,
|
|
partner=self.order_id.partner_id.id,
|
|
quantity=vals.get('product_uom_qty') or self.product_uom_qty,
|
|
date=self.order_id.date_order,
|
|
pricelist=self.order_id.pricelist_id.id,
|
|
uom=self.product_uom.id
|
|
)
|
|
|
|
result = {'domain': domain}
|
|
|
|
title = False
|
|
message = False
|
|
warning = {}
|
|
if product.sale_line_warn != 'no-message':
|
|
title = _("Warning for %s") % product.name
|
|
message = product.sale_line_warn_msg
|
|
warning['title'] = title
|
|
warning['message'] = message
|
|
result = {'warning': warning}
|
|
if product.sale_line_warn == 'block':
|
|
self.product_id = False
|
|
return result
|
|
|
|
name = product.name_get()[0][1]
|
|
if product.description_sale:
|
|
name += '\n' + product.description_sale
|
|
vals['name'] = name
|
|
|
|
self._compute_tax_id()
|
|
|
|
if self.order_id.pricelist_id and self.order_id.partner_id:
|
|
vals['price_unit'] = self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id)
|
|
self.update(vals)
|
|
|
|
return result
|
|
|
|
@api.onchange('product_uom', 'product_uom_qty')
|
|
def product_uom_change(self):
|
|
if not self.product_uom or not self.product_id:
|
|
self.price_unit = 0.0
|
|
return
|
|
if self.order_id.pricelist_id and self.order_id.partner_id:
|
|
product = self.product_id.with_context(
|
|
lang=self.order_id.partner_id.lang,
|
|
partner=self.order_id.partner_id.id,
|
|
quantity=self.product_uom_qty,
|
|
date=self.order_id.date_order,
|
|
pricelist=self.order_id.pricelist_id.id,
|
|
uom=self.product_uom.id,
|
|
fiscal_position=self.env.context.get('fiscal_position')
|
|
)
|
|
self.price_unit = self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id)
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
if self.filtered(lambda x: x.state in ('sale', 'done')):
|
|
raise UserError(_('You can not remove a sale order line.\nDiscard changes and try setting the quantity to 0.'))
|
|
return super(SaleOrderLine, self).unlink()
|
|
|
|
@api.multi
|
|
def _get_delivered_qty(self):
|
|
'''
|
|
Intended to be overridden in sale_stock and sale_mrp
|
|
:return: the quantity delivered
|
|
:rtype: float
|
|
'''
|
|
return 0.0
|
|
|
|
def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
|
|
"""Retrieve the price before applying the pricelist
|
|
:param obj product: object of current product record
|
|
:parem float qty: total quentity of product
|
|
:param tuple price_and_rule: tuple(price, suitable_rule) coming from pricelist computation
|
|
:param obj uom: unit of measure of current order line
|
|
:param integer pricelist_id: pricelist id of sale order"""
|
|
PricelistItem = self.env['product.pricelist.item']
|
|
field_name = 'lst_price'
|
|
currency_id = None
|
|
product_currency = None
|
|
if rule_id:
|
|
pricelist_item = PricelistItem.browse(rule_id)
|
|
if pricelist_item.pricelist_id.discount_policy == 'without_discount':
|
|
while pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id and pricelist_item.base_pricelist_id.discount_policy == 'without_discount':
|
|
price, rule_id = pricelist_item.base_pricelist_id.with_context(uom=uom.id).get_product_price_rule(product, qty, self.order_id.partner_id)
|
|
pricelist_item = PricelistItem.browse(rule_id)
|
|
|
|
if pricelist_item.base == 'standard_price':
|
|
field_name = 'standard_price'
|
|
if pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id:
|
|
field_name = 'price'
|
|
product = product.with_context(pricelist=pricelist_item.base_pricelist_id.id)
|
|
product_currency = pricelist_item.base_pricelist_id.currency_id
|
|
currency_id = pricelist_item.pricelist_id.currency_id
|
|
|
|
product_currency = product_currency or(product.company_id and product.company_id.currency_id) or self.env.user.company_id.currency_id
|
|
if not currency_id:
|
|
currency_id = product_currency
|
|
cur_factor = 1.0
|
|
else:
|
|
if currency_id.id == product_currency.id:
|
|
cur_factor = 1.0
|
|
else:
|
|
cur_factor = currency_id._get_conversion_rate(product_currency, currency_id)
|
|
|
|
product_uom = self.env.context.get('uom') or product.uom_id.id
|
|
if uom and uom.id != product_uom:
|
|
# the unit price is in a different uom
|
|
uom_factor = uom._compute_price(1.0, product.uom_id)
|
|
else:
|
|
uom_factor = 1.0
|
|
|
|
return product[field_name] * uom_factor * cur_factor, currency_id.id
|
|
|
|
def _get_protected_fields(self):
|
|
return [
|
|
'product_id', 'name', 'price_unit', 'product_uom', 'product_uom_qty',
|
|
'tax_id', 'analytic_tag_ids'
|
|
]
|
|
|
|
@api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id')
|
|
def _onchange_discount(self):
|
|
self.discount = 0.0
|
|
if not (self.product_id and self.product_uom and
|
|
self.order_id.partner_id and self.order_id.pricelist_id and
|
|
self.order_id.pricelist_id.discount_policy == 'without_discount' and
|
|
self.env.user.has_group('sale.group_discount_per_so_line')):
|
|
return
|
|
|
|
context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order)
|
|
pricelist_context = dict(context_partner, uom=self.product_uom.id)
|
|
|
|
price, rule_id = self.order_id.pricelist_id.with_context(pricelist_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id)
|
|
new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id)
|
|
|
|
if new_list_price != 0:
|
|
if self.order_id.pricelist_id.currency_id.id != currency_id:
|
|
# we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
|
|
new_list_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(new_list_price, self.order_id.pricelist_id.currency_id)
|
|
discount = (new_list_price - price) / new_list_price * 100
|
|
if discount > 0:
|
|
self.discount = discount
|