This commit is contained in:
@@ -3,16 +3,24 @@ name: Publish Package
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- "8.0"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- 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
|
||||
run: |
|
||||
@@ -21,7 +29,9 @@ jobs:
|
||||
import re
|
||||
|
||||
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 '')
|
||||
publish = bool(re.fullmatch(r'\d+\.\d+', branch))
|
||||
@@ -75,7 +85,7 @@ jobs:
|
||||
|
||||
OWNER = 'tryton-do'
|
||||
PACKAGE_TYPE = 'pypi'
|
||||
API_ROOT = 'https://gitea.joseagrc.com/api/v1'
|
||||
API_ROOT = 'https://code.unpaismejor.org.do/api/v1'
|
||||
|
||||
def normalize(name):
|
||||
return re.sub(r'[-_.]+', '-', name).lower()
|
||||
@@ -187,5 +197,5 @@ jobs:
|
||||
TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
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/*
|
||||
|
||||
@@ -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::
|
||||
|
||||
pip install trytond_account_debit_note
|
||||
pip install unpaismejor_account_debit_note
|
||||
trytond-admin -d <database> -u account_debit_note
|
||||
|
||||
Publishing
|
||||
|
||||
+32
-26
@@ -30,6 +30,7 @@ _ONE_HUNDRED = Decimal('100')
|
||||
|
||||
|
||||
def _decimal(value):
|
||||
"""Return *value* as Decimal without binary floating-point artifacts."""
|
||||
if value is None:
|
||||
return _ZERO
|
||||
return Decimal(str(value))
|
||||
@@ -107,7 +108,8 @@ class Invoice(metaclass=PoolMeta):
|
||||
|
||||
Tryton core exposes Invoice._credit(), which copies the invoice and
|
||||
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)
|
||||
self.__class__._debit_force_positive_sign(debit)
|
||||
@@ -115,48 +117,49 @@ class Invoice(metaclass=PoolMeta):
|
||||
|
||||
@classmethod
|
||||
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 = []
|
||||
for line in debit_note.lines:
|
||||
if line.type != 'line':
|
||||
continue
|
||||
|
||||
quantity = _decimal(line.quantity)
|
||||
unit_price = _decimal(line.unit_price)
|
||||
new_quantity = abs(quantity)
|
||||
new_unit_price = abs(unit_price)
|
||||
new_quantity = -quantity
|
||||
|
||||
if quantity != new_quantity or unit_price != new_unit_price:
|
||||
if quantity != new_quantity:
|
||||
line.quantity = new_quantity
|
||||
line.unit_price = new_unit_price
|
||||
touched.append(line)
|
||||
|
||||
if touched and all(getattr(line, 'id', None) for line in touched):
|
||||
Line = Pool().get('account.invoice.line')
|
||||
Line.save(touched)
|
||||
|
||||
for tax in getattr(debit_note, 'taxes', None) or []:
|
||||
tax.base = abs(_decimal(tax.base))
|
||||
tax.amount = abs(_decimal(tax.amount))
|
||||
tax.base = -_decimal(tax.base)
|
||||
tax.amount = -_decimal(tax.amount)
|
||||
|
||||
@classmethod
|
||||
def _debit_apply_factor(cls, origin, debit_note, factor):
|
||||
if factor == _ONE:
|
||||
return
|
||||
|
||||
Line = Pool().get('account.invoice.line')
|
||||
lines = [line for line in debit_note.lines if line.type == 'line']
|
||||
touched = []
|
||||
|
||||
for line in lines:
|
||||
quantity = _decimal(line.quantity)
|
||||
unit_price = _decimal(line.unit_price)
|
||||
scale = _unit_price_scale(line)
|
||||
|
||||
line.quantity = abs(quantity)
|
||||
line.unit_price = _round_decimal(abs(unit_price) * factor, scale)
|
||||
line.unit_price = _round_decimal(unit_price * factor, scale)
|
||||
touched.append(line)
|
||||
|
||||
if touched and all(getattr(line, 'id', None) for line in touched):
|
||||
Line = Pool().get('account.invoice.line')
|
||||
Line.save(touched)
|
||||
|
||||
cls._debit_adjust_last_line(origin, touched, factor)
|
||||
@@ -166,11 +169,11 @@ class Invoice(metaclass=PoolMeta):
|
||||
if not lines:
|
||||
return
|
||||
|
||||
Line = Pool().get('account.invoice.line')
|
||||
target_untaxed_abs = abs(_decimal(origin.untaxed_amount)) * factor
|
||||
current_untaxed_abs = sum(
|
||||
abs(_decimal(cls._debit_line_amount(line))) for line in lines)
|
||||
difference = target_untaxed_abs - current_untaxed_abs
|
||||
target_untaxed = _decimal(origin.untaxed_amount) * factor
|
||||
current_untaxed = sum(
|
||||
(_decimal(cls._debit_line_amount(line)) for line in lines),
|
||||
_ZERO)
|
||||
difference = target_untaxed - current_untaxed
|
||||
if abs(difference) < Decimal('0.0001'):
|
||||
return
|
||||
|
||||
@@ -179,19 +182,18 @@ class Invoice(metaclass=PoolMeta):
|
||||
if quantity == _ZERO:
|
||||
return
|
||||
|
||||
unit_price_abs = abs(_decimal(last.unit_price))
|
||||
unit_price_delta_abs = difference / abs(quantity)
|
||||
new_unit_price_abs = unit_price_abs + unit_price_delta_abs
|
||||
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))
|
||||
new_unit_price = _decimal(last.unit_price) + difference / quantity
|
||||
last.unit_price = _round_decimal(
|
||||
new_unit_price, _unit_price_scale(last))
|
||||
if getattr(last, 'id', None):
|
||||
Line = Pool().get('account.invoice.line')
|
||||
Line.save([last])
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
@@ -223,6 +225,10 @@ class Invoice(metaclass=PoolMeta):
|
||||
debit_amount = values.pop('debit_amount', None)
|
||||
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 = [
|
||||
cls._debit_compute_factor(
|
||||
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."
|
||||
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"
|
||||
msgid "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."
|
||||
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"
|
||||
msgid "debit_start_form"
|
||||
msgstr "debit_start_form"
|
||||
|
||||
@@ -19,5 +19,8 @@
|
||||
<record model="ir.message" id="msg_invalid_debit_mode">
|
||||
<field name="text">Invalid debit note mode.</field>
|
||||
</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>
|
||||
</tryton>
|
||||
|
||||
+5
-4
@@ -3,11 +3,11 @@ requires = ['hatchling >= 1', 'hatch-tryton']
|
||||
build-backend = 'hatchling.build'
|
||||
|
||||
[project]
|
||||
name = 'trytond_account_debit_note'
|
||||
name = 'unpaismejor_account_debit_note'
|
||||
dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme']
|
||||
requires-python = '>=3.10'
|
||||
maintainers = [
|
||||
{name = "Solutema"},
|
||||
{name = "Fundación Un País Mejor"},
|
||||
]
|
||||
description = "Debit notes for Tryton invoices"
|
||||
license = 'GPL-3.0-or-later'
|
||||
@@ -26,11 +26,12 @@ classifiers = [
|
||||
account_debit_note = 'trytond.modules.account_debit_note'
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://www.tryton.org/"
|
||||
repository = "https://gitea.joseagrc.com/tryton-do/account_debit_note"
|
||||
homepage = "https://www.unpaismejor.org.do"
|
||||
repository = "https://code.unpaismejor.org.do/tryton-do/account_debit_note"
|
||||
|
||||
[tool.hatch.build]
|
||||
include = [
|
||||
'CHANGELOG',
|
||||
'COPYRIGHT',
|
||||
'LICENSE',
|
||||
'README.rst',
|
||||
|
||||
@@ -11,6 +11,28 @@ class MockInvoice:
|
||||
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):
|
||||
'Test Account Debit Note module'
|
||||
module = 'account_debit_note'
|
||||
@@ -49,6 +71,47 @@ class AccountDebitNoteLogicTestCase(unittest.TestCase):
|
||||
Invoice._debit_compute_factor(
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user