544 lines
19 KiB
Python
544 lines
19 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 sql import Null
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import ModelView, Workflow, fields
|
|
from trytond.modules.product import price_digits, round_price
|
|
from trytond.modules.sale.exceptions import (
|
|
SaleConfirmError, SaleQuotationError)
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval, If
|
|
from trytond.transaction import Transaction
|
|
|
|
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().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 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={'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",
|
|
domain=[
|
|
If(~Eval('carrier') & ~Eval('state').in_(['draft', 'cancelled']),
|
|
('shipment_cost_method', '=', None),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
shipment_costs = fields.One2Many(
|
|
'stock.shipment.cost_sale', 'sale', "Shipment Costs", readonly=True)
|
|
|
|
@classmethod
|
|
def __register__(cls, module):
|
|
cursor = Transaction().connection.cursor()
|
|
table = cls.__table__()
|
|
|
|
super().__register__(module)
|
|
|
|
# Migration from 6.6: shipment_cost_method domain
|
|
cursor.execute(*table.update(
|
|
[table.shipment_cost_method],
|
|
[Null],
|
|
where=(table.carrier == Null)
|
|
& (table.state != 'draft')
|
|
& (table.shipment_cost_method != Null)))
|
|
|
|
@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(
|
|
'warehouse', 'shipment_address',
|
|
methods=['_get_carrier_selection_pattern'])
|
|
def on_change_with_available_carriers(self, name=None):
|
|
pool = Pool()
|
|
CarrierSelection = pool.get('carrier.selection')
|
|
|
|
if (self.warehouse
|
|
and self.shipment_address
|
|
and self.warehouse.address == self.shipment_address):
|
|
return []
|
|
|
|
pattern = self._get_carrier_selection_pattern()
|
|
return CarrierSelection.get_carriers(pattern)
|
|
|
|
@fields.depends('carrier', methods=['on_change_with_available_carriers'])
|
|
def on_change_party(self):
|
|
super().on_change_party()
|
|
if self.party and self.party.sale_shipment_cost_method != 'default':
|
|
self.shipment_cost_method = self.party.sale_shipment_cost_method
|
|
else:
|
|
self.shipment_cost_method = self.default_shipment_cost_method()
|
|
self.available_carriers = self.on_change_with_available_carriers()
|
|
if not self.available_carriers:
|
|
self.carrier = None
|
|
elif self.shipment_cost_method:
|
|
if (not self.carrier
|
|
or self.carrier not in self.available_carriers):
|
|
self.carrier = self.available_carriers[0]
|
|
|
|
@fields.depends(
|
|
'carrier', 'shipment_cost_method',
|
|
methods=['on_change_with_available_carriers'])
|
|
def on_change_shipment_party(self):
|
|
super().on_change_shipment_party()
|
|
self.available_carriers = self.on_change_with_available_carriers()
|
|
if not self.available_carriers:
|
|
self.carrier = None
|
|
elif self.shipment_cost_method:
|
|
if (not self.carrier
|
|
or self.carrier not in self.available_carriers):
|
|
for carrier in self.available_carriers:
|
|
if carrier.active:
|
|
self.carrier = carrier
|
|
break
|
|
|
|
@fields.depends(
|
|
'carrier', 'shipment_cost_method',
|
|
methods=['on_change_with_available_carriers'])
|
|
def on_change_shipment_address(self):
|
|
try:
|
|
super_on_change = super().on_change_shipment_address
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
super_on_change()
|
|
|
|
self.available_carriers = self.on_change_with_available_carriers()
|
|
if not self.available_carriers:
|
|
self.carrier = None
|
|
elif self.shipment_cost_method:
|
|
if (not self.carrier
|
|
or self.carrier not in self.available_carriers):
|
|
self.carrier = self.available_carriers[0]
|
|
|
|
def check_for_quotation(self):
|
|
super().check_for_quotation()
|
|
if self.shipment_cost_method and self.available_carriers:
|
|
for line in self.lines:
|
|
if (line.product
|
|
and line.product.type != 'service'
|
|
and line.quantity >= 0
|
|
and not self.carrier):
|
|
raise SaleQuotationError(
|
|
gettext('sale_shipment_cost'
|
|
'.msg_sale_carrier_required_for_quotation',
|
|
sale=self.rec_name))
|
|
|
|
@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().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().confirm(sales)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def process(cls, sales):
|
|
with Transaction().set_context(_shipment_cost_invoiced=list()):
|
|
super().process(sales)
|
|
|
|
@classmethod
|
|
def _process_invoice_shipment_states(cls, sales):
|
|
pool = Pool()
|
|
ShipmentOut = pool.get('stock.shipment.out')
|
|
ShipmentCostSale = pool.get('stock.shipment.cost_sale')
|
|
|
|
sent, not_sent = [], []
|
|
for sale in sales:
|
|
if sale.shipment_cost_method == 'order':
|
|
if sale.shipment_state == 'sent':
|
|
sent.append(sale)
|
|
else:
|
|
not_sent.append(sale)
|
|
|
|
super()._process_invoice_shipment_states(sales)
|
|
|
|
to_save, to_delete, shipments = [], [], set()
|
|
for sale in sent:
|
|
if sale.shipment_state != 'sent':
|
|
to_delete.extend(sale.shipment_costs)
|
|
shipments.update(sale.shipments)
|
|
for sale in not_sent:
|
|
if sale.shipment_state == 'sent':
|
|
to_save.extend(sale._get_shipment_costs())
|
|
shipments.update(sale.shipments)
|
|
|
|
ShipmentCostSale.delete(to_delete)
|
|
ShipmentCostSale.save(to_save)
|
|
ShipmentOut.set_shipment_cost(shipments)
|
|
|
|
@property
|
|
def _cost_shipments(self):
|
|
"Return the shipments to apply cost sale"
|
|
return [s for s in self.shipments if s.state != 'cancelled']
|
|
|
|
def _get_shipment_costs(self):
|
|
"Yield shipment costs"
|
|
pool = Pool()
|
|
ShipmentCostSale = pool.get('stock.shipment.cost_sale')
|
|
cost = self.shipment_cost_amount
|
|
shipments = self._cost_shipments
|
|
sum_ = sum(s.cost_used for s in shipments if s.cost_used)
|
|
for shipment in shipments:
|
|
if sum_:
|
|
factor = (shipment.cost_used or 0) / sum_
|
|
else:
|
|
factor = Decimal(1) / len(shipments)
|
|
yield ShipmentCostSale(
|
|
shipment=shipment,
|
|
sale=self,
|
|
amount=round_price(cost * factor),
|
|
currency=self.currency)
|
|
|
|
def _get_carrier_context(self, carrier):
|
|
return {}
|
|
|
|
def compute_shipment_cost(self, carrier):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Currency = pool.get('currency.currency')
|
|
movable = any(
|
|
line.quantity >= 0 for line in self.lines if line.movable)
|
|
if movable:
|
|
with Transaction().set_context(self._get_carrier_context(carrier)):
|
|
cost, currency_id = carrier.get_sale_price()
|
|
if cost is not None:
|
|
with Transaction().set_context(company=self.company.id):
|
|
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.carrier and self.shipment_cost_method:
|
|
cost = self.compute_shipment_cost(self.carrier)
|
|
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(
|
|
self.carrier, cost, unit_price=unit_price))
|
|
self.lines = lines
|
|
return removed
|
|
|
|
def get_shipment_cost_line(self, carrier, cost, unit_price=None):
|
|
pool = Pool()
|
|
SaleLine = pool.get('sale.line')
|
|
|
|
product = 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)
|
|
cost_line.amount = cost_line.on_change_with_amount()
|
|
return cost_line
|
|
|
|
def _get_shipment_grouping_fields(self, shipment):
|
|
pool = Pool()
|
|
ShipmentOut = pool.get('stock.shipment.out')
|
|
fields = super()._get_shipment_grouping_fields(shipment)
|
|
fields.add('carrier')
|
|
if isinstance(shipment, ShipmentOut):
|
|
fields.add('cost_sale_method')
|
|
return fields
|
|
|
|
@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 _get_shipment_sale(self, Shipment, key):
|
|
pool = Pool()
|
|
ShipmentOut = pool.get('stock.shipment.out')
|
|
shipment = super()._get_shipment_sale(Shipment, key)
|
|
if isinstance(shipment, ShipmentOut):
|
|
shipment.cost_sale_method = self.shipment_cost_method
|
|
shipment.carrier = self.carrier
|
|
return shipment
|
|
|
|
@classmethod
|
|
def copy(cls, sales, default=None):
|
|
default = default.copy() if default is not None else {}
|
|
default.setdefault('shipment_costs', None)
|
|
return super().copy(sales, default=default)
|
|
|
|
|
|
class Line(metaclass=PoolMeta):
|
|
__name__ = 'sale.line'
|
|
shipment_cost = fields.Numeric('Shipment Cost', digits=price_digits)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
# shipment_cost is needed to compute the unit_price
|
|
cls.unit_price.depends.add('shipment_cost')
|
|
|
|
@classmethod
|
|
def __register__(cls, module):
|
|
table_h = cls.__table_handler__(module)
|
|
|
|
super().__register__(module)
|
|
|
|
# Migration from 6.4: drop shipment cost unique
|
|
table_h.drop_constraint('sale_shipment_cost_unique')
|
|
|
|
@fields.depends('shipment_cost', 'unit_price')
|
|
def compute_unit_price(self):
|
|
unit_price = super().compute_unit_price()
|
|
if self.shipment_cost is not None:
|
|
unit_price = self.unit_price
|
|
return unit_price
|
|
|
|
def get_invoice_line(self):
|
|
context = Transaction().context
|
|
shipment_cost_invoiced = context.get('_shipment_cost_invoiced')
|
|
lines = super().get_invoice_line()
|
|
if (self.shipment_cost is not None
|
|
and shipment_cost_invoiced is not None):
|
|
for shipment in self.sale.shipments:
|
|
if (shipment.state == 'done'
|
|
and shipment.id not in shipment_cost_invoiced):
|
|
invoice_line = shipment.get_cost_sale_invoice_line(
|
|
self.sale._get_invoice(), origin=self)
|
|
if invoice_line:
|
|
lines.append(invoice_line)
|
|
shipment_cost_invoiced.append(shipment.id)
|
|
return lines
|
|
|
|
@property
|
|
def _invoice_remaining_quantity(self):
|
|
quantity = super()._invoice_remaining_quantity
|
|
if self.shipment_cost is not None:
|
|
quantity = 0
|
|
return quantity
|
|
|
|
def _get_invoice_line_quantity(self):
|
|
quantity = super()._get_invoice_line_quantity()
|
|
if self.shipment_cost is not None:
|
|
if self.sale.shipment_cost_method == 'shipment':
|
|
quantity = 0
|
|
elif (self.sale.shipment_cost_method == 'order'
|
|
and self.sale.invoice_method == 'shipment'):
|
|
shipments = [
|
|
s for s in self.sale.shipments
|
|
if s.cost_sale_method == 'order']
|
|
if (not shipments
|
|
or all(s.state != 'done' for s in shipments)):
|
|
quantity = 0
|
|
return quantity
|
|
|
|
def _get_invoiced_quantity(self):
|
|
quantity = super()._get_invoiced_quantity()
|
|
if self.shipment_cost is not None:
|
|
if self.sale.shipment_cost_method == 'shipment':
|
|
quantity = 0
|
|
return quantity
|
|
|
|
|
|
class HandleInvoiceException(metaclass=PoolMeta):
|
|
__name__ = 'sale.handle.invoice.exception'
|
|
|
|
def transition_handle(self):
|
|
pool = Pool()
|
|
Shipment = pool.get('stock.shipment.out')
|
|
shipment_cost_recreated = set()
|
|
for invoice in self.ask.domain_invoices:
|
|
if invoice in self.ask.recreate_invoices:
|
|
for line in invoice.lines:
|
|
for shipment in line.cost_sale_shipments:
|
|
if shipment in self.record.shipments:
|
|
shipment_cost_recreated.add(shipment)
|
|
if shipment_cost_recreated:
|
|
Shipment.write(list(shipment_cost_recreated), {
|
|
'cost_sale_invoice_line': None,
|
|
})
|
|
return super().transition_handle()
|
|
|
|
|
|
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().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 is not None:
|
|
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'),
|
|
})
|
|
|
|
@classmethod
|
|
def default_amount_shipment_cost_included(cls):
|
|
return False
|
|
|
|
def get_context_formula(self, sale_line):
|
|
context = super().get_context_formula(sale_line)
|
|
if sale_line and sale_line.shipment_cost is not None:
|
|
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 is not None)
|
|
if not self.untaxed_amount:
|
|
amount -= sum(
|
|
t.amount
|
|
for l in sale.lines if l.shipment_cost is not None
|
|
for t in l._get_taxes().values())
|
|
return amount
|