Files
tryton/sale.py
T
Cédric Krier db9e571879 Use the instance context to get carrier prices
issue10666
review358661002
(grafted from c910b270db7cc102d49cb8c0bfb6343c5db418d9)

--HG--
branch : 6.0
2021-08-27 08:48:34 +02:00

397 lines
14 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from trytond import backend
from trytond.i18n import gettext
from trytond.model import ModelView, Workflow, fields
from trytond.transaction import Transaction
from trytond.pyson import Eval, If
from trytond.pool import Pool, PoolMeta
from trytond.modules.product import price_digits, round_price
from trytond.modules.sale.exceptions import SaleConfirmError
sale_shipment_cost_method = fields.Selection(
'get_sale_shipment_cost_methods', "Sale Shipment Cost Method")
def get_sale_methods(field_name):
@classmethod
def func(cls):
pool = Pool()
Sale = pool.get('sale.sale')
return Sale.fields_get([field_name])[field_name]['selection']
return func
class Configuration(metaclass=PoolMeta):
__name__ = 'sale.configuration'
sale_shipment_cost_method = fields.MultiValue(sale_shipment_cost_method)
get_sale_shipment_cost_methods = get_sale_methods('shipment_cost_method')
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field == 'sale_shipment_cost_method':
return pool.get('sale.configuration.sale_method')
return super(Configuration, cls).multivalue_model(field)
@classmethod
def default_sale_shipment_cost_method(cls, **pattern):
return cls.multivalue_model(
'sale_shipment_cost_method').default_sale_shipment_cost_method()
class ConfigurationSaleMethod(metaclass=PoolMeta):
__name__ = 'sale.configuration.sale_method'
sale_shipment_cost_method = sale_shipment_cost_method
get_sale_shipment_cost_methods = get_sale_methods('shipment_cost_method')
@classmethod
def __register__(cls, module_name):
exist = backend.TableHandler.table_exist(cls._table)
if exist:
table = cls.__table_handler__(module_name)
exist &= table.column_exist('sale_shipment_cost_method')
super(ConfigurationSaleMethod, cls).__register__(module_name)
if not exist:
cls._migrate_property([], [], [])
@classmethod
def _migrate_property(cls, field_names, value_names, fields):
field_names.append('sale_shipment_cost_method')
value_names.append('sale_shipment_cost_method')
super(ConfigurationSaleMethod, cls)._migrate_property(
field_names, value_names, fields)
@classmethod
def default_sale_shipment_cost_method(cls):
return 'order'
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
carrier = fields.Many2One('carrier', 'Carrier',
domain=[
If(Eval('state') == 'draft', [
('carrier_product.salable', '=', True),
('id', 'in', Eval('available_carriers', [])),
],
[]),
],
states={
'readonly': Eval('state') != 'draft',
},
context={
'company': Eval('company', -1),
},
depends=['state', 'available_carriers', 'company'])
available_carriers = fields.Function(
fields.Many2Many('carrier', None, None, 'Available Carriers'),
'on_change_with_available_carriers')
shipment_cost_method = fields.Selection([
(None, "None"),
('order', 'On Order'),
('shipment', 'On Shipment'),
], 'Shipment Cost Method', states={
'readonly': Eval('state') != 'draft',
}, depends=['state'])
@classmethod
def __register__(cls, module):
super().__register__(module)
table_h = cls.__table_handler__(module)
# Migration from 5.8: remove required on shipment_cost_method
table_h.not_null_action('shipment_cost_method', 'remove')
@classmethod
def default_shipment_cost_method(cls, **pattern):
Config = Pool().get('sale.configuration')
config = Config(1)
return config.get_multivalue(
'sale_shipment_cost_method', **pattern)
@fields.depends('company')
def on_change_company(self):
super().on_change_company()
self.shipment_cost_method = self.default_shipment_cost_method(
company=self.company.id if self.company else None)
@fields.depends('warehouse', 'shipment_address')
def _get_carrier_selection_pattern(self):
pattern = {}
if (self.warehouse
and self.warehouse.address
and self.warehouse.address.country):
pattern['from_country'] = self.warehouse.address.country.id
else:
pattern['from_country'] = None
if self.shipment_address and self.shipment_address.country:
pattern['to_country'] = self.shipment_address.country.id
else:
pattern['to_country'] = None
return pattern
@fields.depends(methods=['_get_carrier_selection_pattern'])
def on_change_with_available_carriers(self, name=None):
pool = Pool()
CarrierSelection = pool.get('carrier.selection')
pattern = self._get_carrier_selection_pattern()
carriers = CarrierSelection.get_carriers(pattern)
return [c.id for c in carriers]
@fields.depends('carrier', methods=['on_change_with_available_carriers'])
def on_change_party(self):
super(Sale, self).on_change_party()
self.available_carriers = self.on_change_with_available_carriers()
if self.available_carriers and (not self.carrier
or self.carrier not in self.available_carriers):
self.carrier = self.available_carriers[0]
elif not self.available_carriers:
self.carrier = None
@fields.depends('carrier', methods=['on_change_with_available_carriers'])
def on_change_shipment_party(self):
super(Sale, self).on_change_shipment_party()
self.available_carriers = self.on_change_with_available_carriers()
if self.available_carriers and (not self.carrier
or self.carrier not in self.available_carriers):
self.carrier = self.available_carriers[0]
elif not self.available_carriers:
self.carrier = None
@fields.depends('carrier', methods=['on_change_with_available_carriers'])
def on_change_shipment_address(self):
try:
super_on_change = super(Sale, self).on_change_shipment_address
except AttributeError:
pass
else:
super_on_change()
self.available_carriers = self.on_change_with_available_carriers()
if self.available_carriers and (not self.carrier
or self.carrier not in self.available_carriers):
self.carrier = self.available_carriers[0]
elif not self.available_carriers:
self.carrier = None
@classmethod
@ModelView.button
@Workflow.transition('quotation')
def quote(cls, sales):
pool = Pool()
Line = pool.get('sale.line')
removed = []
for sale in sales:
removed.extend(sale.set_shipment_cost())
Line.delete(removed)
cls.save(sales)
super(Sale, cls).quote(sales)
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
def confirm(cls, sales):
for sale in sales:
if sale.carrier and sale.carrier not in sale.available_carriers:
raise SaleConfirmError(
gettext('sale_shipment_cost.msg_sale_invalid_carrier',
sale=sale.rec_name,
carrier=sale.carrier.rec_name))
super(Sale, cls).confirm(sales)
def _get_carrier_context(self):
return {}
def compute_shipment_cost(self):
pool = Pool()
Date = pool.get('ir.date')
Currency = pool.get('currency.currency')
Move = pool.get('stock.move')
stockable = any(
line.quantity >= 0 for line in self.lines
if line.type == 'line'
and line.product
and line.product.type in Move.get_product_types())
if self.carrier and stockable:
with Transaction().set_context(self._get_carrier_context()):
cost, currency_id = self.carrier.get_sale_price()
today = Date.today()
date = self.sale_date or today
with Transaction().set_context(date=date):
return Currency.compute(
Currency(currency_id), cost, self.currency, round=False)
def set_shipment_cost(self):
cost = None
if self.shipment_cost_method:
cost = self.compute_shipment_cost()
removed = []
unit_price = None
lines = list(self.lines or [])
for line in self.lines:
if line.type == 'line' and line.shipment_cost is not None:
if line.shipment_cost == cost:
unit_price = line.unit_price * Decimal(str(line.quantity))
lines.remove(line)
removed.append(line)
if cost is not None:
lines.append(self.get_shipment_cost_line(
cost, unit_price=unit_price))
self.lines = lines
return removed
def get_shipment_cost_line(self, cost, unit_price=None):
pool = Pool()
SaleLine = pool.get('sale.line')
product = self.carrier.carrier_product
sequence = None
if self.lines:
last_line = self.lines[-1]
if last_line.sequence is not None:
sequence = last_line.sequence + 1
shipment_cost = round_price(cost)
cost_line = SaleLine(
sale=self,
sequence=sequence,
type='line',
product=product,
quantity=1, # XXX
unit=product.sale_uom,
shipment_cost=shipment_cost,
)
cost_line.on_change_product()
if unit_price is not None:
cost_line.unit_price = round_price(unit_price)
else:
cost_line.unit_price = round_price(cost)
if not cost_line.unit_price:
cost_line.quantity = 0
cost_line.amount = cost_line.on_change_with_amount()
return cost_line
@property
def shipment_cost_amount(self):
cost = Decimal(0)
for line in self.lines:
if line.type == 'line' and line.shipment_cost is not None:
cost += line.amount
return cost
def create_shipment(self, shipment_type):
pool = Pool()
Shipment = pool.get('stock.shipment.out')
shipments = super(Sale, self).create_shipment(shipment_type)
if shipment_type == 'out' and shipments and self.carrier:
for shipment in shipments:
shipment.carrier = self.carrier
Shipment.save(shipments)
return shipments
def create_invoice(self):
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
Shipment = pool.get('stock.shipment.out')
invoice = super(Sale, self).create_invoice()
if invoice and self.shipment_cost_method == 'shipment':
invoice_lines = []
# Copy shipments to avoid losing changes as the cache is cleared
# after invoice line save because shipment is a Function field
shipments = list(self.shipments)
for shipment in shipments:
if (shipment.state == 'done'
and shipment.carrier
and shipment.cost
and not shipment.cost_invoice_line):
invoice_line = shipment.get_cost_invoice_line(invoice)
if not invoice_line:
continue
invoice_line.invoice = invoice
invoice_lines.append(invoice_line)
shipment.cost_invoice_line = invoice_line
InvoiceLine.save(invoice_lines)
Shipment.save(shipments)
invoice.update_taxes()
return invoice
class Line(metaclass=PoolMeta):
__name__ = 'sale.line'
shipment_cost = fields.Numeric('Shipment Cost', digits=price_digits)
def _get_invoice_line_quantity(self):
quantity = super()._get_invoice_line_quantity()
if self.shipment_cost:
if self.sale.shipment_cost_method == 'shipment':
return 0
elif (self.sale.shipment_cost_method == 'order'
and self.sale.invoice_method == 'shipment'):
shipments = self.sale.shipments
if (not shipments
or all(s.state != 'done' for s in shipments)):
return 0
return quantity
class ReturnSale(metaclass=PoolMeta):
__name__ = 'sale.return_sale'
def do_return_(self, action):
pool = Pool()
Sale = pool.get('sale.sale')
SaleLine = pool.get('sale.line')
action, data = super(ReturnSale, self).do_return_(action)
return_sales = Sale.browse(data['res_id'])
lines = []
for sale in return_sales:
for line in sale.lines:
# Do not consider return shipment cost as a shipment cost
if line.shipment_cost:
line.shipment_cost = None
lines.append(line)
SaleLine.save(lines)
return action, data
class Promotion(metaclass=PoolMeta):
__name__ = 'sale.promotion'
amount_shipment_cost_included = fields.Boolean(
"Amount with Shipment Cost Included",
states={
'invisible': ~Eval('amount'),
},
depends=['amount'])
@classmethod
def default_amount_shipment_cost_included(cls):
return False
def get_context_formula(self, sale_line):
context = super(Promotion, self).get_context_formula(sale_line)
if sale_line and sale_line.shipment_cost:
context['names']['unit_price'] = sale_line.shipment_cost
return context
def get_sale_amount(self, sale):
amount = super().get_sale_amount(sale)
if not self.amount_shipment_cost_included:
amount -= sum(l.amount for l in sale.lines if l.shipment_cost)
if not self.untaxed_amount:
amount -= sum(
v['amount']
for l in sale.lines if l.shipment_cost
for v in l._get_taxes().values())
return amount