This commit is contained in:
@@ -3,16 +3,24 @@ name: Publish Package
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "8.0"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: https://gitea.com/actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
github-server-url: "http://unpaismejor-gitea-app:3000"
|
||||||
|
ref: "8.0"
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Detect package branch
|
- name: Detect package branch
|
||||||
run: |
|
run: |
|
||||||
@@ -21,7 +29,9 @@ jobs:
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
branch = (
|
branch = (
|
||||||
os.environ.get('GITHUB_REF_NAME')
|
os.environ.get('GITEA_REF_NAME')
|
||||||
|
or os.environ.get('GITEA_HEAD_REF')
|
||||||
|
or os.environ.get('GITHUB_REF_NAME')
|
||||||
or os.environ.get('GITHUB_HEAD_REF')
|
or os.environ.get('GITHUB_HEAD_REF')
|
||||||
or '')
|
or '')
|
||||||
publish = bool(re.fullmatch(r'\d+\.\d+', branch))
|
publish = bool(re.fullmatch(r'\d+\.\d+', branch))
|
||||||
@@ -75,7 +85,7 @@ jobs:
|
|||||||
|
|
||||||
OWNER = 'tryton-do'
|
OWNER = 'tryton-do'
|
||||||
PACKAGE_TYPE = 'pypi'
|
PACKAGE_TYPE = 'pypi'
|
||||||
API_ROOT = 'https://gitea.joseagrc.com/api/v1'
|
API_ROOT = 'https://code.unpaismejor.org.do/api/v1'
|
||||||
|
|
||||||
def normalize(name):
|
def normalize(name):
|
||||||
return re.sub(r'[-_.]+', '-', name).lower()
|
return re.sub(r'[-_.]+', '-', name).lower()
|
||||||
@@ -187,5 +197,5 @@ jobs:
|
|||||||
TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
python -m twine upload \
|
python -m twine upload \
|
||||||
--repository-url https://gitea.joseagrc.com/api/packages/tryton-do/pypi \
|
--repository-url https://code.unpaismejor.org.do/api/packages/tryton-do/pypi \
|
||||||
dist/*
|
dist/*
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Version 8.0.0 - 2026-08-14
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
* Preserve the source invoice's signed adjustment lines and manual taxes.
|
||||||
|
* Make partial debit rounding operate on signed net amounts.
|
||||||
|
* Reject ambiguous fixed-amount operations on multiple source invoices.
|
||||||
|
* Declare Fundación Un País Mejor as maintainer and package publisher.
|
||||||
|
* Add the Gitea Actions package publishing workflow for the 8.0 series.
|
||||||
+1
-1
@@ -13,7 +13,7 @@ Installing
|
|||||||
|
|
||||||
Install the package and activate the module in the database::
|
Install the package and activate the module in the database::
|
||||||
|
|
||||||
pip install trytond_account_debit_note
|
pip install unpaismejor_account_debit_note
|
||||||
trytond-admin -d <database> -u account_debit_note
|
trytond-admin -d <database> -u account_debit_note
|
||||||
|
|
||||||
Publishing
|
Publishing
|
||||||
|
|||||||
+32
-26
@@ -30,6 +30,7 @@ _ONE_HUNDRED = Decimal('100')
|
|||||||
|
|
||||||
|
|
||||||
def _decimal(value):
|
def _decimal(value):
|
||||||
|
"""Return *value* as Decimal without binary floating-point artifacts."""
|
||||||
if value is None:
|
if value is None:
|
||||||
return _ZERO
|
return _ZERO
|
||||||
return Decimal(str(value))
|
return Decimal(str(value))
|
||||||
@@ -107,7 +108,8 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
|
|
||||||
Tryton core exposes Invoice._credit(), which copies the invoice and
|
Tryton core exposes Invoice._credit(), which copies the invoice and
|
||||||
reverses line signs. Reusing it keeps the same field-copying behavior
|
reverses line signs. Reusing it keeps the same field-copying behavior
|
||||||
as the core credit flow; the debit note then restores normal signs.
|
as the core credit flow; the debit note then reverses those signs once
|
||||||
|
more to restore the exact signs of the source invoice.
|
||||||
"""
|
"""
|
||||||
debit = self._credit(**values)
|
debit = self._credit(**values)
|
||||||
self.__class__._debit_force_positive_sign(debit)
|
self.__class__._debit_force_positive_sign(debit)
|
||||||
@@ -115,48 +117,49 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _debit_force_positive_sign(cls, debit_note):
|
def _debit_force_positive_sign(cls, debit_note):
|
||||||
Line = Pool().get('account.invoice.line')
|
"""Undo the sign reversal performed by Tryton's core ``_credit``.
|
||||||
|
|
||||||
|
Do not use ``abs`` here: invoices may legitimately contain negative
|
||||||
|
adjustment lines or manual taxes. A debit note must preserve those
|
||||||
|
relative signs rather than turn every component positive.
|
||||||
|
"""
|
||||||
touched = []
|
touched = []
|
||||||
for line in debit_note.lines:
|
for line in debit_note.lines:
|
||||||
if line.type != 'line':
|
if line.type != 'line':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
quantity = _decimal(line.quantity)
|
quantity = _decimal(line.quantity)
|
||||||
unit_price = _decimal(line.unit_price)
|
new_quantity = -quantity
|
||||||
new_quantity = abs(quantity)
|
|
||||||
new_unit_price = abs(unit_price)
|
|
||||||
|
|
||||||
if quantity != new_quantity or unit_price != new_unit_price:
|
if quantity != new_quantity:
|
||||||
line.quantity = new_quantity
|
line.quantity = new_quantity
|
||||||
line.unit_price = new_unit_price
|
|
||||||
touched.append(line)
|
touched.append(line)
|
||||||
|
|
||||||
if touched and all(getattr(line, 'id', None) for line in touched):
|
if touched and all(getattr(line, 'id', None) for line in touched):
|
||||||
|
Line = Pool().get('account.invoice.line')
|
||||||
Line.save(touched)
|
Line.save(touched)
|
||||||
|
|
||||||
for tax in getattr(debit_note, 'taxes', None) or []:
|
for tax in getattr(debit_note, 'taxes', None) or []:
|
||||||
tax.base = abs(_decimal(tax.base))
|
tax.base = -_decimal(tax.base)
|
||||||
tax.amount = abs(_decimal(tax.amount))
|
tax.amount = -_decimal(tax.amount)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _debit_apply_factor(cls, origin, debit_note, factor):
|
def _debit_apply_factor(cls, origin, debit_note, factor):
|
||||||
if factor == _ONE:
|
if factor == _ONE:
|
||||||
return
|
return
|
||||||
|
|
||||||
Line = Pool().get('account.invoice.line')
|
|
||||||
lines = [line for line in debit_note.lines if line.type == 'line']
|
lines = [line for line in debit_note.lines if line.type == 'line']
|
||||||
touched = []
|
touched = []
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
quantity = _decimal(line.quantity)
|
|
||||||
unit_price = _decimal(line.unit_price)
|
unit_price = _decimal(line.unit_price)
|
||||||
scale = _unit_price_scale(line)
|
scale = _unit_price_scale(line)
|
||||||
|
|
||||||
line.quantity = abs(quantity)
|
line.unit_price = _round_decimal(unit_price * factor, scale)
|
||||||
line.unit_price = _round_decimal(abs(unit_price) * factor, scale)
|
|
||||||
touched.append(line)
|
touched.append(line)
|
||||||
|
|
||||||
if touched and all(getattr(line, 'id', None) for line in touched):
|
if touched and all(getattr(line, 'id', None) for line in touched):
|
||||||
|
Line = Pool().get('account.invoice.line')
|
||||||
Line.save(touched)
|
Line.save(touched)
|
||||||
|
|
||||||
cls._debit_adjust_last_line(origin, touched, factor)
|
cls._debit_adjust_last_line(origin, touched, factor)
|
||||||
@@ -166,11 +169,11 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
if not lines:
|
if not lines:
|
||||||
return
|
return
|
||||||
|
|
||||||
Line = Pool().get('account.invoice.line')
|
target_untaxed = _decimal(origin.untaxed_amount) * factor
|
||||||
target_untaxed_abs = abs(_decimal(origin.untaxed_amount)) * factor
|
current_untaxed = sum(
|
||||||
current_untaxed_abs = sum(
|
(_decimal(cls._debit_line_amount(line)) for line in lines),
|
||||||
abs(_decimal(cls._debit_line_amount(line))) for line in lines)
|
_ZERO)
|
||||||
difference = target_untaxed_abs - current_untaxed_abs
|
difference = target_untaxed - current_untaxed
|
||||||
if abs(difference) < Decimal('0.0001'):
|
if abs(difference) < Decimal('0.0001'):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -179,19 +182,18 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
if quantity == _ZERO:
|
if quantity == _ZERO:
|
||||||
return
|
return
|
||||||
|
|
||||||
unit_price_abs = abs(_decimal(last.unit_price))
|
new_unit_price = _decimal(last.unit_price) + difference / quantity
|
||||||
unit_price_delta_abs = difference / abs(quantity)
|
last.unit_price = _round_decimal(
|
||||||
new_unit_price_abs = unit_price_abs + unit_price_delta_abs
|
new_unit_price, _unit_price_scale(last))
|
||||||
if new_unit_price_abs < _ZERO:
|
|
||||||
new_unit_price_abs = _ZERO
|
|
||||||
|
|
||||||
last.quantity = abs(quantity)
|
|
||||||
last.unit_price = _round_decimal(new_unit_price_abs, _unit_price_scale(last))
|
|
||||||
if getattr(last, 'id', None):
|
if getattr(last, 'id', None):
|
||||||
|
Line = Pool().get('account.invoice.line')
|
||||||
Line.save([last])
|
Line.save([last])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _debit_line_amount(cls, line):
|
def _debit_line_amount(cls, line):
|
||||||
|
on_change_with_amount = getattr(line, 'on_change_with_amount', None)
|
||||||
|
if on_change_with_amount:
|
||||||
|
return on_change_with_amount()
|
||||||
return _decimal(line.quantity) * _decimal(line.unit_price)
|
return _decimal(line.quantity) * _decimal(line.unit_price)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -223,6 +225,10 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
debit_amount = values.pop('debit_amount', None)
|
debit_amount = values.pop('debit_amount', None)
|
||||||
debit_amount_basis = values.pop('debit_amount_basis', None) or 'total'
|
debit_amount_basis = values.pop('debit_amount_basis', None) or 'total'
|
||||||
|
|
||||||
|
if debit_mode == 'amount' and len(invoices) != 1:
|
||||||
|
raise UserError(gettext(
|
||||||
|
'account_debit_note.msg_amount_multiple_invoices'))
|
||||||
|
|
||||||
factors = [
|
factors = [
|
||||||
cls._debit_compute_factor(
|
cls._debit_compute_factor(
|
||||||
origin, debit_mode, debit_percent, debit_amount,
|
origin, debit_mode, debit_percent, debit_amount,
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ msgctxt "model:ir.message,text:msg_missing_debit_percent"
|
|||||||
msgid "Debit percentage is required."
|
msgid "Debit percentage is required."
|
||||||
msgstr "El porcentaje de débito es obligatorio."
|
msgstr "El porcentaje de débito es obligatorio."
|
||||||
|
|
||||||
|
msgctxt "model:ir.message,text:msg_amount_multiple_invoices"
|
||||||
|
msgid "A fixed debit amount can only be applied to one source invoice at a time."
|
||||||
|
msgstr "Un importe fijo de débito solo puede aplicarse a una factura de origen a la vez."
|
||||||
|
|
||||||
msgctxt "model:ir.ui.view,name:debit_start_view_form"
|
msgctxt "model:ir.ui.view,name:debit_start_view_form"
|
||||||
msgid "debit_start_form"
|
msgid "debit_start_form"
|
||||||
msgstr "debit_start_form"
|
msgstr "debit_start_form"
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ msgctxt "model:ir.message,text:msg_missing_debit_percent"
|
|||||||
msgid "Debit percentage is required."
|
msgid "Debit percentage is required."
|
||||||
msgstr "El porcentaje de débito es requerido."
|
msgstr "El porcentaje de débito es requerido."
|
||||||
|
|
||||||
|
msgctxt "model:ir.message,text:msg_amount_multiple_invoices"
|
||||||
|
msgid "A fixed debit amount can only be applied to one source invoice at a time."
|
||||||
|
msgstr "Un monto fijo de débito solo puede aplicarse a una factura de origen a la vez."
|
||||||
|
|
||||||
msgctxt "model:ir.ui.view,name:debit_start_view_form"
|
msgctxt "model:ir.ui.view,name:debit_start_view_form"
|
||||||
msgid "debit_start_form"
|
msgid "debit_start_form"
|
||||||
msgstr "debit_start_form"
|
msgstr "debit_start_form"
|
||||||
|
|||||||
@@ -19,5 +19,8 @@
|
|||||||
<record model="ir.message" id="msg_invalid_debit_mode">
|
<record model="ir.message" id="msg_invalid_debit_mode">
|
||||||
<field name="text">Invalid debit note mode.</field>
|
<field name="text">Invalid debit note mode.</field>
|
||||||
</record>
|
</record>
|
||||||
|
<record model="ir.message" id="msg_amount_multiple_invoices">
|
||||||
|
<field name="text">A fixed debit amount can only be applied to one source invoice at a time.</field>
|
||||||
|
</record>
|
||||||
</data>
|
</data>
|
||||||
</tryton>
|
</tryton>
|
||||||
|
|||||||
+5
-4
@@ -3,11 +3,11 @@ requires = ['hatchling >= 1', 'hatch-tryton']
|
|||||||
build-backend = 'hatchling.build'
|
build-backend = 'hatchling.build'
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = 'trytond_account_debit_note'
|
name = 'unpaismejor_account_debit_note'
|
||||||
dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme']
|
dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme']
|
||||||
requires-python = '>=3.10'
|
requires-python = '>=3.10'
|
||||||
maintainers = [
|
maintainers = [
|
||||||
{name = "Solutema"},
|
{name = "Fundación Un País Mejor"},
|
||||||
]
|
]
|
||||||
description = "Debit notes for Tryton invoices"
|
description = "Debit notes for Tryton invoices"
|
||||||
license = 'GPL-3.0-or-later'
|
license = 'GPL-3.0-or-later'
|
||||||
@@ -26,11 +26,12 @@ classifiers = [
|
|||||||
account_debit_note = 'trytond.modules.account_debit_note'
|
account_debit_note = 'trytond.modules.account_debit_note'
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
homepage = "https://www.tryton.org/"
|
homepage = "https://www.unpaismejor.org.do"
|
||||||
repository = "https://gitea.joseagrc.com/tryton-do/account_debit_note"
|
repository = "https://code.unpaismejor.org.do/tryton-do/account_debit_note"
|
||||||
|
|
||||||
[tool.hatch.build]
|
[tool.hatch.build]
|
||||||
include = [
|
include = [
|
||||||
|
'CHANGELOG',
|
||||||
'COPYRIGHT',
|
'COPYRIGHT',
|
||||||
'LICENSE',
|
'LICENSE',
|
||||||
'README.rst',
|
'README.rst',
|
||||||
|
|||||||
@@ -11,6 +11,28 @@ class MockInvoice:
|
|||||||
total_amount = Decimal('118.00')
|
total_amount = Decimal('118.00')
|
||||||
|
|
||||||
|
|
||||||
|
class MockLine:
|
||||||
|
type = 'line'
|
||||||
|
|
||||||
|
def __init__(self, quantity, unit_price):
|
||||||
|
self.quantity = Decimal(quantity)
|
||||||
|
self.unit_price = Decimal(unit_price)
|
||||||
|
|
||||||
|
|
||||||
|
class MockTax:
|
||||||
|
|
||||||
|
def __init__(self, base, amount):
|
||||||
|
self.base = Decimal(base)
|
||||||
|
self.amount = Decimal(amount)
|
||||||
|
|
||||||
|
|
||||||
|
class MockDebitNote:
|
||||||
|
|
||||||
|
def __init__(self, lines=(), taxes=()):
|
||||||
|
self.lines = list(lines)
|
||||||
|
self.taxes = list(taxes)
|
||||||
|
|
||||||
|
|
||||||
class AccountDebitNoteTestCase(ModuleTestCase):
|
class AccountDebitNoteTestCase(ModuleTestCase):
|
||||||
'Test Account Debit Note module'
|
'Test Account Debit Note module'
|
||||||
module = 'account_debit_note'
|
module = 'account_debit_note'
|
||||||
@@ -49,6 +71,47 @@ class AccountDebitNoteLogicTestCase(unittest.TestCase):
|
|||||||
Invoice._debit_compute_factor(
|
Invoice._debit_compute_factor(
|
||||||
MockInvoice(), 'percent', Decimal('101'), None, 'total')
|
MockInvoice(), 'percent', Decimal('101'), None, 'total')
|
||||||
|
|
||||||
|
def test_fixed_amount_rejects_multiple_invoices(self):
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
Invoice.debit(
|
||||||
|
[MockInvoice(), MockInvoice()],
|
||||||
|
debit_mode='amount', debit_amount=Decimal('10'))
|
||||||
|
|
||||||
|
def test_debit_restores_negative_adjustment_line_sign(self):
|
||||||
|
# Core _credit() changed the original quantity 2 to -2 while keeping
|
||||||
|
# the original negative adjustment price.
|
||||||
|
line = MockLine('-2', '-15')
|
||||||
|
debit = MockDebitNote([line])
|
||||||
|
|
||||||
|
Invoice._debit_force_positive_sign(debit)
|
||||||
|
|
||||||
|
self.assertEqual(line.quantity, Decimal('2'))
|
||||||
|
self.assertEqual(line.unit_price, Decimal('-15'))
|
||||||
|
|
||||||
|
def test_debit_restores_manual_tax_sign_without_abs(self):
|
||||||
|
# Core _credit() negated the original base 100 and amount -18.
|
||||||
|
tax = MockTax('-100', '18')
|
||||||
|
debit = MockDebitNote(taxes=[tax])
|
||||||
|
|
||||||
|
Invoice._debit_force_positive_sign(debit)
|
||||||
|
|
||||||
|
self.assertEqual(tax.base, Decimal('100'))
|
||||||
|
self.assertEqual(tax.amount, Decimal('-18'))
|
||||||
|
|
||||||
|
def test_partial_debit_preserves_relative_line_signs(self):
|
||||||
|
positive = MockLine('2', '50')
|
||||||
|
adjustment = MockLine('1', '-20')
|
||||||
|
debit = MockDebitNote([positive, adjustment])
|
||||||
|
origin = MockInvoice()
|
||||||
|
origin.untaxed_amount = Decimal('80')
|
||||||
|
|
||||||
|
Invoice._debit_apply_factor(origin, debit, Decimal('0.5'))
|
||||||
|
|
||||||
|
self.assertEqual(positive.quantity, Decimal('2'))
|
||||||
|
self.assertEqual(positive.unit_price, Decimal('25.00000'))
|
||||||
|
self.assertEqual(adjustment.quantity, Decimal('1'))
|
||||||
|
self.assertEqual(adjustment.unit_price, Decimal('-10.00000'))
|
||||||
|
|
||||||
|
|
||||||
del ModuleTestCase
|
del ModuleTestCase
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user