diff --git a/__init__.py b/__init__.py index 3568075..7546a06 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,7 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. from trytond.pool import Pool +from . import amendment from . import sale from . import move @@ -11,3 +12,7 @@ def register(): sale.SaleLine, move.Move, module='sale_discount', type_='model') + Pool.register( + amendment.AmendmentLine, + module='sale_discount', type_='model', + depends=['sale_amendment']) diff --git a/amendment.py b/amendment.py new file mode 100644 index 0000000..8029f12 --- /dev/null +++ b/amendment.py @@ -0,0 +1,76 @@ +# 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.pool import PoolMeta +from trytond.pyson import Eval +from trytond.modules.account_invoice_discount.invoice import (gross_unit_price_digits, + discount_digits) +from trytond.modules.product import round_price + +STATES={ + 'readonly': Eval('state') != 'draft', + 'invisible': Eval('action') != 'line', + 'required': Eval('action') == 'line', + } + + +class AmendmentLine(metaclass=PoolMeta): + __name__ = 'sale.amendment.line' + gross_unit_price = fields.Numeric('Gross Price', digits=gross_unit_price_digits, + states=STATES, depends=['state', 'action']) + discount = fields.Numeric('Discount', digits=discount_digits, + states=STATES, depends=['state', 'action']) + + @classmethod + def __setup__(cls): + super().__setup__() + cls.unit_price.states['readonly'] = True + + @fields.depends(methods=['update_prices']) + def on_change_gross_unit_price(self): + return self.update_prices() + + @fields.depends('unit_price', methods=['update_prices']) + def on_change_unit_price(self): + # unit_price has readonly state but could set unit_price from source code + if self.unit_price is not None: + self.update_prices() + + @fields.depends(methods=['update_prices']) + def on_change_discount(self): + return self.update_prices() + + @fields.depends('line') + def on_change_line(self): + super().on_change_line() + if self.line: + self.gross_unit_price = self.line.gross_unit_price + self.discount = self.line.discount + else: + self.gross_unit_price = None + self.discount = None + + def _apply_line(self, sale, sale_line): + super()._apply_line(sale, sale_line) + sale_line.gross_unit_price = self.gross_unit_price + sale_line.discount = self.discount + + @fields.depends('gross_unit_price', 'unit_price', 'discount') + def update_prices(self): + # TODO not support amendment upgrade_prices and sale_discount from sale (header) + unit_price = None + gross_unit_price = self.gross_unit_price + if self.gross_unit_price is not None and self.discount is not None: + unit_price = self.gross_unit_price * (1 - self.discount) + unit_price = round_price(unit_price) + + if self.discount != 1: + gross_unit_price = unit_price / (1 - self.discount) + + gup_digits = self.__class__.gross_unit_price.digits[1] + gross_unit_price = gross_unit_price.quantize( + Decimal(str(10.0 ** -gup_digits))) + + self.gross_unit_price = gross_unit_price + self.unit_price = unit_price diff --git a/amendment.xml b/amendment.xml new file mode 100644 index 0000000..05eb82f --- /dev/null +++ b/amendment.xml @@ -0,0 +1,12 @@ + + + + + + sale.amendment.line + + sale_amendment_line_form + + + diff --git a/locale/ca.po b/locale/ca.po index 656a6dd..c7c3b6e 100644 --- a/locale/ca.po +++ b/locale/ca.po @@ -10,10 +10,6 @@ msgctxt "field:sale.line,gross_unit_price:" msgid "Gross Price" msgstr "Preu brut" -msgctxt "field:sale.line,gross_unit_price_wo_round:" -msgid "Gross Price without rounding" -msgstr "Preu brut sense arrodoniment" - msgctxt "field:sale.sale,sale_discount:" msgid "Sale Discount" msgstr "Descompte venda" diff --git a/locale/es.po b/locale/es.po index b3604c6..701c8b1 100644 --- a/locale/es.po +++ b/locale/es.po @@ -10,10 +10,6 @@ msgctxt "field:sale.line,gross_unit_price:" msgid "Gross Price" msgstr "Precio bruto" -msgctxt "field:sale.line,gross_unit_price_wo_round:" -msgid "Gross Price without rounding" -msgstr "Precio bruto sin redondeo" - msgctxt "field:sale.sale,sale_discount:" msgid "Sale Discount" msgstr "Descuento venta" diff --git a/sale.py b/sale.py index 093fb93..038bc27 100644 --- a/sale.py +++ b/sale.py @@ -6,11 +6,10 @@ from trytond.model import fields from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval from trytond.transaction import Transaction -from trytond.modules.account_invoice_discount.invoice import discount_digits from trytond.modules.currency.fields import Monetary -from trytond.modules.product import price_digits, round_price - -__all__ = ['Sale', 'SaleLine', 'discount_digits'] +from trytond.modules.account_invoice_discount.invoice import (gross_unit_price_digits, + discount_digits) +from trytond.modules.product import round_price STATES = { 'invisible': Eval('type') != 'line', @@ -82,11 +81,8 @@ class Sale(metaclass=PoolMeta): class SaleLine(metaclass=PoolMeta): __name__ = 'sale.line' - gross_unit_price = Monetary('Gross Price', digits=price_digits, + gross_unit_price = Monetary('Gross Price', digits=gross_unit_price_digits, currency='currency', states=STATES, depends=['type', 'sale_state']) - gross_unit_price_wo_round = Monetary('Gross Price without rounding', - digits=(16, price_digits[1] + discount_digits[1]), currency='currency', - readonly=True) discount = fields.Numeric('Discount', digits=discount_digits, states=STATES, depends=['type', 'sale_state']) @@ -94,7 +90,6 @@ class SaleLine(metaclass=PoolMeta): def __setup__(cls): super().__setup__() cls.unit_price.states['readonly'] = True - cls.unit_price.digits = (20, price_digits[1] + discount_digits[1]) @staticmethod def default_discount(): @@ -106,16 +101,13 @@ class SaleLine(metaclass=PoolMeta): and self.promotion and self.draft_unit_price) - @fields.depends('gross_unit_price', 'discount', + @fields.depends('sale', 'gross_unit_price', 'unit_price', 'discount', methods=['on_change_with_amount']) def update_prices(self): unit_price = None - gross_unit_price = gross_unit_price_wo_round = self.gross_unit_price + gross_unit_price = self.gross_unit_price sale_discount = Transaction().context.get('sale_discount') - if self.gross_unit_price is None: - return - if sale_discount is None: if self.sale and hasattr(self.sale, 'sale_discount'): sale_discount = self.sale.sale_discount or Decimal(0) @@ -136,21 +128,19 @@ class SaleLine(metaclass=PoolMeta): discount = (self.discount + sale_discount - self.discount * sale_discount) if discount != 1: - gross_unit_price_wo_round = unit_price / (1 - discount) + gross_unit_price = unit_price / (1 - discount) elif self.discount and self.discount != 1: - gross_unit_price_wo_round = unit_price / (1 - self.discount) + gross_unit_price = unit_price / (1 - self.discount) elif sale_discount and sale_discount != 1: - gross_unit_price_wo_round = unit_price / (1 - sale_discount) + gross_unit_price = unit_price / (1 - sale_discount) unit_price = round_price(unit_price) - gup_wo_r_digits = self.__class__.gross_unit_price_wo_round.digits[1] - gross_unit_price_wo_round = gross_unit_price_wo_round.quantize( - Decimal(str(10.0 ** -gup_wo_r_digits))) - gross_unit_price = gross_unit_price_wo_round + gup_digits = self.__class__.gross_unit_price.digits[1] + gross_unit_price = gross_unit_price.quantize( + Decimal(str(10.0 ** -gup_digits))) - self.gross_unit_price = round_price(gross_unit_price) - self.gross_unit_price_wo_round = gross_unit_price_wo_round + self.gross_unit_price = gross_unit_price if self.has_promotion: self.draft_unit_price = unit_price else: @@ -165,6 +155,12 @@ class SaleLine(metaclass=PoolMeta): def on_change_gross_unit_price(self): return self.update_prices() + @fields.depends('unit_price', methods=['update_prices']) + def on_change_unit_price(self): + # unit_price has readonly state but could set unit_price from source code + if self.unit_price is not None: + self.update_prices() + @fields.depends('sale', methods=['update_prices']) def on_change_discount(self): return self.update_prices() diff --git a/tests/scenario_sale_amendment.rst b/tests/scenario_sale_amendment.rst new file mode 100644 index 0000000..662545a --- /dev/null +++ b/tests/scenario_sale_amendment.rst @@ -0,0 +1,163 @@ +======================= +Sale Amendment Scenario +======================= + +Imports:: + + >>> from decimal import Decimal + >>> from proteus import Model, Wizard + >>> from trytond.tests.tools import activate_modules + >>> from trytond.modules.company.tests.tools import create_company, \ + ... get_company + >>> from trytond.modules.account.tests.tools import create_fiscalyear, \ + ... create_chart, get_accounts + >>> from trytond.modules.account_invoice.tests.tools import \ + ... set_fiscalyear_invoice_sequences + +Activate modules:: + + >>> config = activate_modules(['sale_discount', 'sale_amendment']) + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create fiscal year:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences( + ... create_fiscalyear(company)) + >>> fiscalyear.click('create_period') + +Create chart of accounts:: + + >>> _ = create_chart(company) + >>> accounts = get_accounts(company) + >>> revenue = accounts['revenue'] + >>> expense = accounts['expense'] + +Create parties:: + + >>> Party = Model.get('party.party') + >>> customer1 = Party(name="Customer 1") + >>> customer1.save() + >>> customer2 = Party(name="Customer 2") + >>> customer2.save() + +Create account categories:: + + >>> ProductCategory = Model.get('product.category') + >>> account_category = ProductCategory(name="Account Category") + >>> account_category.accounting = True + >>> account_category.account_expense = expense + >>> account_category.account_revenue = revenue + >>> account_category.save() + +Create products:: + + >>> ProductUom = Model.get('product.uom') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> ProductTemplate = Model.get('product.template') + + >>> template = ProductTemplate() + >>> template.name = 'product' + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.salable = True + >>> template.list_price = Decimal('10') + >>> template.account_category = account_category + >>> _ = template.products.new() + >>> template.save() + >>> product1, product2 = template.products + +Sale first product:: + + >>> Sale = Model.get('sale.sale') + >>> sale = Sale() + >>> sale.party = customer1 + >>> sale_line = sale.lines.new() + >>> sale_line.product = product1 + >>> sale_line.quantity = 5.0 + >>> sale_line = sale.lines.new() + >>> sale_line.quantity = 1 + >>> sale_line.product = product1 + >>> sale_line.gross_unit_price = Decimal('10.0000') + >>> sale_line.discount = Decimal('0.10') + >>> sale_line.unit_price == Decimal('9.0000') + True + >>> sale_line = sale.lines.new() + >>> sale_line.product = product1 + >>> sale_line.quantity = 3.0 + >>> sale_line.amount == Decimal('30.00') + True + >>> sale.click('quote') + >>> sale.click('confirm') + >>> sale.state + 'processing' + >>> sale.revision + 0 + >>> sale.total_amount + Decimal('89.00') + >>> len(sale.shipments), len(sale.invoices) + (1, 1) + +Add an amendment:: + + >>> amendment = sale.amendments.new() + >>> line = amendment.lines.new() + >>> line.action = 'line' + >>> line.line = sale.lines[0] + >>> line.product == product1 + True + >>> line.product = product2 + >>> line.quantity + 5.0 + >>> line.quantity = 4.0 + >>> line.gross_unit_price + Decimal('10.0000') + >>> line.discount + Decimal('0') + >>> line.unit_price + Decimal('10.0000') + + >>> line = amendment.lines.new() + >>> line.action = 'line' + >>> line.line = sale.lines[1] + >>> line.gross_unit_price + Decimal('10.0000') + >>> line.discount + Decimal('0.10') + >>> line.unit_price + Decimal('9.0000') + >>> line.discount = Decimal('0.20') + >>> line.unit_price + Decimal('8.0000') + >>> line.gross_unit_price = Decimal('20.0000') + >>> line.unit_price + Decimal('16.0000') + >>> amendment.save() + +Validate amendment:: + + >>> amendment.click('validate_amendment') + >>> sale.reload() + >>> sale.revision + 1 + >>> line = sale.lines[0] + >>> line.product == product2 + True + >>> line.quantity + 4.0 + >>> line.gross_unit_price + Decimal('10.0000') + >>> line.unit_price + Decimal('10.0000') + >>> line = sale.lines[1] + >>> line.gross_unit_price + Decimal('20.0000') + >>> line.unit_price + Decimal('16.0000') + >>> line.discount + Decimal('0.20') + >>> sale.total_amount + Decimal('86.00') diff --git a/tests/test_module.py b/tests/test_module.py index af10871..775103a 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -9,6 +9,7 @@ from trytond.tests.test_tryton import ModuleTestCase class SaleDiscountTestCase(CompanyTestMixin, ModuleTestCase): 'Test SaleDiscount module' module = 'sale_discount' + extras = ['sale_amendment'] del ModuleTestCase diff --git a/tryton.cfg b/tryton.cfg index e978be5..fff1674 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -4,8 +4,10 @@ depends: sale account_invoice_discount extras_depend: + sale_amendment sale_promotion sale_shipment_cost purchase_shipment_cost xml: sale.xml + amendment.xml diff --git a/view/sale_amendment_line_form.xml b/view/sale_amendment_line_form.xml new file mode 100644 index 0000000..e60d7c9 --- /dev/null +++ b/view/sale_amendment_line_form.xml @@ -0,0 +1,16 @@ + + + + + +