Integrate fiscal cancellation reasons
This commit is contained in:
+101
-4
@@ -7,9 +7,9 @@ from sql.aggregate import Sum
|
|||||||
from sql.conditionals import Coalesce
|
from sql.conditionals import Coalesce
|
||||||
from sql.functions import Round
|
from sql.functions import Round
|
||||||
from trytond import backend
|
from trytond import backend
|
||||||
from trytond.model import fields
|
from trytond.model import ModelView, Workflow, fields
|
||||||
from trytond.pool import Pool, PoolMeta
|
from trytond.pool import Pool, PoolMeta
|
||||||
from trytond.pyson import Eval, If, And
|
from trytond.pyson import Eval, If, And, Bool
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from trytond.exceptions import UserError
|
from trytond.exceptions import UserError
|
||||||
from trytond.i18n import gettext
|
from trytond.i18n import gettext
|
||||||
@@ -293,6 +293,43 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
isr_withholding_type_string = isr_withholding_type.translated(
|
isr_withholding_type_string = isr_withholding_type.translated(
|
||||||
'isr_withholding_type')
|
'isr_withholding_type')
|
||||||
|
|
||||||
|
ncf_cancellation_type = fields.Selection(
|
||||||
|
_CANCELLATION_TYPE, 'NCF Cancellation Type',
|
||||||
|
states={
|
||||||
|
'required': (
|
||||||
|
(Eval('state') == 'cancelled')
|
||||||
|
& (Eval('type') == 'out')
|
||||||
|
& Bool(Eval('ncf_number'))
|
||||||
|
& ~Eval('context', {}).get('invoice_force_cancel', False)),
|
||||||
|
'readonly': Eval('state') == 'cancelled',
|
||||||
|
'invisible': Eval('type') == 'in',
|
||||||
|
},
|
||||||
|
depends=['state', 'type', 'ncf_number'])
|
||||||
|
|
||||||
|
ncf_cancellation_type_string = ncf_cancellation_type.translated(
|
||||||
|
'ncf_cancellation_type')
|
||||||
|
|
||||||
|
ncf_cancellation_reason = fields.Text(
|
||||||
|
'NCF Cancellation Reason',
|
||||||
|
states={
|
||||||
|
'required': (
|
||||||
|
(Eval('state') == 'cancelled')
|
||||||
|
& (Eval('type') == 'out')
|
||||||
|
& Bool(Eval('ncf_number'))
|
||||||
|
& ~Eval('context', {}).get('invoice_force_cancel', False)),
|
||||||
|
'readonly': Eval('state') == 'cancelled',
|
||||||
|
'invisible': Eval('type') == 'in',
|
||||||
|
},
|
||||||
|
depends=['state', 'type', 'ncf_number'])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __setup__(cls):
|
||||||
|
super().__setup__()
|
||||||
|
cls._check_modify_exclude.update({
|
||||||
|
'ncf_cancellation_type',
|
||||||
|
'ncf_cancellation_reason',
|
||||||
|
})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __register__(cls, module_name):
|
def __register__(cls, module_name):
|
||||||
"""
|
"""
|
||||||
@@ -308,6 +345,7 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
cls._ncf_prepare_legacy_invoice_ncf_type_column()
|
cls._ncf_prepare_legacy_invoice_ncf_type_column()
|
||||||
super().__register__(module_name)
|
super().__register__(module_name)
|
||||||
cls._ncf_migrate_invoice_ncf_type_from_legacy_column()
|
cls._ncf_migrate_invoice_ncf_type_from_legacy_column()
|
||||||
|
cls._ncf_migrate_legacy_cancellation_fields()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _ncf_column_exists(cls, cursor, table_name, column_name):
|
def _ncf_column_exists(cls, cursor, table_name, column_name):
|
||||||
@@ -447,6 +485,35 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
unmatched,
|
unmatched,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ncf_migrate_legacy_cancellation_fields(cls):
|
||||||
|
"""
|
||||||
|
Copy data from the former account_invoice_cancel_reason module.
|
||||||
|
|
||||||
|
The old module used generic cancellation_type/cancellation_reason
|
||||||
|
columns. ncf_manager stores those values as fiscal DGII cancellation
|
||||||
|
data so they can later feed the NCF annulment report.
|
||||||
|
"""
|
||||||
|
cursor = Transaction().connection.cursor()
|
||||||
|
table_name = 'account_invoice'
|
||||||
|
field_pairs = (
|
||||||
|
('cancellation_type', 'ncf_cancellation_type'),
|
||||||
|
('cancellation_reason', 'ncf_cancellation_reason'),
|
||||||
|
)
|
||||||
|
|
||||||
|
for old_column, new_column in field_pairs:
|
||||||
|
if not cls._ncf_column_exists(cursor, table_name, old_column):
|
||||||
|
continue
|
||||||
|
if not cls._ncf_column_exists(cursor, table_name, new_column):
|
||||||
|
continue
|
||||||
|
cursor.execute(f"""
|
||||||
|
UPDATE {table_name}
|
||||||
|
SET {new_column} = {old_column}
|
||||||
|
WHERE {old_column} IS NOT NULL
|
||||||
|
AND {old_column} <> ''
|
||||||
|
AND ({new_column} IS NULL OR {new_column} = '')
|
||||||
|
""")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_rec_name(cls, name, clause):
|
def search_rec_name(cls, name, clause):
|
||||||
domain = super().search_rec_name(name, clause)
|
domain = super().search_rec_name(name, clause)
|
||||||
@@ -1645,6 +1712,25 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
raise UserError(gettext(
|
raise UserError(gettext(
|
||||||
'ncf_manager.msg_incorrect_lenght_ncf_number_modified'))
|
'ncf_manager.msg_incorrect_lenght_ncf_number_modified'))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ncf_is_fiscal_cancellation_required(invoice):
|
||||||
|
return (
|
||||||
|
invoice.type == 'out'
|
||||||
|
and bool(getattr(invoice, 'ncf_number', None)))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ncf_check_cancellation_information(cls, invoice):
|
||||||
|
if not cls._ncf_is_fiscal_cancellation_required(invoice):
|
||||||
|
return
|
||||||
|
if not invoice.ncf_cancellation_type:
|
||||||
|
raise UserError(gettext(
|
||||||
|
'ncf_manager.msg_missing_ncf_cancellation_type',
|
||||||
|
invoice=invoice.rec_name))
|
||||||
|
if not invoice.ncf_cancellation_reason:
|
||||||
|
raise UserError(gettext(
|
||||||
|
'ncf_manager.msg_missing_ncf_cancellation_reason',
|
||||||
|
invoice=invoice.rec_name))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy(cls, invoices, default=None):
|
def copy(cls, invoices, default=None):
|
||||||
if default is None:
|
if default is None:
|
||||||
@@ -1658,6 +1744,8 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
default.setdefault('ncf_number_modified', None)
|
default.setdefault('ncf_number_modified', None)
|
||||||
default.setdefault('expiration_date', None)
|
default.setdefault('expiration_date', None)
|
||||||
default.setdefault('security_code', None)
|
default.setdefault('security_code', None)
|
||||||
|
default.setdefault('ncf_cancellation_type', None)
|
||||||
|
default.setdefault('ncf_cancellation_reason', None)
|
||||||
|
|
||||||
return super(Invoice, cls).copy(invoices, default=default)
|
return super(Invoice, cls).copy(invoices, default=default)
|
||||||
|
|
||||||
@@ -1674,11 +1762,20 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
('/form/notebook/page[@id="tax_information"]/separator[@id="expense"]', 'states', {
|
('/form/notebook/page[@id="tax_information"]/separator[@id="expense"]', 'states', {
|
||||||
'invisible': Eval('type') == 'out',
|
'invisible': Eval('type') == 'out',
|
||||||
}),
|
}),
|
||||||
('/form/notebook/page[@id="cancel_reason"]', 'states', {
|
('/form/notebook/page[@id="ncf_cancellation"]', 'states', {
|
||||||
'invisible': Eval('type') == 'in',
|
'invisible': (
|
||||||
|
(Eval('type') == 'in') | ~Bool(Eval('ncf_number'))),
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@ModelView.button
|
||||||
|
@Workflow.transition('cancelled')
|
||||||
|
def cancel(cls, invoices):
|
||||||
|
if not Transaction().context.get('invoice_force_cancel'):
|
||||||
|
for invoice in invoices:
|
||||||
|
cls._ncf_check_cancellation_information(invoice)
|
||||||
|
super().cancel(invoices)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _post(cls, invoices):
|
def _post(cls, invoices):
|
||||||
|
|||||||
@@ -139,6 +139,14 @@ msgctxt "field:account.invoice,ncf_type_name:"
|
|||||||
msgid "NCF Type"
|
msgid "NCF Type"
|
||||||
msgstr "Tipo NCF"
|
msgstr "Tipo NCF"
|
||||||
|
|
||||||
|
msgctxt "field:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "NCF Cancellation Type"
|
||||||
|
msgstr "Tipo de Anulación NCF"
|
||||||
|
|
||||||
|
msgctxt "field:account.invoice,ncf_cancellation_reason:"
|
||||||
|
msgid "NCF Cancellation Reason"
|
||||||
|
msgstr "Motivo de Anulación NCF"
|
||||||
|
|
||||||
msgctxt "selection:account.invoice,ncf_type:"
|
msgctxt "selection:account.invoice,ncf_type:"
|
||||||
msgid "Credit Note"
|
msgid "Credit Note"
|
||||||
msgstr "Nota de Crédito"
|
msgstr "Nota de Crédito"
|
||||||
@@ -175,6 +183,46 @@ msgctxt "selection:account.invoice,ncf_type:"
|
|||||||
msgid "Valid Invoice for Tax Credit"
|
msgid "Valid Invoice for Tax Credit"
|
||||||
msgstr "Factura Válida para Crédito Fiscal"
|
msgstr "Factura Válida para Crédito Fiscal"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "01 Deterioro de Factura Pre-Impresa"
|
||||||
|
msgstr "01 Deterioro de Factura Pre-Impresa"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "02 Errores de Impresión (Factura Pre-Impresa)"
|
||||||
|
msgstr "02 Errores de Impresión (Factura Pre-Impresa)"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "03 Impresión Defectuosa"
|
||||||
|
msgstr "03 Impresión Defectuosa"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "04 Corrección de la Información"
|
||||||
|
msgstr "04 Corrección de la Información"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "05 Cambio de Productos"
|
||||||
|
msgstr "05 Cambio de Productos"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "06 Devolución de Productos"
|
||||||
|
msgstr "06 Devolución de Productos"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "07 Omisión de Productos"
|
||||||
|
msgstr "07 Omisión de Productos"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "08 Errores en Secuencias de NCF"
|
||||||
|
msgstr "08 Errores en Secuencias de NCF"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "09 Por Cese de Operaciones"
|
||||||
|
msgstr "09 Por Cese de Operaciones"
|
||||||
|
|
||||||
|
msgctxt "selection:account.invoice,ncf_cancellation_type:"
|
||||||
|
msgid "10 Pérdida O Hurto De Talonario(S)"
|
||||||
|
msgstr "10 Pérdida O Hurto De Talonario(S)"
|
||||||
|
|
||||||
msgctxt "view:account.fiscalyear:"
|
msgctxt "view:account.fiscalyear:"
|
||||||
msgid "NCF Sequences"
|
msgid "NCF Sequences"
|
||||||
msgstr "Secuencias de NCF"
|
msgstr "Secuencias de NCF"
|
||||||
@@ -183,6 +231,10 @@ msgctxt "view:account.invoice:"
|
|||||||
msgid "Tax Information"
|
msgid "Tax Information"
|
||||||
msgstr "Información Fiscal"
|
msgstr "Información Fiscal"
|
||||||
|
|
||||||
|
msgctxt "view:account.invoice:"
|
||||||
|
msgid "NCF Cancellation"
|
||||||
|
msgstr "Anulación NCF"
|
||||||
|
|
||||||
msgctxt "view:account.invoice:"
|
msgctxt "view:account.invoice:"
|
||||||
msgid "Revenue"
|
msgid "Revenue"
|
||||||
msgstr "Ingreso"
|
msgstr "Ingreso"
|
||||||
@@ -710,6 +762,18 @@ msgctxt "model:ir.message,text:msg_missing_credit_note_sequence"
|
|||||||
msgid "No active credit note sequence available for type \"%(ncf_type)s\" in company \"%(company)s\"."
|
msgid "No active credit note sequence available for type \"%(ncf_type)s\" in company \"%(company)s\"."
|
||||||
msgstr "No hay secuencia de nota de crédito activa para el tipo \"%(ncf_type)s\" en la empresa \"%(company)s\"."
|
msgstr "No hay secuencia de nota de crédito activa para el tipo \"%(ncf_type)s\" en la empresa \"%(company)s\"."
|
||||||
|
|
||||||
|
msgctxt "model:ir.message,text:msg_missing_debit_note_sequence"
|
||||||
|
msgid "No active debit note sequence available for type \"%(ncf_type)s\" in company \"%(company)s\"."
|
||||||
|
msgstr "No hay secuencia de nota de débito activa para el tipo \"%(ncf_type)s\" en la empresa \"%(company)s\"."
|
||||||
|
|
||||||
|
msgctxt "model:ir.message,text:msg_missing_ncf_cancellation_type"
|
||||||
|
msgid "You must select a DGII cancellation type before cancelling fiscal invoice \"%(invoice)s\"."
|
||||||
|
msgstr "Debe seleccionar un tipo de anulación DGII antes de cancelar la factura fiscal \"%(invoice)s\"."
|
||||||
|
|
||||||
|
msgctxt "model:ir.message,text:msg_missing_ncf_cancellation_reason"
|
||||||
|
msgid "You must enter a DGII cancellation reason before cancelling fiscal invoice \"%(invoice)s\"."
|
||||||
|
msgstr "Debe indicar un motivo de anulación DGII antes de cancelar la factura fiscal \"%(invoice)s\"."
|
||||||
|
|
||||||
msgctxt "model:ir.message,text:msg_inactive_ncf_sequence"
|
msgctxt "model:ir.message,text:msg_inactive_ncf_sequence"
|
||||||
msgid "The NCF sequence selected for invoice \"%(invoice)s\" is inactive."
|
msgid "The NCF sequence selected for invoice \"%(invoice)s\" is inactive."
|
||||||
msgstr "La secuencia NCF seleccionada para la factura \"%(invoice)s\" está inactiva."
|
msgstr "La secuencia NCF seleccionada para la factura \"%(invoice)s\" está inactiva."
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<record model="ir.message" id="msg_missing_debit_note_sequence">
|
<record model="ir.message" id="msg_missing_debit_note_sequence">
|
||||||
<field name="text">No active debit note sequence available for type "%(ncf_type)s" in company "%(company)s".</field>
|
<field name="text">No active debit note sequence available for type "%(ncf_type)s" in company "%(company)s".</field>
|
||||||
</record>
|
</record>
|
||||||
|
<record model="ir.message" id="msg_missing_ncf_cancellation_type">
|
||||||
|
<field name="text">You must select a DGII cancellation type before cancelling fiscal invoice "%(invoice)s".</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.message" id="msg_missing_ncf_cancellation_reason">
|
||||||
|
<field name="text">You must enter a DGII cancellation reason before cancelling fiscal invoice "%(invoice)s".</field>
|
||||||
|
</record>
|
||||||
<record model="ir.message" id="msg_inactive_ncf_sequence">
|
<record model="ir.message" id="msg_inactive_ncf_sequence">
|
||||||
<field name="text">The NCF sequence selected for invoice "%(invoice)s" is inactive.</field>
|
<field name="text">The NCF sequence selected for invoice "%(invoice)s" is inactive.</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from decimal import Decimal
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from trytond.exceptions import UserError
|
||||||
from trytond.modules.ncf_manager.sequence import Sequence
|
from trytond.modules.ncf_manager.sequence import Sequence
|
||||||
from trytond.modules.ncf_manager.invoice import Invoice
|
from trytond.modules.ncf_manager.invoice import Invoice
|
||||||
from trytond.pool import Pool
|
from trytond.pool import Pool
|
||||||
@@ -236,6 +237,36 @@ class NcfManagerUnitTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(Sequence.on_change_with_approved_qty(sequence), 10)
|
self.assertEqual(Sequence.on_change_with_approved_qty(sequence), 10)
|
||||||
self.assertEqual(Sequence.on_change_with_remaining_qty(sequence), 7)
|
self.assertEqual(Sequence.on_change_with_remaining_qty(sequence), 7)
|
||||||
|
|
||||||
|
def test_fiscal_cancellation_requires_ncf_number(self):
|
||||||
|
fiscal_invoice = SimpleNamespace(type='out', ncf_number='B0100000001')
|
||||||
|
non_fiscal_invoice = SimpleNamespace(type='out', ncf_number=None)
|
||||||
|
supplier_invoice = SimpleNamespace(type='in', ncf_number='B1100000001')
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
Invoice._ncf_is_fiscal_cancellation_required(fiscal_invoice))
|
||||||
|
self.assertFalse(
|
||||||
|
Invoice._ncf_is_fiscal_cancellation_required(non_fiscal_invoice))
|
||||||
|
self.assertFalse(
|
||||||
|
Invoice._ncf_is_fiscal_cancellation_required(supplier_invoice))
|
||||||
|
|
||||||
|
def test_fiscal_cancellation_information_is_required(self):
|
||||||
|
invoice = SimpleNamespace(
|
||||||
|
type='out',
|
||||||
|
rec_name='INV/1',
|
||||||
|
ncf_number='B0100000001',
|
||||||
|
ncf_cancellation_type=None,
|
||||||
|
ncf_cancellation_reason=None)
|
||||||
|
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
Invoice._ncf_check_cancellation_information(invoice)
|
||||||
|
|
||||||
|
invoice.ncf_cancellation_type = '04'
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
Invoice._ncf_check_cancellation_information(invoice)
|
||||||
|
|
||||||
|
invoice.ncf_cancellation_reason = 'Corrección de la información'
|
||||||
|
Invoice._ncf_check_cancellation_information(invoice)
|
||||||
|
|
||||||
def test_debit_note_code_follows_origin_ncf_type(self):
|
def test_debit_note_code_follows_origin_ncf_type(self):
|
||||||
original_enabled = Invoice._ncf_electronic_invoicing_enabled
|
original_enabled = Invoice._ncf_electronic_invoicing_enabled
|
||||||
Invoice._ncf_electronic_invoicing_enabled = classmethod(
|
Invoice._ncf_electronic_invoicing_enabled = classmethod(
|
||||||
|
|||||||
@@ -94,5 +94,12 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<newline/>
|
<newline/>
|
||||||
<newline/>
|
<newline/>
|
||||||
</page>
|
</page>
|
||||||
|
<page id="ncf_cancellation" string="NCF Cancellation">
|
||||||
|
<label name="ncf_cancellation_type"/>
|
||||||
|
<field name="ncf_cancellation_type" widget="selection"/>
|
||||||
|
<newline/>
|
||||||
|
<separator name="ncf_cancellation_reason" colspan="4"/>
|
||||||
|
<field name="ncf_cancellation_reason" colspan="4"/>
|
||||||
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
Reference in New Issue
Block a user