Homologate shared fiscal behavior with Tryton 7.0
Publicar paquete PyPI / publish (push) Successful in 1m37s
Publicar paquete PyPI / publish (push) Successful in 1m37s
This commit is contained in:
@@ -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
|
It does not sign or transmit e-CF documents; transmission remains the explicit
|
||||||
responsibility of an electronic invoicing connector.
|
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
|
Authorization workflow
|
||||||
**********************
|
**********************
|
||||||
|
|
||||||
|
|||||||
+30
-49
@@ -277,7 +277,7 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
'on_change_with_payment_method_barter')
|
'on_change_with_payment_method_barter')
|
||||||
|
|
||||||
payment_method_credit_note = fields.Function(Monetary(
|
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')
|
'on_change_with_payment_method_credit_note')
|
||||||
|
|
||||||
payment_method_mixed_payment = fields.Function(Monetary(
|
payment_method_mixed_payment = fields.Function(Monetary(
|
||||||
@@ -1030,15 +1030,30 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_amount_invoiced_services(cls, name, clause):
|
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()
|
pool = Pool()
|
||||||
Rule = pool.get('ir.rule')
|
Rule = pool.get('ir.rule')
|
||||||
Line = pool.get('account.invoice.line')
|
Line = pool.get('account.invoice.line')
|
||||||
Invoice = pool.get('account.invoice')
|
Invoice = pool.get('account.invoice')
|
||||||
Currency = pool.get('currency.currency')
|
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__()
|
line = Line.__table__()
|
||||||
invoice = Invoice.__table__()
|
invoice = Invoice.__table__()
|
||||||
currency = Currency.__table__()
|
currency = Currency.__table__()
|
||||||
|
product = Product.__table__()
|
||||||
|
template = Template.__table__()
|
||||||
|
|
||||||
_, operator, value = clause
|
_, operator, value = clause
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -1049,53 +1064,19 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
if _backend_name() == 'sqlite':
|
if _backend_name() == 'sqlite':
|
||||||
value = float(value)
|
value = float(value)
|
||||||
|
|
||||||
query = line.join(invoice,
|
query = (line.join(invoice, condition=(invoice.id == line.invoice))
|
||||||
condition=(invoice.id == line.invoice)
|
.join(currency, condition=(currency.id == invoice.currency))
|
||||||
).join(currency,
|
.join(product, condition=(product.id == line.product))
|
||||||
condition=(currency.id == invoice.currency)
|
.join(template, condition=(template.id == product.template))
|
||||||
).select(line.invoice,
|
.select(
|
||||||
where=line.invoice.in_(
|
line.invoice,
|
||||||
invoice_query),
|
where=(line.invoice.in_(invoice_query)
|
||||||
group_by=line.invoice,
|
& (line.type == 'line')
|
||||||
having=Operator(Coalesce(Sum(
|
& (template.type == product_type)),
|
||||||
Round((line.quantity * line.unit_price).cast(
|
group_by=line.invoice,
|
||||||
type_name),
|
having=Operator(Coalesce(Sum(
|
||||||
currency.digits)), 0).cast(type_name),
|
Round((line.quantity * line.unit_price).cast(type_name),
|
||||||
value))
|
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))
|
|
||||||
return [('id', 'in', query)]
|
return [('id', 'in', query)]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ class Party(metaclass=PoolMeta):
|
|||||||
'default_supplier_ncf_type', **pattern)
|
'default_supplier_ncf_type', **pattern)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PartyNcfType(ModelSQL, ValueMixin):
|
class PartyNcfType(ModelSQL, ValueMixin):
|
||||||
"Party Ncf Type"
|
"Party Ncf Type"
|
||||||
__name__ = 'party.party.ncf_type'
|
__name__ = 'party.party.ncf_type'
|
||||||
|
|||||||
+9
-2
@@ -1,5 +1,7 @@
|
|||||||
# This file is part of ncf_manager module for Tryton. The COPYRIGHT file at the top level of
|
# 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.
|
# this repository contains the full copyright notices and license terms.
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
from trytond.exceptions import UserError
|
from trytond.exceptions import UserError
|
||||||
from trytond.model import fields
|
from trytond.model import fields
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
@@ -33,6 +35,11 @@ _NCF_TYPE_MAP = {
|
|||||||
NCF_PREFIXES = tuple(prefix for prefix, _padding in _NCF_TYPE_MAP.values())
|
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):
|
class Sequence(metaclass=PoolMeta):
|
||||||
__name__ = 'ir.sequence'
|
__name__ = 'ir.sequence'
|
||||||
|
|
||||||
@@ -90,14 +97,14 @@ class Sequence(metaclass=PoolMeta):
|
|||||||
def on_change_with_prefix(self, name=None):
|
def on_change_with_prefix(self, name=None):
|
||||||
if not self.sequence_type or not self.sequence_type.name:
|
if not self.sequence_type or not self.sequence_type.name:
|
||||||
return None
|
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
|
return entry[0] if entry else None
|
||||||
|
|
||||||
@fields.depends('sequence_type')
|
@fields.depends('sequence_type')
|
||||||
def on_change_with_padding(self, name=None):
|
def on_change_with_padding(self, name=None):
|
||||||
if not self.sequence_type or not self.sequence_type.name:
|
if not self.sequence_type or not self.sequence_type.name:
|
||||||
return 0
|
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
|
return entry[1] if entry else 0
|
||||||
|
|
||||||
@fields.depends('from_number', 'until_number')
|
@fields.depends('from_number', 'until_number')
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
import inspect
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from trytond.modules.ncf_manager.ncf_manager import (
|
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.sequence import Sequence
|
||||||
from trytond.modules.ncf_manager.importer import (
|
from trytond.modules.ncf_manager.importer import (
|
||||||
normalize_dgii_row, parse_dgii_authorizations)
|
normalize_dgii_row, parse_dgii_authorizations)
|
||||||
|
from trytond.modules.ncf_manager.invoice import Invoice
|
||||||
|
|
||||||
|
|
||||||
class NcfManagerUnitTestCase(unittest.TestCase):
|
class NcfManagerUnitTestCase(unittest.TestCase):
|
||||||
@@ -59,6 +61,18 @@ class NcfManagerUnitTestCase(unittest.TestCase):
|
|||||||
self.assertIsNone(Sequence.on_change_with_prefix(sequence))
|
self.assertIsNone(Sequence.on_change_with_prefix(sequence))
|
||||||
self.assertEqual(Sequence.on_change_with_padding(sequence), 0)
|
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):
|
def test_csv_parser_preserves_unquoted_detail(self):
|
||||||
source_format, rows = parse_dgii_authorizations(
|
source_format, rows = parse_dgii_authorizations(
|
||||||
f'{self.HEADERS}\r\n{self.ROW}\r\n'.encode())
|
f'{self.HEADERS}\r\n{self.ROW}\r\n'.encode())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = {py37,py38,py39,py310}-{sqlite,postgresql}
|
envlist = {py311,py312,py313,py314}-{sqlite,postgresql}
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
extras = test
|
extras = test
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tryton]
|
[tryton]
|
||||||
version=8.0.3
|
version=8.0.4
|
||||||
depends:
|
depends:
|
||||||
account_do
|
account_do
|
||||||
account_invoice_do
|
account_invoice_do
|
||||||
|
|||||||
Reference in New Issue
Block a user