This commit is contained in:
+14
@@ -28,3 +28,17 @@ Configure this repository secret:
|
||||
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.
|
||||
|
||||
Authorization workflow
|
||||
**********************
|
||||
|
||||
NCF authorizations follow the protected workflow ``Draft → Active →
|
||||
Finished``. Create the authorization and its fiscal lines in draft, then use
|
||||
the **Activate** button. A line can select an existing strict sequence or ask
|
||||
the module to create one on activation from its authorized range. Activation
|
||||
checks company, fiscal type, prefix, padding, validity, availability and range
|
||||
overlap. The last issued number automatically finishes the line. Finishing
|
||||
an authorization closes every remaining active line without reusing numbers.
|
||||
|
||||
Electronic fiscal numbers are only reserved by this module. Activating an
|
||||
E-series authorization never signs or transmits an e-CF to DGII.
|
||||
|
||||
+11
-1
@@ -1628,7 +1628,17 @@ class Invoice(metaclass=PoolMeta):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_expired'))
|
||||
|
||||
return strict_sequence.get(), strict_sequence.id
|
||||
number = strict_sequence.get()
|
||||
StrictSequence = Pool().get('ir.sequence.strict')
|
||||
ManagerSequence = Pool().get('ncf.manager.sequence')
|
||||
strict_sequence = StrictSequence(strict_sequence.id)
|
||||
if (strict_sequence.until_number is not None
|
||||
and strict_sequence.number_next is not None
|
||||
and strict_sequence.number_next
|
||||
> strict_sequence.until_number
|
||||
and ncf_sequence.state == 'active'):
|
||||
ManagerSequence.complete([ncf_sequence])
|
||||
return number, strict_sequence.id
|
||||
|
||||
|
||||
def check_credit_note(self):
|
||||
|
||||
@@ -513,3 +513,95 @@ msgstr "Borrador"
|
||||
msgctxt "selection:ncf.manager.sequence,state:finished"
|
||||
msgid "Finished"
|
||||
msgstr "Finalizado"
|
||||
|
||||
msgctxt "field:ncf.manager,activated_by:"
|
||||
msgid "Activated By"
|
||||
msgstr "Activada por"
|
||||
|
||||
msgctxt "field:ncf.manager,activated_at:"
|
||||
msgid "Activated At"
|
||||
msgstr "Fecha de activación"
|
||||
|
||||
msgctxt "field:ncf.manager,finished_by:"
|
||||
msgid "Finished By"
|
||||
msgstr "Finalizada por"
|
||||
|
||||
msgctxt "field:ncf.manager,finished_at:"
|
||||
msgid "Finished At"
|
||||
msgstr "Fecha de finalización"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,sequence_mode:"
|
||||
msgid "Sequence Configuration"
|
||||
msgstr "Configuración de secuencia"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,ncf_sequences:"
|
||||
msgid "Fiscal Sequence"
|
||||
msgstr "Secuencia fiscal"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,expected_prefix:"
|
||||
msgid "Expected Prefix"
|
||||
msgstr "Prefijo esperado"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,expected_padding:"
|
||||
msgid "Expected Padding"
|
||||
msgstr "Longitud del consecutivo"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,from_number:"
|
||||
msgid "Authorized From"
|
||||
msgstr "Desde autorizado"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,until_number:"
|
||||
msgid "Authorized Until"
|
||||
msgstr "Hasta autorizado"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,number_next:"
|
||||
msgid "First Number to Issue"
|
||||
msgstr "Primer número por emitir"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,expiration_date:"
|
||||
msgid "Expiration Date"
|
||||
msgstr "Fecha de vencimiento"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,availability:"
|
||||
msgid "Availability"
|
||||
msgstr "Disponibilidad"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,sequence_mode:existing"
|
||||
msgid "Select Existing Sequence"
|
||||
msgstr "Seleccionar secuencia existente"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,sequence_mode:create"
|
||||
msgid "Create Sequence on Activation"
|
||||
msgstr "Crear secuencia al activar"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,availability:available"
|
||||
msgid "Available"
|
||||
msgstr "Disponible"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,availability:low"
|
||||
msgid "Running Low"
|
||||
msgstr "Próxima a agotarse"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,availability:expired"
|
||||
msgid "Expired"
|
||||
msgstr "Vencida"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,availability:exhausted"
|
||||
msgid "Exhausted"
|
||||
msgstr "Agotada"
|
||||
|
||||
msgctxt "model:ir.model.button,string:ncf_manager_activate_button"
|
||||
msgid "Activate"
|
||||
msgstr "Activar"
|
||||
|
||||
msgctxt "model:ir.model.button,string:ncf_manager_finish_button"
|
||||
msgid "Finish"
|
||||
msgstr "Finalizar"
|
||||
|
||||
msgctxt "model:ir.model.button,string:ncf_sequence_finish_button"
|
||||
msgid "Finish Sequence"
|
||||
msgstr "Finalizar secuencia"
|
||||
|
||||
msgctxt "view:ncf.manager:"
|
||||
msgid "Authorized Fiscal Sequences"
|
||||
msgstr "Secuencias fiscales autorizadas"
|
||||
|
||||
+100
-1
@@ -1,6 +1,13 @@
|
||||
msgctxt "selection:account.invoice,isr_withholding_type:"
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ncf_manager 8.0\n"
|
||||
"PO-Revision-Date: 2026-08-16 00:00-0400\n"
|
||||
"Last-Translator: Fundación Un País Mejor\n"
|
||||
"Language-Team: Spanish (Latin America)\n"
|
||||
"Language: es_419\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgctxt "field:account.configuration,default_customer_ncf_type:"
|
||||
msgid "Default Customer NCF Type"
|
||||
@@ -1021,3 +1028,95 @@ msgstr "Ingreso"
|
||||
msgctxt "view:account.invoice:"
|
||||
msgid "Tax Information"
|
||||
msgstr "Información Fiscal"
|
||||
|
||||
msgctxt "field:ncf.manager,activated_by:"
|
||||
msgid "Activated By"
|
||||
msgstr "Activada por"
|
||||
|
||||
msgctxt "field:ncf.manager,activated_at:"
|
||||
msgid "Activated At"
|
||||
msgstr "Fecha de activación"
|
||||
|
||||
msgctxt "field:ncf.manager,finished_by:"
|
||||
msgid "Finished By"
|
||||
msgstr "Finalizada por"
|
||||
|
||||
msgctxt "field:ncf.manager,finished_at:"
|
||||
msgid "Finished At"
|
||||
msgstr "Fecha de finalización"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,sequence_mode:"
|
||||
msgid "Sequence Configuration"
|
||||
msgstr "Configuración de secuencia"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,ncf_sequences:"
|
||||
msgid "Fiscal Sequence"
|
||||
msgstr "Secuencia fiscal"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,expected_prefix:"
|
||||
msgid "Expected Prefix"
|
||||
msgstr "Prefijo esperado"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,expected_padding:"
|
||||
msgid "Expected Padding"
|
||||
msgstr "Longitud del consecutivo"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,from_number:"
|
||||
msgid "Authorized From"
|
||||
msgstr "Desde autorizado"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,until_number:"
|
||||
msgid "Authorized Until"
|
||||
msgstr "Hasta autorizado"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,number_next:"
|
||||
msgid "First Number to Issue"
|
||||
msgstr "Primer número por emitir"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,expiration_date:"
|
||||
msgid "Expiration Date"
|
||||
msgstr "Fecha de vencimiento"
|
||||
|
||||
msgctxt "field:ncf.manager.sequence,availability:"
|
||||
msgid "Availability"
|
||||
msgstr "Disponibilidad"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,sequence_mode:existing"
|
||||
msgid "Select Existing Sequence"
|
||||
msgstr "Seleccionar secuencia existente"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,sequence_mode:create"
|
||||
msgid "Create Sequence on Activation"
|
||||
msgstr "Crear secuencia al activar"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,availability:available"
|
||||
msgid "Available"
|
||||
msgstr "Disponible"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,availability:low"
|
||||
msgid "Running Low"
|
||||
msgstr "Próxima a agotarse"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,availability:expired"
|
||||
msgid "Expired"
|
||||
msgstr "Vencida"
|
||||
|
||||
msgctxt "selection:ncf.manager.sequence,availability:exhausted"
|
||||
msgid "Exhausted"
|
||||
msgstr "Agotada"
|
||||
|
||||
msgctxt "model:ir.model.button,string:ncf_manager_activate_button"
|
||||
msgid "Activate"
|
||||
msgstr "Activar"
|
||||
|
||||
msgctxt "model:ir.model.button,string:ncf_manager_finish_button"
|
||||
msgid "Finish"
|
||||
msgstr "Finalizar"
|
||||
|
||||
msgctxt "model:ir.model.button,string:ncf_sequence_finish_button"
|
||||
msgid "Finish Sequence"
|
||||
msgstr "Finalizar secuencia"
|
||||
|
||||
msgctxt "view:ncf.manager:"
|
||||
msgid "Authorized Fiscal Sequences"
|
||||
msgstr "Secuencias fiscales autorizadas"
|
||||
|
||||
+33
@@ -126,5 +126,38 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<record model="ir.message" id="msg_ncf_next_before_range">
|
||||
<field name="text">The next fiscal number cannot be before the authorized range.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_manager_without_sequences">
|
||||
<field name="text">Add at least one fiscal sequence before activating the authorization.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_missing_authorization">
|
||||
<field name="text">Application and authorization numbers are required before activation.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_authorization_used">
|
||||
<field name="text">This authorization cannot be deleted because one of its fiscal sequences has been used on an invoice.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_sequence_used">
|
||||
<field name="text">This fiscal sequence cannot be deleted because it has been used on an invoice.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_sequence_type_mismatch">
|
||||
<field name="text">The selected sequence type does not correspond to the selected NCF type.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_non_fiscal_sequence">
|
||||
<field name="text">The “Invoice without NCF” classification does not create a fiscal sequence.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_assisted_values_required">
|
||||
<field name="text">Starting, ending and first issue numbers are required to create the fiscal sequence.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_sequence_overlap">
|
||||
<field name="text">The authorized range overlaps an active range for the same company and NCF type.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_sequence_already_assigned">
|
||||
<field name="text">The fiscal sequence is already assigned to another authorization line.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_sequence_delete_not_draft">
|
||||
<field name="text">Only draft fiscal sequence lines can be deleted.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_authorization_delete_not_draft">
|
||||
<field name="text">Only draft fiscal authorizations can be deleted.</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
+379
-15
@@ -1,8 +1,12 @@
|
||||
# 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 datetime
|
||||
|
||||
# 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.
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import ModelView, ModelSQL, fields
|
||||
|
||||
from trytond.model import ModelView, ModelSQL, Workflow, fields, Unique
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval
|
||||
from trytond.transaction import Transaction
|
||||
@@ -34,8 +38,48 @@ NCF_TYPE = [
|
||||
('99', 'Factura sin NCF'),
|
||||
]
|
||||
|
||||
ELECTRONIC_NCF_TYPES = frozenset({
|
||||
'31', '32', '33', '34', '41', '43', '44', '45', '46', '47'})
|
||||
|
||||
class NCFManager(ModelSQL, ModelView):
|
||||
NCF_SEQUENCE_TYPE_IDS = {
|
||||
'1': 'sequence_type_out_invoice_01',
|
||||
'2': 'sequence_type_out_invoice_02',
|
||||
'3': 'sequence_type_out_invoice_03',
|
||||
'4': 'sequence_type_out_credit_note_04',
|
||||
'11': 'sequence_type_out_invoice_11',
|
||||
'12': 'sequence_type_out_invoice_12',
|
||||
'13': 'sequence_type_out_invoice_13',
|
||||
'14': 'sequence_type_out_invoice_14',
|
||||
'15': 'sequence_type_out_invoice_15',
|
||||
'16': 'sequence_type_out_invoice_16',
|
||||
'17': 'sequence_type_out_invoice_17',
|
||||
'31': 'sequence_type_out_invoice_31',
|
||||
'32': 'sequence_type_out_invoice_32',
|
||||
'33': 'sequence_type_out_invoice_33',
|
||||
'34': 'sequence_type_out_credit_note_34',
|
||||
'41': 'sequence_type_out_invoice_41',
|
||||
'43': 'sequence_type_out_invoice_43',
|
||||
'44': 'sequence_type_out_invoice_44',
|
||||
'45': 'sequence_type_out_invoice_45',
|
||||
'46': 'sequence_type_out_invoice_46',
|
||||
'47': 'sequence_type_out_invoice_47',
|
||||
}
|
||||
|
||||
|
||||
def ncf_prefix(ncf_type):
|
||||
if not ncf_type or ncf_type == '99':
|
||||
return None
|
||||
series = 'E' if ncf_type in ELECTRONIC_NCF_TYPES else 'B'
|
||||
return f'{series}{ncf_type.zfill(2)}'
|
||||
|
||||
|
||||
def ncf_padding(ncf_type):
|
||||
if not ncf_type or ncf_type == '99':
|
||||
return 0
|
||||
return 10 if ncf_type in ELECTRONIC_NCF_TYPES else 8
|
||||
|
||||
|
||||
class NCFManager(Workflow, ModelSQL, ModelView):
|
||||
|
||||
'NCF Manager'
|
||||
__name__ = 'ncf.manager'
|
||||
@@ -66,10 +110,17 @@ class NCFManager(ModelSQL, ModelView):
|
||||
('draft', 'Draft'),
|
||||
('active', 'Active'),
|
||||
('finished', 'Finished'),
|
||||
], "State", sort=False)
|
||||
], "State", readonly=True, required=True, sort=False)
|
||||
|
||||
state_string = state.translated('state')
|
||||
|
||||
activated_by = fields.Many2One(
|
||||
'res.user', 'Activated By', readonly=True)
|
||||
activated_at = fields.DateTime('Activated At', readonly=True)
|
||||
finished_by = fields.Many2One(
|
||||
'res.user', 'Finished By', readonly=True)
|
||||
finished_at = fields.DateTime('Finished At', readonly=True)
|
||||
|
||||
del _states, _depends
|
||||
|
||||
@classmethod
|
||||
@@ -79,6 +130,20 @@ class NCFManager(ModelSQL, ModelView):
|
||||
('app_date', 'DESC'),
|
||||
('id', 'DESC'),
|
||||
]
|
||||
cls._transitions |= {
|
||||
('draft', 'active'),
|
||||
('active', 'finished'),
|
||||
}
|
||||
cls._buttons.update({
|
||||
'activate': {
|
||||
'invisible': Eval('state') != 'draft',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'finish': {
|
||||
'invisible': Eval('state') != 'active',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
def get_rec_name(self, name):
|
||||
parts = []
|
||||
@@ -96,8 +161,79 @@ class NCFManager(ModelSQL, ModelView):
|
||||
def default_state():
|
||||
return 'draft'
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('active')
|
||||
def activate(cls, managers):
|
||||
Line = Pool().get('ncf.manager.sequence')
|
||||
now = datetime.datetime.now()
|
||||
user = Transaction().user
|
||||
lines = []
|
||||
for manager in managers:
|
||||
if not manager.ncf_sequences:
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_manager_without_sequences'))
|
||||
manager._validate_authorization()
|
||||
for line in manager.ncf_sequences:
|
||||
line.prepare_sequence()
|
||||
line._validate_fiscal_sequence()
|
||||
line._validate_no_overlap()
|
||||
lines.append(line)
|
||||
if lines:
|
||||
Line.save(lines)
|
||||
Line.activate(lines)
|
||||
cls.write(managers, {
|
||||
'activated_by': user,
|
||||
'activated_at': now,
|
||||
})
|
||||
|
||||
class NCFManagerSequences(ModelSQL, ModelView):
|
||||
def _validate_authorization(self):
|
||||
if not self.app_number.strip() or not self.auth_number.strip():
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_missing_authorization'))
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('finished')
|
||||
def finish(cls, managers):
|
||||
Line = Pool().get('ncf.manager.sequence')
|
||||
lines = [line for manager in managers
|
||||
for line in manager.ncf_sequences if line.state == 'active']
|
||||
if lines:
|
||||
Line.complete(lines)
|
||||
cls.write(managers, {
|
||||
'finished_by': Transaction().user,
|
||||
'finished_at': datetime.datetime.now(),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def delete(cls, managers):
|
||||
if any(manager.state != 'draft' for manager in managers):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_authorization_delete_not_draft'))
|
||||
Invoice = Pool().get('account.invoice')
|
||||
if Invoice.search([
|
||||
('ncf_sequence.pos', 'in', [m.id for m in managers]),
|
||||
], limit=1):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_authorization_used'))
|
||||
super().delete(managers)
|
||||
|
||||
@classmethod
|
||||
def copy(cls, managers, default=None):
|
||||
default = dict(default or {})
|
||||
default.update({
|
||||
'state': 'draft',
|
||||
'ncf_sequences': None,
|
||||
'activated_by': None,
|
||||
'activated_at': None,
|
||||
'finished_by': None,
|
||||
'finished_at': None,
|
||||
})
|
||||
return super().copy(managers, default=default)
|
||||
|
||||
|
||||
class NCFManagerSequences(Workflow, ModelSQL, ModelView):
|
||||
|
||||
'NCF Manager Sequences'
|
||||
__name__ = 'ncf.manager.sequence'
|
||||
@@ -118,11 +254,60 @@ class NCFManagerSequences(ModelSQL, ModelView):
|
||||
|
||||
ncf_type_string = ncf_type.translated('ncf_type')
|
||||
|
||||
ncf_sequences = fields.Many2One('ir.sequence.strict', 'NCF Sequences',
|
||||
domain=[
|
||||
('company', '=', Eval(
|
||||
'context', {}).get('company', -1)),
|
||||
], states=_states, depends=_depends)
|
||||
company = fields.Function(
|
||||
fields.Many2One('company.company', 'Company'),
|
||||
'on_change_with_company')
|
||||
|
||||
expected_prefix = fields.Function(
|
||||
fields.Char('Expected Prefix'), 'on_change_with_expected_prefix')
|
||||
expected_padding = fields.Function(
|
||||
fields.Integer('Expected Padding'),
|
||||
'on_change_with_expected_padding')
|
||||
|
||||
sequence_mode = fields.Selection([
|
||||
('existing', 'Select Existing Sequence'),
|
||||
('create', 'Create Sequence on Activation'),
|
||||
], 'Sequence Configuration', required=True, states=_states,
|
||||
depends=_depends, sort=False)
|
||||
|
||||
ncf_sequences = fields.Many2One('ir.sequence.strict', 'Fiscal Sequence',
|
||||
domain=[
|
||||
('company', '=', Eval('company', -1)),
|
||||
('prefix', '=', Eval('expected_prefix')),
|
||||
('active', '=', True),
|
||||
],
|
||||
states={
|
||||
**_states,
|
||||
'required': Eval('sequence_mode') == 'existing',
|
||||
'invisible': Eval('sequence_mode') == 'create',
|
||||
},
|
||||
depends=_depends + [
|
||||
'company',
|
||||
'sequence_mode', 'expected_prefix', 'expected_padding'])
|
||||
|
||||
from_number = fields.Integer(
|
||||
'Authorized From', states={
|
||||
**_states,
|
||||
'required': Eval('sequence_mode') == 'create',
|
||||
'invisible': Eval('sequence_mode') != 'create',
|
||||
}, depends=_depends + ['sequence_mode'])
|
||||
until_number = fields.Integer(
|
||||
'Authorized Until', states={
|
||||
**_states,
|
||||
'required': Eval('sequence_mode') == 'create',
|
||||
'invisible': Eval('sequence_mode') != 'create',
|
||||
}, depends=_depends + ['sequence_mode'])
|
||||
number_next = fields.Integer(
|
||||
'First Number to Issue', states={
|
||||
**_states,
|
||||
'required': Eval('sequence_mode') == 'create',
|
||||
'invisible': Eval('sequence_mode') != 'create',
|
||||
}, depends=_depends + ['sequence_mode'])
|
||||
expiration_date = fields.Date(
|
||||
'Expiration Date', states={
|
||||
**_states,
|
||||
'invisible': Eval('sequence_mode') != 'create',
|
||||
}, depends=_depends + ['sequence_mode'])
|
||||
|
||||
invoice_type = fields.Selection([
|
||||
('invoice_customer', 'Customer Invoice'),
|
||||
@@ -134,16 +319,51 @@ class NCFManagerSequences(ModelSQL, ModelView):
|
||||
('draft', 'Draft'),
|
||||
('active', 'Active'),
|
||||
('finished', 'Finished'),
|
||||
], "State", sort=False)
|
||||
], "State", readonly=True, required=True, sort=False)
|
||||
|
||||
state_string = state.translated('state')
|
||||
|
||||
availability = fields.Function(fields.Selection([
|
||||
('available', 'Available'),
|
||||
('low', 'Running Low'),
|
||||
('expired', 'Expired'),
|
||||
('exhausted', 'Exhausted'),
|
||||
], 'Availability', sort=False), 'get_availability')
|
||||
availability_string = availability.translated('availability')
|
||||
finished_by = fields.Many2One(
|
||||
'res.user', 'Finished By', readonly=True)
|
||||
finished_at = fields.DateTime('Finished At', readonly=True)
|
||||
|
||||
del _states, _depends
|
||||
|
||||
@staticmethod
|
||||
def default_state():
|
||||
return 'draft'
|
||||
|
||||
@staticmethod
|
||||
def default_sequence_mode():
|
||||
return 'existing'
|
||||
|
||||
@fields.depends('pos', '_parent_pos.company')
|
||||
def on_change_with_company(self, name=None):
|
||||
return self.pos.company.id if self.pos and self.pos.company else None
|
||||
|
||||
@fields.depends('ncf_type')
|
||||
def on_change_with_expected_prefix(self, name=None):
|
||||
return ncf_prefix(self.ncf_type)
|
||||
|
||||
@fields.depends('ncf_type')
|
||||
def on_change_with_expected_padding(self, name=None):
|
||||
return ncf_padding(self.ncf_type)
|
||||
|
||||
@fields.depends(
|
||||
'sequence_mode', 'ncf_type', 'ncf_sequences',
|
||||
'from_number', 'until_number', 'number_next', 'expiration_date')
|
||||
def on_change_ncf_type(self):
|
||||
if (self.ncf_sequences
|
||||
and self.ncf_sequences.prefix != ncf_prefix(self.ncf_type)):
|
||||
self.ncf_sequences = None
|
||||
|
||||
def get_rec_name(self, name):
|
||||
if not self.ncf_type_string:
|
||||
return ''
|
||||
@@ -160,13 +380,157 @@ class NCFManagerSequences(ModelSQL, ModelView):
|
||||
sequence = self.ncf_sequences
|
||||
if not sequence:
|
||||
return
|
||||
if self.ncf_type == '99':
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_non_fiscal_sequence'))
|
||||
if sequence.company != self.pos.company:
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_company_mismatch'))
|
||||
expected_prefix = ('E' if self.ncf_type in {
|
||||
'31', '32', '33', '34', '41', '43', '44', '45', '46', '47'
|
||||
} else 'B') + self.ncf_type.zfill(2)
|
||||
expected_prefix = ncf_prefix(self.ncf_type)
|
||||
if self.ncf_type != '99' and sequence.prefix != expected_prefix:
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_prefix_mismatch',
|
||||
prefix=expected_prefix))
|
||||
if sequence.padding != ncf_padding(self.ncf_type):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_invalid_padding',
|
||||
padding=ncf_padding(self.ncf_type)))
|
||||
ModelData = Pool().get('ir.model.data')
|
||||
expected_type = ModelData.get_id(
|
||||
'ncf_manager', NCF_SEQUENCE_TYPE_IDS[self.ncf_type])
|
||||
if sequence.sequence_type.id != expected_type:
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_type_mismatch'))
|
||||
today = Transaction().context.get('date') or datetime.date.today()
|
||||
if sequence.expiration_date and sequence.expiration_date < today:
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_expired'))
|
||||
if (sequence.until_number is not None
|
||||
and sequence.number_next is not None
|
||||
and sequence.number_next > sequence.until_number):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_finished'))
|
||||
|
||||
def prepare_sequence(self):
|
||||
if self.sequence_mode != 'create':
|
||||
return
|
||||
if self.ncf_type == '99':
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_non_fiscal_sequence'))
|
||||
self._validate_assisted_values()
|
||||
pool = Pool()
|
||||
ModelData = pool.get('ir.model.data')
|
||||
Sequence = pool.get('ir.sequence.strict')
|
||||
sequence_type = ModelData.get_id(
|
||||
'ncf_manager', NCF_SEQUENCE_TYPE_IDS[self.ncf_type])
|
||||
sequence, = Sequence.create([{
|
||||
'name': f'{ncf_prefix(self.ncf_type)} - {self.pos.auth_number}',
|
||||
'sequence_type': sequence_type,
|
||||
'company': self.pos.company.id,
|
||||
'prefix': ncf_prefix(self.ncf_type),
|
||||
'padding': ncf_padding(self.ncf_type),
|
||||
'number_increment': 1,
|
||||
'number_next': self.number_next,
|
||||
'from_number': self.from_number,
|
||||
'until_number': self.until_number,
|
||||
'expiration_date': self.expiration_date,
|
||||
}])
|
||||
self.ncf_sequences = sequence
|
||||
self.sequence_mode = 'existing'
|
||||
|
||||
def _validate_assisted_values(self):
|
||||
if None in (self.from_number, self.until_number, self.number_next):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_assisted_values_required'))
|
||||
if not (1 <= self.from_number <= self.number_next
|
||||
<= self.until_number):
|
||||
raise UserError(gettext('ncf_manager.msg_ncf_invalid_range'))
|
||||
|
||||
def _validate_no_overlap(self):
|
||||
sequence = self.ncf_sequences
|
||||
if not sequence:
|
||||
return
|
||||
candidates = self.__class__.search([
|
||||
('id', '!=', self.id),
|
||||
('pos.company', '=', self.pos.company.id),
|
||||
('ncf_type', '=', self.ncf_type),
|
||||
('state', '=', 'active'),
|
||||
('ncf_sequences', '!=', None),
|
||||
])
|
||||
for candidate in candidates:
|
||||
other = candidate.ncf_sequences
|
||||
if None in (sequence.from_number, sequence.until_number,
|
||||
other.from_number, other.until_number):
|
||||
continue
|
||||
if (sequence.from_number <= other.until_number
|
||||
and other.from_number <= sequence.until_number):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_overlap'))
|
||||
|
||||
def get_availability(self, name):
|
||||
sequence = self.ncf_sequences
|
||||
if not sequence:
|
||||
return 'available'
|
||||
today = Transaction().context.get('date') or datetime.date.today()
|
||||
if sequence.expiration_date and sequence.expiration_date < today:
|
||||
return 'expired'
|
||||
if (sequence.until_number is not None
|
||||
and sequence.number_next is not None):
|
||||
remaining = sequence.until_number - sequence.number_next + 1
|
||||
if remaining <= 0:
|
||||
return 'exhausted'
|
||||
approved = ((sequence.until_number - sequence.from_number + 1)
|
||||
if sequence.from_number is not None else 0)
|
||||
if remaining <= max(10, int(approved * .1)):
|
||||
return 'low'
|
||||
return 'available'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
table = cls.__table__()
|
||||
cls._sql_constraints += [
|
||||
('fiscal_sequence_unique', Unique(table, table.ncf_sequences),
|
||||
'ncf_manager.msg_ncf_sequence_already_assigned'),
|
||||
]
|
||||
cls._transitions |= {
|
||||
('draft', 'active'),
|
||||
('active', 'finished'),
|
||||
}
|
||||
cls._buttons.update({
|
||||
'finish': {
|
||||
'invisible': Eval('state') != 'active',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
@Workflow.transition('active')
|
||||
def activate(cls, lines):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
def finish(cls, lines):
|
||||
cls.complete(lines)
|
||||
|
||||
@classmethod
|
||||
@Workflow.transition('finished')
|
||||
def complete(cls, lines):
|
||||
cls.write(lines, {
|
||||
'finished_by': Transaction().user,
|
||||
'finished_at': datetime.datetime.now(),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def delete(cls, lines):
|
||||
if any(line.state != 'draft' for line in lines):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_delete_not_draft'))
|
||||
Invoice = Pool().get('account.invoice')
|
||||
if Invoice.search([
|
||||
('ncf_sequence', 'in', [line.id for line in lines]),
|
||||
], limit=1):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_used'))
|
||||
super().delete(lines)
|
||||
|
||||
@@ -57,6 +57,27 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.button" id="ncf_manager_activate_button">
|
||||
<field name="name">activate</field>
|
||||
<field name="string">Activate</field>
|
||||
<field name="model">ncf.manager</field>
|
||||
</record>
|
||||
<record model="ir.model.button-res.group"
|
||||
id="ncf_manager_activate_button_group">
|
||||
<field name="button" ref="ncf_manager_activate_button"/>
|
||||
<field name="group" ref="account.group_account_admin"/>
|
||||
</record>
|
||||
<record model="ir.model.button" id="ncf_manager_finish_button">
|
||||
<field name="name">finish</field>
|
||||
<field name="string">Finish</field>
|
||||
<field name="model">ncf.manager</field>
|
||||
</record>
|
||||
<record model="ir.model.button-res.group"
|
||||
id="ncf_manager_finish_button_group">
|
||||
<field name="button" ref="ncf_manager_finish_button"/>
|
||||
<field name="group" ref="account.group_account_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="ncf_manager_sequences_view_form">
|
||||
<field name="model">ncf.manager.sequence</field>
|
||||
<field name="type">form</field>
|
||||
@@ -86,5 +107,16 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.button" id="ncf_sequence_finish_button">
|
||||
<field name="name">finish</field>
|
||||
<field name="string">Finish Sequence</field>
|
||||
<field name="model">ncf.manager.sequence</field>
|
||||
</record>
|
||||
<record model="ir.model.button-res.group"
|
||||
id="ncf_sequence_finish_button_group">
|
||||
<field name="button" ref="ncf_sequence_finish_button"/>
|
||||
<field name="group" ref="account.group_account_admin"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
@@ -141,3 +141,7 @@ class SequenceStrict(Sequence):
|
||||
_table = None # Needed to reset Sequence._table
|
||||
_strict = True
|
||||
|
||||
ncf_authorization_lines = fields.One2Many(
|
||||
'ncf.manager.sequence', 'ncf_sequences', 'NCF Authorizations',
|
||||
readonly=True)
|
||||
|
||||
|
||||
@@ -216,6 +216,46 @@ class NcfManagerTestCase(ModuleTestCase):
|
||||
self.assertEqual(
|
||||
invoice.expiration_date, datetime.date(today.year, 12, 31))
|
||||
|
||||
@with_transaction()
|
||||
def test_assisted_sequence_workflow(self):
|
||||
pool = Pool()
|
||||
Manager = pool.get('ncf.manager')
|
||||
today = datetime.date.today()
|
||||
company = self._create_company()
|
||||
|
||||
manager, = Manager.create([{
|
||||
'company': company.id,
|
||||
'app_date': today,
|
||||
'app_number': 'APP0000001',
|
||||
'auth_number': 'AUT0000001',
|
||||
'ncf_sequences': [('create', [{
|
||||
'invoice_type': 'invoice_customer',
|
||||
'ncf_type': '1',
|
||||
'sequence_mode': 'create',
|
||||
'from_number': 100,
|
||||
'number_next': 100,
|
||||
'until_number': 109,
|
||||
'expiration_date': datetime.date(today.year, 12, 31),
|
||||
}])],
|
||||
}])
|
||||
|
||||
Manager.activate([manager])
|
||||
manager = Manager(manager.id)
|
||||
line, = manager.ncf_sequences
|
||||
self.assertEqual(manager.state, 'active')
|
||||
self.assertEqual(line.state, 'active')
|
||||
self.assertEqual(line.sequence_mode, 'existing')
|
||||
self.assertEqual(line.ncf_sequences.prefix, 'B01')
|
||||
self.assertEqual(line.ncf_sequences.padding, 8)
|
||||
self.assertEqual(line.ncf_sequences.number_next, 100)
|
||||
self.assertEqual(line.availability, 'low')
|
||||
|
||||
Manager.finish([manager])
|
||||
manager = Manager(manager.id)
|
||||
self.assertEqual(manager.state, 'finished')
|
||||
self.assertEqual(manager.ncf_sequences[0].state, 'finished')
|
||||
self.assertIsNotNone(manager.finished_at)
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
|
||||
|
||||
+28
-10
@@ -3,15 +3,33 @@
|
||||
The COPYRIGHT file at the top level of this repository contains the full
|
||||
copyright notices and license types. -->
|
||||
<form>
|
||||
<label name="company"/>
|
||||
<field name="company"/>
|
||||
<label name="app_date"/>
|
||||
<field name="app_date"/>
|
||||
<label name="app_number"/>
|
||||
<field name="app_number"/>
|
||||
<label name="auth_number"/>
|
||||
<field name="auth_number"/>
|
||||
<group id="authorization" col="4" colspan="4">
|
||||
<label name="company"/>
|
||||
<field name="company"/>
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<label name="app_date"/>
|
||||
<field name="app_date"/>
|
||||
<newline/>
|
||||
<label name="app_number"/>
|
||||
<field name="app_number"/>
|
||||
<label name="auth_number"/>
|
||||
<field name="auth_number"/>
|
||||
</group>
|
||||
<separator string="Authorized Fiscal Sequences" id="sequences" colspan="4"/>
|
||||
<field name="ncf_sequences" colspan="4"/>
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<group id="audit" col="4" colspan="4">
|
||||
<label name="activated_by"/>
|
||||
<field name="activated_by"/>
|
||||
<label name="activated_at"/>
|
||||
<field name="activated_at"/>
|
||||
<label name="finished_by"/>
|
||||
<field name="finished_by"/>
|
||||
<label name="finished_at"/>
|
||||
<field name="finished_at"/>
|
||||
</group>
|
||||
<group col="2" colspan="4" id="buttons">
|
||||
<button name="activate" icon="tryton-ok"/>
|
||||
<button name="finish" icon="tryton-stop"/>
|
||||
</group>
|
||||
</form>
|
||||
|
||||
@@ -6,9 +6,28 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="invoice_type"/>
|
||||
<label name="ncf_type"/>
|
||||
<field name="ncf_type"/>
|
||||
<label name="expected_prefix"/>
|
||||
<field name="expected_prefix"/>
|
||||
<label name="sequence_mode"/>
|
||||
<field name="sequence_mode"/>
|
||||
<label name="ncf_sequences"/>
|
||||
<field name="ncf_sequences"/>
|
||||
<newline/>
|
||||
<label name="from_number"/>
|
||||
<field name="from_number"/>
|
||||
<label name="until_number"/>
|
||||
<field name="until_number"/>
|
||||
<label name="number_next"/>
|
||||
<field name="number_next"/>
|
||||
<label name="expiration_date"/>
|
||||
<field name="expiration_date"/>
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<label name="availability"/>
|
||||
<field name="availability"/>
|
||||
<label name="finished_by"/>
|
||||
<field name="finished_by"/>
|
||||
<label name="finished_at"/>
|
||||
<field name="finished_at"/>
|
||||
<button name="finish" icon="tryton-stop"/>
|
||||
</form>
|
||||
|
||||
@@ -4,6 +4,13 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="invoice_type"/>
|
||||
<field name="ncf_type"/>
|
||||
<field name="expected_prefix"/>
|
||||
<field name="ncf_sequences"/>
|
||||
<field name="from_number" optional="1"/>
|
||||
<field name="until_number" optional="1"/>
|
||||
<field name="number_next" optional="1"/>
|
||||
<field name="expiration_date" optional="1"/>
|
||||
<field name="availability"/>
|
||||
<field name="state"/>
|
||||
<button name="finish"/>
|
||||
</tree>
|
||||
|
||||
@@ -8,4 +8,6 @@ copyright notices and license types. -->
|
||||
<field name="app_number"/>
|
||||
<field name="auth_number"/>
|
||||
<field name="state"/>
|
||||
<button name="activate"/>
|
||||
<button name="finish"/>
|
||||
</tree>
|
||||
|
||||
Reference in New Issue
Block a user