first commit
This commit is contained in:
543
modules/sale_shipment_cost/sale.py
Normal file
543
modules/sale_shipment_cost/sale.py
Normal file
@@ -0,0 +1,543 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user