Add protected NCF authorization workflow
Publicar paquete PyPI / publish (push) Successful in 41s

This commit is contained in:
José Arturo García
2026-08-16 13:52:53 -04:00
parent b96639f3e8
commit 60bbed6de0
13 changed files with 762 additions and 28 deletions
+14
View File
@@ -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
View File
@@ -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):
+92
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+378 -14
View File
@@ -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',
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(
'context', {}).get('company', -1)),
], states=_states, depends=_depends)
('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)
+32
View File
@@ -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>
+4
View File
@@ -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)
+40
View File
@@ -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
+20 -2
View File
@@ -3,15 +3,33 @@
The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license types. -->
<form>
<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>
+19
View File
@@ -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>
+7
View File
@@ -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>
+2
View File
@@ -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>