diff --git a/README.rst b/README.rst
index 758a615..d02fa86 100644
--- a/README.rst
+++ b/README.rst
@@ -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.
diff --git a/invoice.py b/invoice.py
index f73168c..b5c5220 100755
--- a/invoice.py
+++ b/invoice.py
@@ -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):
diff --git a/locale/es.po b/locale/es.po
index cc42596..1970946 100644
--- a/locale/es.po
+++ b/locale/es.po
@@ -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"
diff --git a/locale/es_419.po b/locale/es_419.po
index 4bfae75..7d72390 100644
--- a/locale/es_419.po
+++ b/locale/es_419.po
@@ -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"
diff --git a/message.xml b/message.xml
index 8fef676..415b728 100644
--- a/message.xml
+++ b/message.xml
@@ -126,5 +126,38 @@ this repository contains the full copyright notices and license terms. -->
The next fiscal number cannot be before the authorized range.
+
+ Add at least one fiscal sequence before activating the authorization.
+
+
+ Application and authorization numbers are required before activation.
+
+
+ This authorization cannot be deleted because one of its fiscal sequences has been used on an invoice.
+
+
+ This fiscal sequence cannot be deleted because it has been used on an invoice.
+
+
+ The selected sequence type does not correspond to the selected NCF type.
+
+
+ The “Invoice without NCF” classification does not create a fiscal sequence.
+
+
+ Starting, ending and first issue numbers are required to create the fiscal sequence.
+
+
+ The authorized range overlaps an active range for the same company and NCF type.
+
+
+ The fiscal sequence is already assigned to another authorization line.
+
+
+ Only draft fiscal sequence lines can be deleted.
+
+
+ Only draft fiscal authorizations can be deleted.
+
diff --git a/ncf_manager.py b/ncf_manager.py
index 0eff543..b262d13 100644
--- a/ncf_manager.py
+++ b/ncf_manager.py
@@ -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)
diff --git a/ncf_manager.xml b/ncf_manager.xml
index d8c873c..3834df5 100644
--- a/ncf_manager.xml
+++ b/ncf_manager.xml
@@ -57,6 +57,27 @@ this repository contains the full copyright notices and license terms. -->
+
+ activate
+ Activate
+ ncf.manager
+
+
+
+
+
+
+ finish
+ Finish
+ ncf.manager
+
+
+
+
+
+
ncf.manager.sequence
form
@@ -86,5 +107,16 @@ this repository contains the full copyright notices and license terms. -->
+
+ finish
+ Finish Sequence
+ ncf.manager.sequence
+
+
+
+
+
+
diff --git a/sequence.py b/sequence.py
index 6ef13b8..41982c0 100644
--- a/sequence.py
+++ b/sequence.py
@@ -140,4 +140,8 @@ class SequenceStrict(Sequence):
__name__ = 'ir.sequence.strict'
_table = None # Needed to reset Sequence._table
_strict = True
+
+ ncf_authorization_lines = fields.One2Many(
+ 'ncf.manager.sequence', 'ncf_sequences', 'NCF Authorizations',
+ readonly=True)
diff --git a/tests/test_module.py b/tests/test_module.py
index e6831d8..c3e146f 100644
--- a/tests/test_module.py
+++ b/tests/test_module.py
@@ -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
diff --git a/view/ncf_manager_form.xml b/view/ncf_manager_form.xml
index dbec5d9..b251ca0 100644
--- a/view/ncf_manager_form.xml
+++ b/view/ncf_manager_form.xml
@@ -3,15 +3,33 @@
The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license types. -->
diff --git a/view/ncf_manager_sequences_form.xml b/view/ncf_manager_sequences_form.xml
index a89ab2f..d26b3f8 100644
--- a/view/ncf_manager_sequences_form.xml
+++ b/view/ncf_manager_sequences_form.xml
@@ -6,9 +6,28 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
diff --git a/view/ncf_manager_sequences_tree.xml b/view/ncf_manager_sequences_tree.xml
index 041fcc1..a248763 100644
--- a/view/ncf_manager_sequences_tree.xml
+++ b/view/ncf_manager_sequences_tree.xml
@@ -4,6 +4,13 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
+
+
+
+
diff --git a/view/ncf_manager_tree.xml b/view/ncf_manager_tree.xml
index ad76743..6d6b719 100644
--- a/view/ncf_manager_tree.xml
+++ b/view/ncf_manager_tree.xml
@@ -8,4 +8,6 @@ copyright notices and license types. -->
+
+