Homologate shared fiscal behavior with Tryton 7.0
Publicar paquete PyPI / publish (push) Successful in 1m37s

This commit is contained in:
José Arturo García
2026-08-30 18:28:13 -04:00
parent c83cf048ea
commit 3b5d900221
7 changed files with 62 additions and 55 deletions
+7
View File
@@ -29,6 +29,13 @@ The module assigns fiscal numbers transactionally from ``ir.sequence.strict``.
It does not sign or transmit e-CF documents; transmission remains the explicit
responsibility of an electronic invoicing connector.
Version compatibility
*********************
The 7.0 and 8.0 series share the same fiscal models, workflows, importer,
validations and tests. Only the module registration, packaging metadata and
report XML required by each Tryton series remain version-specific.
Authorization workflow
**********************
+30 -49
View File
@@ -277,7 +277,7 @@ class Invoice(metaclass=PoolMeta):
'on_change_with_payment_method_barter')
payment_method_credit_note = fields.Function(Monetary(
"Credit Note'", currency='currency', digits='currency'),
"Credit Note", currency='currency', digits='currency'),
'on_change_with_payment_method_credit_note')
payment_method_mixed_payment = fields.Function(Monetary(
@@ -1030,15 +1030,30 @@ class Invoice(metaclass=PoolMeta):
@classmethod
def search_amount_invoiced_services(cls, name, clause):
return cls._search_amount_invoiced_by_product_type(
name, clause, 'service')
@classmethod
def search_amount_invoiced_goods(cls, name, clause):
return cls._search_amount_invoiced_by_product_type(
name, clause, 'goods')
@classmethod
def _search_amount_invoiced_by_product_type(
cls, name, clause, product_type):
pool = Pool()
Rule = pool.get('ir.rule')
Line = pool.get('account.invoice.line')
Invoice = pool.get('account.invoice')
Currency = pool.get('currency.currency')
type_name = cls.amount_invoiced_services._field.sql_type().base
Product = pool.get('product.product')
Template = pool.get('product.template')
type_name = getattr(cls, name)._field.sql_type().base
line = Line.__table__()
invoice = Invoice.__table__()
currency = Currency.__table__()
product = Product.__table__()
template = Template.__table__()
_, operator, value = clause
if value is None:
@@ -1049,53 +1064,19 @@ class Invoice(metaclass=PoolMeta):
if _backend_name() == 'sqlite':
value = float(value)
query = line.join(invoice,
condition=(invoice.id == line.invoice)
).join(currency,
condition=(currency.id == invoice.currency)
).select(line.invoice,
where=line.invoice.in_(
invoice_query),
group_by=line.invoice,
having=Operator(Coalesce(Sum(
Round((line.quantity * line.unit_price).cast(
type_name),
currency.digits)), 0).cast(type_name),
value))
return [('id', 'in', query)]
@classmethod
def search_amount_invoiced_goods(cls, name, clause):
pool = Pool()
Rule = pool.get('ir.rule')
Line = pool.get('account.invoice.line')
Invoice = pool.get('account.invoice')
Currency = pool.get('currency.currency')
type_name = cls.amount_invoiced_goods._field.sql_type().base
line = Line.__table__()
invoice = Invoice.__table__()
currency = Currency.__table__()
_, operator, value = clause
if value is None:
return []
invoice_query = Rule.query_get('account.invoice')
Operator = fields.SQL_OPERATORS[operator]
if _backend_name() == 'sqlite':
value = float(value)
query = line.join(invoice,
condition=(invoice.id == line.invoice)
).join(currency,
condition=(currency.id == invoice.currency)
).select(line.invoice,
where=line.invoice.in_(invoice_query),
group_by=line.invoice,
having=Operator(Coalesce(Sum(
Round((line.quantity * line.unit_price).cast(
type_name),
currency.digits)), 0).cast(type_name),
value))
query = (line.join(invoice, condition=(invoice.id == line.invoice))
.join(currency, condition=(currency.id == invoice.currency))
.join(product, condition=(product.id == line.product))
.join(template, condition=(template.id == product.template))
.select(
line.invoice,
where=(line.invoice.in_(invoice_query)
& (line.type == 'line')
& (template.type == product_type)),
group_by=line.invoice,
having=Operator(Coalesce(Sum(
Round((line.quantity * line.unit_price).cast(type_name),
currency.digits)), 0).cast(type_name), value)))
return [('id', 'in', query)]
@classmethod
-2
View File
@@ -69,8 +69,6 @@ class Party(metaclass=PoolMeta):
'default_supplier_ncf_type', **pattern)
class PartyNcfType(ModelSQL, ValueMixin):
"Party Ncf Type"
__name__ = 'party.party.ncf_type'
+9 -2
View File
@@ -1,5 +1,7 @@
# This file is part of ncf_manager module for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import unicodedata
from trytond.exceptions import UserError
from trytond.model import fields
from trytond.pyson import Eval
@@ -33,6 +35,11 @@ _NCF_TYPE_MAP = {
NCF_PREFIXES = tuple(prefix for prefix, _padding in _NCF_TYPE_MAP.values())
def _sequence_type_entry(sequence_type):
name = unicodedata.normalize('NFC', sequence_type.name or '')
return _NCF_TYPE_MAP.get(name)
class Sequence(metaclass=PoolMeta):
__name__ = 'ir.sequence'
@@ -90,14 +97,14 @@ class Sequence(metaclass=PoolMeta):
def on_change_with_prefix(self, name=None):
if not self.sequence_type or not self.sequence_type.name:
return None
entry = _NCF_TYPE_MAP.get(self.sequence_type.name)
entry = _sequence_type_entry(self.sequence_type)
return entry[0] if entry else None
@fields.depends('sequence_type')
def on_change_with_padding(self, name=None):
if not self.sequence_type or not self.sequence_type.name:
return 0
entry = _NCF_TYPE_MAP.get(self.sequence_type.name)
entry = _sequence_type_entry(self.sequence_type)
return entry[1] if entry else 0
@fields.depends('from_number', 'until_number')
+14
View File
@@ -1,4 +1,5 @@
from types import SimpleNamespace
import inspect
import unittest
from trytond.modules.ncf_manager.ncf_manager import (
@@ -6,6 +7,7 @@ from trytond.modules.ncf_manager.ncf_manager import (
from trytond.modules.ncf_manager.sequence import Sequence
from trytond.modules.ncf_manager.importer import (
normalize_dgii_row, parse_dgii_authorizations)
from trytond.modules.ncf_manager.invoice import Invoice
class NcfManagerUnitTestCase(unittest.TestCase):
@@ -59,6 +61,18 @@ class NcfManagerUnitTestCase(unittest.TestCase):
self.assertIsNone(Sequence.on_change_with_prefix(sequence))
self.assertEqual(Sequence.on_change_with_padding(sequence), 0)
def test_decomposed_electronic_sequence_name_is_supported(self):
sequence = SimpleNamespace()
sequence.sequence_type = SimpleNamespace(name='Compras Electro\u0301nico')
self.assertEqual(Sequence.on_change_with_prefix(sequence), 'E41')
self.assertEqual(Sequence.on_change_with_padding(sequence), 10)
def test_amount_searcher_filters_the_requested_product_type(self):
source = inspect.getsource(
Invoice._search_amount_invoiced_by_product_type)
self.assertIn('template.type == product_type', source)
self.assertIn("line.type == 'line'", source)
def test_csv_parser_preserves_unquoted_detail(self):
source_format, rows = parse_dgii_authorizations(
f'{self.HEADERS}\r\n{self.ROW}\r\n'.encode())
+1 -1
View File
@@ -1,5 +1,5 @@
[tox]
envlist = {py37,py38,py39,py310}-{sqlite,postgresql}
envlist = {py311,py312,py313,py314}-{sqlite,postgresql}
[testenv]
extras = test
+1 -1
View File
@@ -1,5 +1,5 @@
[tryton]
version=8.0.3
version=8.0.4
depends:
account_do
account_invoice_do