Add support for selling with excise number using excise tax to calculate price list
This commit is contained in:
@@ -119,6 +119,19 @@ The excise code stores the codes from the `System for Exchange of Excise Data
|
||||
.. |Products --> Configuration --> Excise Codes| replace:: :menuselection:`Products --> Configuration --> Excise Codes`
|
||||
__ https://demo.tryton.org/model/product.eu.excise_code
|
||||
|
||||
.. _model-product.price_list:
|
||||
|
||||
Price List
|
||||
==========
|
||||
|
||||
When the *Account Stock EU Excise Module* is activated, the price list gains
|
||||
new criteria based on `Excise Tax <model-account.stock.eu.excise.tax>` and duty
|
||||
suspension.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The Price List concept is introduced by the :doc:`Product Price List Module
|
||||
<product_price_list:index>`.
|
||||
|
||||
.. _concept-stock.location.warehouse:
|
||||
|
||||
@@ -160,3 +173,17 @@ the type of excise duty that applies.
|
||||
|
||||
The `Stock Move <stock:model-stock.move>` concept is introduced by the
|
||||
:doc:`Stock Module <stock:index>`.
|
||||
|
||||
.. _model-sale.sale:
|
||||
|
||||
Sale
|
||||
====
|
||||
|
||||
When the *Account Stock EU Excise Module* is activated, sales gain new
|
||||
properties for storing the `Excise Number <model-party.identifier>` of the
|
||||
customer and calculating the duty amount.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The `Sale <sale:model-sale.sale>` concept is introduced by the :doc:`Sale
|
||||
Module <sale:index>`.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from trytond.model import DeactivableMixin, ModelSQL, ModelView, Unique, fields
|
||||
from trytond.pool import PoolMeta
|
||||
from trytond.pyson import Bool, Eval, If
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class Template(metaclass=PoolMeta):
|
||||
@@ -105,3 +106,25 @@ class Product_EUExciseTax(ModelSQL, ModelView):
|
||||
@classmethod
|
||||
def search_rec_name(cls, name, clause):
|
||||
return [('excise_tax.rec_name', *clause[1:])]
|
||||
|
||||
|
||||
class PriceList(metaclass=PoolMeta):
|
||||
__name__ = 'product.price_list'
|
||||
|
||||
def compute(self, product, quantity, uom, pattern=None):
|
||||
context = Transaction().context
|
||||
pattern = pattern.copy() if pattern is not None else {}
|
||||
pattern.setdefault('eu_excise_tax', context.get('eu_excise_tax'))
|
||||
pattern.setdefault('eu_excise_duty', context.get('eu_excise_duty'))
|
||||
return super().compute(product, quantity, uom, pattern=pattern)
|
||||
|
||||
|
||||
class PriceListLine(metaclass=PoolMeta):
|
||||
__name__ = 'product.price_list.line'
|
||||
|
||||
eu_excise_tax = fields.Many2One(
|
||||
'account.stock.eu.excise.tax', "Excise Tax")
|
||||
eu_excise_duty = fields.Selection([
|
||||
(None, ""),
|
||||
('suspension', "Suspension"),
|
||||
], "Excise Duty Suspension")
|
||||
|
||||
@@ -85,4 +85,24 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="name">product-account_stock_eu_excise_tax_form</field>
|
||||
</record>
|
||||
</data>
|
||||
<data depends="product_price_list">
|
||||
<record model="ir.ui.view" id="product_price_list_line_view_form">
|
||||
<field name="model">product.price_list.line</field>
|
||||
<field name="inherit" ref="product_price_list.price_list_line_view_form"/>
|
||||
<field name="name">product_price_list_line_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_price_list_line_view_list">
|
||||
<field name="model">product.price_list.line</field>
|
||||
<field name="inherit" ref="product_price_list.price_list_line_view_tree"/>
|
||||
<field name="name">product_price_list_line_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_price_list_line_view_list_sequence">
|
||||
<field name="model">product.price_list.line</field>
|
||||
<field name="inherit" ref="product_price_list.price_list_line_view_tree_sequence"/>
|
||||
<field name="name">product_price_list_line_list</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# 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.model import fields
|
||||
from trytond.modules.currency.fields import Monetary
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval, If
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .party import IDENTIFIER_TYPES
|
||||
|
||||
|
||||
class Sale(metaclass=PoolMeta):
|
||||
__name__ = 'sale.sale'
|
||||
|
||||
warehouse_eu_excise_number = fields.Function(
|
||||
fields.Many2One('party.identifier', "Warehouse Excise Number"),
|
||||
'on_change_with_warehouse_eu_excise_number')
|
||||
eu_excise_number = fields.Many2One(
|
||||
'party.identifier', "Excise Number",
|
||||
domain=[
|
||||
('type', 'in', Eval('eu_excise_types', [])),
|
||||
('party', '=', If(Eval('shipment_party', -1),
|
||||
Eval('shipment_party', -1),
|
||||
Eval('party', -1))),
|
||||
('address', '=', Eval('shipment_address', -1)),
|
||||
],
|
||||
states={
|
||||
'invisible': ~Eval('warehouse_eu_excise_number'),
|
||||
'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
|
||||
})
|
||||
eu_excise_types = fields.Function(
|
||||
fields.MultiSelection(IDENTIFIER_TYPES, "Excise Types"),
|
||||
'on_change_with_eu_excise_types')
|
||||
eu_excise_duty_amount = fields.Function(Monetary(
|
||||
"Excise Duty Amount", digits='currency', currency='currency'),
|
||||
'get_eu_excise_duty_amount')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.warehouse.states['readonly'] = (
|
||||
cls.warehouse.states.get('readonly', False)
|
||||
| Eval('eu_excise_number'))
|
||||
|
||||
@fields.depends(methods=['_clear_eu_excise_number'])
|
||||
def on_change_party(self):
|
||||
try:
|
||||
super().on_change_party()
|
||||
except AttributeError:
|
||||
pass
|
||||
self._clear_eu_excise_number()
|
||||
|
||||
@fields.depends(methods=['_clear_eu_excise_number'])
|
||||
def on_change_shipment_party(self):
|
||||
try:
|
||||
super().on_change_shipment_party()
|
||||
except AttributeError:
|
||||
pass
|
||||
self._clear_eu_excise_number()
|
||||
|
||||
@fields.depends('party', 'shipment_party', 'eu_excise_number')
|
||||
def _clear_eu_excise_number(self):
|
||||
if (self.eu_excise_number
|
||||
and self.eu_excise_number.party != (
|
||||
self.shipment_party or self.party)):
|
||||
self.eu_excise_number = None
|
||||
|
||||
@fields.depends('warehouse', 'company')
|
||||
def on_change_with_warehouse_eu_excise_number(self, name=None):
|
||||
if self.warehouse:
|
||||
return self.warehouse.get_eu_excise_number(self.company)
|
||||
|
||||
@fields.depends('warehouse_eu_excise_number')
|
||||
def on_change_with_eu_excise_types(self, name=None):
|
||||
types = set()
|
||||
if self.warehouse_eu_excise_number:
|
||||
types.add('eu_excise')
|
||||
types.add(self.warehouse_eu_excise_number.type)
|
||||
return list(types)
|
||||
|
||||
def get_eu_excise_duty_amount(self, name):
|
||||
return sum(
|
||||
(l.eu_excise_duty_amount for l in self.lines
|
||||
if l.eu_excise_duty_amount is not None),
|
||||
Decimal(0))
|
||||
|
||||
def _get_shipment_sale(self, Shipment, key):
|
||||
shipment = super()._get_shipment_sale(Shipment, key)
|
||||
shipment.eu_excise_number = self.eu_excise_number
|
||||
return shipment
|
||||
|
||||
def _get_shipment_grouping_fields(self, shipment):
|
||||
return super()._get_shipment_grouping_fields(shipment) | {
|
||||
'eu_excise_number'}
|
||||
|
||||
|
||||
class Line(metaclass=PoolMeta):
|
||||
__name__ = 'sale.line'
|
||||
|
||||
eu_excise_duty_amount = fields.Function(Monetary(
|
||||
"Excise Duty Amount", digits='currency', currency='currency'),
|
||||
'get_eu_excise_duty_amount')
|
||||
|
||||
def get_eu_excise_duty_amount(self, name):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
Currency = pool.get('currency.currency')
|
||||
|
||||
amount = None
|
||||
if self.product and self.eu_excise_duty != 'suspension':
|
||||
country = None
|
||||
if (self.warehouse
|
||||
and self.warehouse.address
|
||||
and self.warehouse.address.country):
|
||||
country = self.warehouse.address.country
|
||||
if eu_excise_tax := self.product.get_eu_excise_tax(country):
|
||||
if not (date := self.sale.sale_date):
|
||||
with Transaction().set_context(
|
||||
company=self.sale.company.id):
|
||||
date = Date.today()
|
||||
if tax_rate := eu_excise_tax.get_tax_rate({
|
||||
'date': date,
|
||||
}):
|
||||
amount = tax_rate.compute(
|
||||
self.product, self.quantity, self.unit)
|
||||
if amount is not None:
|
||||
with Transaction().set_context(date=date):
|
||||
amount = Currency.compute(
|
||||
eu_excise_tax.currency, amount, self.currency)
|
||||
return amount
|
||||
|
||||
@property
|
||||
@fields.depends(
|
||||
'sale',
|
||||
'_parent_sale.warehouse',
|
||||
'_parent_sale.warehouse_eu_excise_number',
|
||||
'_parent_sale.eu_excise_number',
|
||||
'product')
|
||||
def eu_excise_duty(self):
|
||||
if (self.sale
|
||||
and self.sale.warehouse_eu_excise_number
|
||||
and self.sale.eu_excise_number
|
||||
and self.product):
|
||||
warehouse_eu_excise_number = self.sale.warehouse_eu_excise_number
|
||||
eu_excise_number = self.sale.eu_excise_number
|
||||
if (warehouse_eu_excise_number.is_excise_product(self.product)
|
||||
and eu_excise_number.is_excise_product(self.product)):
|
||||
return 'suspension'
|
||||
|
||||
@fields.depends(
|
||||
'sale', '_parent_sale.warehouse', 'product',
|
||||
methods=['eu_excise_duty'])
|
||||
def _get_context_sale_price(self):
|
||||
context = super()._get_context_sale_price()
|
||||
context['eu_excise_duty'] = self.eu_excise_duty
|
||||
country = None
|
||||
if (self.warehouse
|
||||
and self.warehouse.address
|
||||
and self.warehouse.address.country):
|
||||
country = self.warehouse.address.country
|
||||
if eu_excise_tax := self.product.get_eu_excise_tax(country):
|
||||
context['eu_excise_tax'] = eu_excise_tax.id
|
||||
return context
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data depends="sale">
|
||||
<record model="ir.ui.view" id="sale_sale_view_form">
|
||||
<field name="model">sale.sale</field>
|
||||
<field name="inherit" ref="sale.sale_view_form"/>
|
||||
<field name="name">sale_sale_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="sale_line_view_list">
|
||||
<field name="model">sale.line</field>
|
||||
<field name="inherit" ref="sale.sale_line_view_tree_sequence"/>
|
||||
<field name="name">sale_line_list</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -0,0 +1,143 @@
|
||||
=====================================
|
||||
Account Stock EU Excise Sale Scenario
|
||||
=====================================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from decimal import Decimal
|
||||
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.modules.company.tests.tools import create_company, get_company
|
||||
>>> from trytond.modules.currency.tests.tools import get_currency
|
||||
>>> from trytond.tests.tools import activate_modules, assertEqual
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules(
|
||||
... ['account_stock_eu_excise', 'sale', 'sale_price_list'],
|
||||
... create_company)
|
||||
|
||||
>>> Country = Model.get('country.country')
|
||||
>>> ExciseCode = Model.get('product.eu.excise_code')
|
||||
>>> ExciseTax = Model.get('account.stock.eu.excise.tax')
|
||||
>>> Location = Model.get('stock.location')
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> PriceList = Model.get('product.price_list')
|
||||
>>> ProductTemplate = Model.get('product.template')
|
||||
>>> ProductUom = Model.get('product.uom')
|
||||
>>> Sale = Model.get('sale.sale')
|
||||
|
||||
Get company::
|
||||
|
||||
>>> company = get_company()
|
||||
|
||||
Setup tax warehouse::
|
||||
|
||||
>>> liter, = ProductUom.find([('name', '=', "Liter")])
|
||||
>>> france = Country(code='FR', name='France')
|
||||
>>> france.save()
|
||||
|
||||
>>> excise_code = ExciseCode(code='W200')
|
||||
>>> excise_code.save()
|
||||
|
||||
>>> company_party = company.party
|
||||
>>> company_address, = company_party.addresses
|
||||
>>> company_address.country = france
|
||||
>>> excise_number = company_party.identifiers.new(type='eu_excise')
|
||||
>>> excise_number.address = company_address
|
||||
>>> excise_number.code = "LU00000987ABC"
|
||||
>>> excise_number.eu_excise_codes.append(ExciseCode(excise_code.id))
|
||||
>>> company_party.save()
|
||||
>>> excise_number, = company_party.identifiers
|
||||
|
||||
>>> warehouse, = Location.find([('code', '=', 'WH')])
|
||||
>>> warehouse.address = company_address
|
||||
>>> wh_excise_number = warehouse.eu_excise_numbers.new()
|
||||
>>> wh_excise_number.eu_excise_number = excise_number
|
||||
>>> warehouse.save()
|
||||
|
||||
>>> excise_tax = ExciseTax(code='TAX')
|
||||
>>> excise_tax.quantity = 'measurement_volume'
|
||||
>>> excise_tax.uom = liter
|
||||
>>> excise_tax.country = france
|
||||
>>> excise_tax.currency = get_currency('EUR')
|
||||
>>> tax_rate = excise_tax.tax_rates.new()
|
||||
>>> tax_rate.formula = 'quantity * 5'
|
||||
>>> excise_tax.save()
|
||||
|
||||
Create a customer::
|
||||
|
||||
>>> customer = Party(name="Customer")
|
||||
>>> customer.save()
|
||||
>>> customer_excise_number = customer.identifiers.new(type='eu_excise')
|
||||
>>> customer_excise_number.address, = customer.addresses
|
||||
>>> customer_excise_number.code = "LU00000987DEF"
|
||||
>>> customer_excise_number.eu_excise_codes.append(ExciseCode(excise_code.id))
|
||||
>>> customer.save()
|
||||
>>> customer_excise_number, = customer.identifiers
|
||||
|
||||
Create a product::
|
||||
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = "Wine"
|
||||
>>> template.default_uom = liter
|
||||
>>> template.type = 'goods'
|
||||
>>> template.list_price = Decimal('20.0000')
|
||||
>>> template.eu_excise_code = excise_code
|
||||
>>> template.salable = True
|
||||
>>> _ = template.eu_excise_taxes.new(excise_tax=excise_tax)
|
||||
>>> template.save()
|
||||
>>> product, = template.products
|
||||
|
||||
Create a price list::
|
||||
|
||||
>>> price_list = PriceList(name="Price", price='list_price')
|
||||
|
||||
>>> price_list_line = price_list.lines.new()
|
||||
>>> price_list_line.eu_excise_tax = excise_tax
|
||||
>>> price_list_line.eu_excise_duty = 'suspension'
|
||||
>>> price_list_line.formula = 'unit_price * .9'
|
||||
|
||||
>>> price_list_line = price_list.lines.new()
|
||||
>>> price_list_line.eu_excise_tax = excise_tax
|
||||
>>> price_list_line.eu_excise_duty = None
|
||||
>>> price_list_line.formula = 'unit_price'
|
||||
|
||||
>>> price_list.save()
|
||||
|
||||
Create a sale without suspension::
|
||||
|
||||
>>> sale = Sale(party=customer, price_list=price_list)
|
||||
>>> sale.invoice_method = 'manual'
|
||||
>>> line = sale.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.unit_price
|
||||
Decimal('20.0000')
|
||||
>>> line.quantity = 1
|
||||
>>> sale.save()
|
||||
>>> sale.eu_excise_duty_amount
|
||||
Decimal('2.50')
|
||||
|
||||
Create a sale with suspension::
|
||||
|
||||
>>> sale = Sale(party=customer, price_list=price_list)
|
||||
>>> sale.invoice_method = 'manual'
|
||||
>>> sale.eu_excise_number = customer_excise_number
|
||||
>>> line = sale.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.unit_price
|
||||
Decimal('18.0000')
|
||||
>>> line.quantity = 1
|
||||
>>> sale.save()
|
||||
>>> sale.eu_excise_duty_amount
|
||||
Decimal('0')
|
||||
|
||||
Check excise number is passed to shipment::
|
||||
|
||||
>>> sale.click('quote')
|
||||
>>> sale.click('confirm')
|
||||
>>> sale.state
|
||||
'processing'
|
||||
|
||||
>>> shipment, = sale.shipments
|
||||
>>> assertEqual(shipment.eu_excise_number, customer_excise_number)
|
||||
@@ -8,7 +8,9 @@ from trytond.tests.test_tryton import ModuleTestCase
|
||||
class AccountStockEuExciseTestCase(CompanyTestMixin, ModuleTestCase):
|
||||
"Test Account Stock Eu Excise module"
|
||||
module = 'account_stock_eu_excise'
|
||||
extras = ['product_measurements', 'production']
|
||||
extras = [
|
||||
'product_price_list', 'production', 'sale', 'sale_shipment_grouping',
|
||||
'sale_price_list']
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
|
||||
@@ -12,11 +12,16 @@ depends:
|
||||
product_measurements
|
||||
stock
|
||||
extras_depend:
|
||||
product_price_list
|
||||
production
|
||||
sale
|
||||
sale_shipment_grouping
|
||||
sale_price_list
|
||||
xml:
|
||||
party.xml
|
||||
product.xml
|
||||
stock.xml
|
||||
sale.xml
|
||||
account_stock_eu.xml
|
||||
message.xml
|
||||
|
||||
@@ -45,6 +50,16 @@ model:
|
||||
account_stock_eu.ExciseDeclarationProduct
|
||||
account_stock_eu.ExciseDeclarationProductLine
|
||||
|
||||
[register product_price_list]
|
||||
model:
|
||||
product.PriceList
|
||||
product.PriceListLine
|
||||
|
||||
[register production]
|
||||
model:
|
||||
account_stock_eu.ExciseDeclarationProductLine_Production
|
||||
|
||||
[register sale]
|
||||
model:
|
||||
sale.Sale
|
||||
sale.Line
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="//field[@name='product']" position="after">
|
||||
<label name="eu_excise_tax"/>
|
||||
<field name="eu_excise_tax"/>
|
||||
<label name="eu_excise_duty"/>
|
||||
<field name="eu_excise_duty"/>
|
||||
</xpath>
|
||||
</data>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="//field[@name='product']" position="after">
|
||||
<field name="eu_excise_tax" expand="1" optional="1"/>
|
||||
<field name="eu_excise_duty" optional="1"/>
|
||||
</xpath>
|
||||
</data>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="//field[@name='taxes']" position="after">
|
||||
<field name="eu_excise_duty_amount" optional="1"/>
|
||||
</xpath>
|
||||
</data>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="//field[@name='shipment_address']" position="after">
|
||||
<label name="eu_excise_number"/>
|
||||
<field name="eu_excise_number"/>
|
||||
<newline/>
|
||||
</xpath>
|
||||
</data>
|
||||
Reference in New Issue
Block a user