Homologate fiscal management with Tryton 8.0

This commit is contained in:
José Arturo García
2026-08-30 18:28:13 -04:00
parent c68efb499b
commit 7b3edd3e51
37 changed files with 4782 additions and 1148 deletions
+12
View File
@@ -0,0 +1,12 @@
Version 7.0.3 - 2026-08-30
==========================
* Homologación funcional con la serie 8.0 manteniendo el registro y el
empaquetado propios de Tryton 7.0.
* Flujo protegido de autorizaciones y rangos NCF, importador DGII idempotente,
auditoría de activación y cierre, y restricciones por usuario.
* Validaciones de prefijo, rango, solapamiento por dirección, numeración,
notas de crédito/débito y cancelación fiscal.
* Corrección de búsquedas de montos por tipo de producto y compatibilidad con
nombres Unicode de secuencias electrónicas.
+8 -2
View File
@@ -1,10 +1,11 @@
from trytond.pool import Pool
from . import invoice
from . import importer
from . import ncf_manager
from . import sequence
from . import configuration
from . import party
from . import report
from . import user
def register():
Pool.register(
@@ -17,7 +18,12 @@ def register():
configuration.DefaultNcfType,
party.PartyNcfType,
party.Party,
user.User,
importer.ImportNCFStart,
module='ncf_manager', type_='model')
Pool.register(
report.InvoiceReport,
importer.ImportNCF,
module='ncf_manager', type_='wizard')
Pool.register(
invoice.InvoiceReport,
module='ncf_manager', type_='report')
+27 -4
View File
@@ -2,6 +2,7 @@
# this repository contains the full copyright notices and license terms.
import logging
from trytond import backend
from trytond.model import fields, ModelSQL
from trytond.modules.company.model import CompanyValueMixin
from trytond.pool import PoolMeta, Pool
@@ -12,6 +13,11 @@ from trytond.modules.ncf_manager.ncf_manager import NCF_TYPE
logger = logging.getLogger(__name__)
def _backend_name():
name = backend.name
return name() if callable(name) else name
class Configuration(metaclass=PoolMeta):
'Account Configuration'
__name__ = 'account.configuration'
@@ -80,6 +86,9 @@ class DefaultNcfType(ModelSQL, CompanyValueMixin):
@classmethod
def _ncf_column_exists(cls, cursor, table_name, column_name):
if _backend_name() == 'sqlite':
cursor.execute(f'PRAGMA table_info("{table_name}")')
return any(row[1] == column_name for row in cursor.fetchall())
cursor.execute("""
SELECT 1
FROM information_schema.columns
@@ -90,6 +99,12 @@ class DefaultNcfType(ModelSQL, CompanyValueMixin):
@classmethod
def _ncf_column_udt_name(cls, cursor, table_name, column_name):
if _backend_name() == 'sqlite':
cursor.execute(f'PRAGMA table_info("{table_name}")')
for row in cursor.fetchall():
if row[1] == column_name:
return (row[2] or '').lower()
return None
cursor.execute("""
SELECT udt_name
FROM information_schema.columns
@@ -129,7 +144,7 @@ class DefaultNcfType(ModelSQL, CompanyValueMixin):
udt_name = cls._ncf_column_udt_name(
cursor, table_name, old_column)
if udt_name in {'int2', 'int4', 'int8'}:
if udt_name in {'int2', 'int4', 'int8', 'integer'}:
logger.info(
"Renaming legacy %s.%s integer column to %s before "
"Tryton creates the new Selection column",
@@ -221,6 +236,14 @@ class DefaultNcfType(ModelSQL, CompanyValueMixin):
# avoid converting a value such as '31' again.
if not cls._ncf_column_exists(cursor, table_name, new_column):
continue
if _backend_name() == 'sqlite':
logger.info(
"Skipping PostgreSQL-style text fallback migration for "
"%s.%s on SQLite",
table_name,
new_column,
)
continue
udt_name = cls._ncf_column_udt_name(cursor, table_name, new_column)
if udt_name not in {'varchar', 'text', 'bpchar'}:
@@ -246,14 +269,14 @@ class DefaultNcfType(ModelSQL, CompanyValueMixin):
WHERE config.{new_column} IS NOT NULL
AND config.{new_column} <> ''
AND config.{new_column} ~ '^[0-9]+$'
AND config.{new_column} NOT IN %s
AND config.{new_column} != ALL(%s)
AND config.{new_column}::integer = seq.id
AND seq.ncf_type IS NOT NULL
AND seq.ncf_type <> ''
""", (valid_ncf_type_codes,))
""", (list(valid_ncf_type_codes),))
logger.info(
"Migrated %s text rows for account configuration field %s",
cursor.rowcount,
new_column,
)
)
+75 -6
View File
@@ -1,9 +1,78 @@
##################
Ncf Manager Module
NCF Manager Module
##################
.. toctree::
:maxdepth: 2
The ncf_manager module manages Dominican Republic fiscal receipt numbers
for Tryton invoices.
usage
design
It provides NCF/e-CF types, fiscal sequence management, invoice fiscal fields,
NCF assignment on posting, and fiscal preparation for debit and credit notes.
Installing
**********
Install the package and activate the module in the database::
pip install solutema_ncf_manager
trytond-admin -d <database> -u ncf_manager
Publishing
**********
The included Gitea Actions workflow builds and publishes the package to the
Gitea PyPI registry from version-series branches such as ``7.0``.
Configure this repository secret:
* ``REGISTRY_PASSWORD``
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.
Version compatibility
*********************
The 7.0 and 8.0 series share the same fiscal models, workflows, importer,
validations and tests. Only the module registration, packaging metadata and
report XML required by each Tryton series remain version-specific.
Authorization workflow
**********************
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. Incoming supplier documents and outgoing customer documents use
independent numbering authorities, so overlap is enforced only within the
same document direction. The last issued number automatically finishes the
line. Finishing an authorization closes every remaining active line without
reusing numbers.
Legacy migration lines keep their original technical sequence immutable after
activation. Prefix selection is enforced while a line is in draft and fiscal
activation performs the same server-side validation; closing a protected
historical line therefore does not invalidate already-linked invoices.
Electronic fiscal numbers are only reserved by this module. Activating an
E-series authorization never signs or transmits an e-CF to DGII.
Importing DGII authorizations
*****************************
Use **NCF Manager → Import DGII Authorization** to load an authorization
downloaded from the DGII Virtual Office. XML, CSV and the HTML table exported
with an ``.xls`` extension are supported and detected from their content.
The importer creates an independent workflow record for every ``NUM_AUTOR``.
The same ``NUM_SOL`` may therefore appear in several records without modifying
an authorization that is already active; file names are not used as fiscal
identifiers. It validates
approval status, company name, NCF type, prefix, range and approved quantity,
and stores the original file, SHA-256, user and import date for audit.
Imports are idempotent. Reimporting an equivalent CSV, XML or XLS record does
not create another line, while different fiscal data for an existing
authorization is rejected. The safe default leaves the result in draft so
the first number can be reviewed. Immediate activation is explicit and still
does not transmit electronic documents.
+333
View File
@@ -0,0 +1,333 @@
import csv
import datetime
import hashlib
from html.parser import HTMLParser
import json
import re
import unicodedata
import xml.etree.ElementTree as ET
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.model import ModelView, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
from .ncf_manager import NCF_SEQUENCE_TYPE_IDS, ncf_padding, ncf_prefix
REQUIRED_COLUMNS = {
'NUM_SOL', 'NUM_AUTOR', 'FECHA_AUTOR', 'OBSERVACIONES',
'NUMERO_DESDE', 'NUMERO_HASTA', 'CODTIPO', 'FECHA_SOLICITUD',
'ESTATUS', 'CANTIDAD_APROBADA',
}
SUPPLIER_NCF_TYPES = frozenset({'11', '13', '17', '41', '43', '47'})
MAX_IMPORT_SIZE = 5 * 1024 * 1024
def _text(value):
return (value or '').replace('\xa0', ' ').strip()
def _normalized_name(value):
value = unicodedata.normalize('NFKD', _text(value).upper())
return ''.join(character for character in value
if character.isalnum())
def _parse_datetime(value):
value = _text(value)
if not value:
return None
try:
return datetime.datetime.fromisoformat(value).replace(tzinfo=None)
except ValueError:
pass
for pattern in ('%d/%m/%Y %H:%M:%S', '%d/%m/%Y %H:%M'):
try:
return datetime.datetime.strptime(value, pattern)
except ValueError:
continue
raise ValueError(f'Invalid date: {value}')
class _HTMLTableParser(HTMLParser):
def __init__(self):
super().__init__()
self.rows = []
self._row = None
self._cell = None
def handle_starttag(self, tag, attrs):
if tag == 'tr':
self._row = []
elif tag in {'td', 'th'} and self._row is not None:
self._cell = []
def handle_data(self, data):
if self._cell is not None:
self._cell.append(data)
def handle_endtag(self, tag):
if tag in {'td', 'th'} and self._cell is not None:
self._row.append(_text(''.join(self._cell)))
self._cell = None
elif tag == 'tr' and self._row:
self.rows.append(self._row)
self._row = None
def _parse_xml(data):
root = ET.fromstring(data)
rows = []
for element in list(root):
row = {
child.tag.rsplit('}', 1)[-1]: _text(child.text)
for child in list(element)
}
if row:
rows.append(row)
return rows
def _parse_html(data):
parser = _HTMLTableParser()
parser.feed(data.decode('utf-8-sig', errors='replace'))
if len(parser.rows) < 2:
return []
header = parser.rows[0]
return [dict(zip(header, row)) for row in parser.rows[1:]]
def _parse_csv(data):
# DGII does not always quote DETALLE_LOG even when it contains commas.
# Splitting at most len(header)-1 times preserves that final field.
text = data.decode('utf-8-sig', errors='strict')
lines = [line for line in text.splitlines() if line.strip()]
if len(lines) < 2:
return []
header = next(csv.reader([lines[0]]))
rows = []
for line in lines[1:]:
values = line.split(',', len(header) - 1)
rows.append(dict(zip(header, values)))
return rows
def parse_dgii_authorizations(data):
data = bytes(data)
if not data or len(data) > MAX_IMPORT_SIZE:
raise UserError(gettext('ncf_manager.msg_dgii_import_invalid_size'))
stripped = data.lstrip()
try:
if stripped.startswith(b'<'):
if re.search(br'<\s*(?:\w+:)?SolicitudNCF\b', stripped[:500]):
rows = _parse_xml(data)
source_format = 'xml'
elif re.search(br'<\s*(?:html|div|table)\b', stripped[:500], re.I):
rows = _parse_html(data)
source_format = 'xls_html'
else:
raise ValueError('Unsupported XML/HTML document')
else:
rows = _parse_csv(data)
source_format = 'csv'
except (UnicodeDecodeError, ET.ParseError, ValueError) as exception:
raise UserError(gettext(
'ncf_manager.msg_dgii_import_read_error', error=str(exception)))
if not rows or not REQUIRED_COLUMNS.issubset(rows[0]):
raise UserError(gettext('ncf_manager.msg_dgii_import_invalid_file'))
return source_format, [{key: _text(value) for key, value in row.items()}
for row in rows]
def normalize_dgii_row(row):
if row['ESTATUS'].upper() != 'APROBADA':
raise UserError(gettext(
'ncf_manager.msg_dgii_import_not_approved',
authorization=row['NUM_AUTOR']))
ncf_type = row['CODTIPO'].lstrip('0')
if ncf_type not in NCF_SEQUENCE_TYPE_IDS:
raise UserError(gettext(
'ncf_manager.msg_dgii_import_unknown_type', type=ncf_type))
start = row['NUMERO_DESDE'].upper()
end = row['NUMERO_HASTA'].upper()
prefix = ncf_prefix(ncf_type)
padding = ncf_padding(ncf_type)
pattern = rf'{re.escape(prefix)}(\d{{{padding}}})'
start_match = re.fullmatch(pattern, start)
end_match = re.fullmatch(pattern, end)
if not start_match or not end_match:
raise UserError(gettext(
'ncf_manager.msg_dgii_import_invalid_range',
authorization=row['NUM_AUTOR']))
from_number = int(start_match.group(1))
until_number = int(end_match.group(1))
approved = int(row['CANTIDAD_APROBADA'])
if from_number < 1 or until_number < from_number:
raise UserError(gettext('ncf_manager.msg_ncf_invalid_range'))
if approved != until_number - from_number + 1:
raise UserError(gettext(
'ncf_manager.msg_dgii_import_quantity_mismatch',
authorization=row['NUM_AUTOR']))
try:
requested = int(row.get('CANTIDAD_SOLICITADA') or 0) or None
authorization_date = _parse_datetime(row['FECHA_AUTOR'])
application_date = _parse_datetime(row['FECHA_SOLICITUD'])
expiration = _parse_datetime(row.get('FECHA_VIGENCIA'))
except ValueError as exception:
raise UserError(gettext(
'ncf_manager.msg_dgii_import_read_error', error=str(exception)))
if not application_date or not authorization_date:
raise UserError(gettext(
'ncf_manager.msg_dgii_import_missing_dates',
authorization=row['NUM_AUTOR']))
normalized = {
'application_number': row['NUM_SOL'],
'authorization_number': row['NUM_AUTOR'],
'application_date': application_date,
'authorization_date': authorization_date,
'company_name': row.get('OBSERVACIONES', ''),
'ncf_type': ncf_type,
'invoice_type': ('invoice_supplier'
if ncf_type in SUPPLIER_NCF_TYPES else 'invoice_customer'),
'from_number': from_number,
'until_number': until_number,
'approved_quantity': approved,
'requested_quantity': requested,
'expiration_date': expiration.date() if expiration else None,
'usage_type': row.get('TIPO_USO', ''),
'status': row['ESTATUS'].upper(),
'detail': row.get('DETALLE_LOG', ''),
}
fingerprint_values = {key: value.isoformat()
if isinstance(value, (datetime.date, datetime.datetime)) else value
for key, value in normalized.items()
if key not in {'detail', 'company_name'}}
normalized['fingerprint'] = hashlib.sha256(json.dumps(
fingerprint_values, ensure_ascii=False, sort_keys=True,
separators=(',', ':')).encode()).hexdigest()
return normalized
def import_dgii_authorizations(company, data, filename, activate=False):
pool = Pool()
Manager = pool.get('ncf.manager')
Line = pool.get('ncf.manager.sequence')
data = bytes(data)
_source_format, rows = parse_dgii_authorizations(data)
normalized_rows = [normalize_dgii_row(row) for row in rows]
company_name = _normalized_name(company.party.name)
for row in normalized_rows:
supplied_name = _normalized_name(row['company_name'])
if supplied_name and supplied_name != company_name:
raise UserError(gettext(
'ncf_manager.msg_dgii_import_company_mismatch',
company=row['company_name']))
file_hash = hashlib.sha256(data).hexdigest()
managers_to_activate = []
imported_lines = []
for row in normalized_rows:
duplicates = Line.search([
('pos.company', '=', company.id),
('auth_number', '=', row['authorization_number']),
], limit=1)
if duplicates:
if duplicates[0].import_fingerprint != row['fingerprint']:
raise UserError(gettext(
'ncf_manager.msg_dgii_import_conflict',
authorization=row['authorization_number']))
continue
# A DGII request number may appear in more than one downloaded
# authorization. Keep every NUM_AUTOR in its own workflow record so
# importing a later authorization never mutates an active one.
manager, = Manager.create([{
'company': company.id,
'app_number': row['application_number'],
'app_date': row['application_date'].date(),
'source_filename': filename,
'source_hash': file_hash,
'source_file': data,
'imported_by': Transaction().user,
'imported_at': datetime.datetime.now(),
}])
line, = Line.create([{
'pos': manager.id,
'auth_number': row['authorization_number'],
'authorization_date': row['authorization_date'],
'ncf_type': row['ncf_type'],
'invoice_type': row['invoice_type'],
'sequence_mode': 'create',
'from_number': row['from_number'],
'number_next': row['from_number'],
'until_number': row['until_number'],
'expiration_date': row['expiration_date'],
'dgii_status': row['status'],
'requested_quantity': row['requested_quantity'],
'approved_quantity': row['approved_quantity'],
'usage_type': row['usage_type'],
'dgii_detail': row['detail'],
'import_fingerprint': row['fingerprint'],
'source_filename': filename,
'source_hash': file_hash,
'source_file': data,
'imported_by': Transaction().user,
'imported_at': datetime.datetime.now(),
}])
imported_lines.append(line)
managers_to_activate.append(manager)
if activate and managers_to_activate:
Manager.activate(managers_to_activate)
return managers_to_activate, imported_lines
class ImportNCFStart(ModelView):
'Import DGII NCF Authorization'
__name__ = 'ncf.import.start'
company = fields.Many2One(
'company.company', 'Company', required=True)
file_ = fields.Binary('DGII File', required=True, filename='filename')
filename = fields.Char('Filename', required=True)
activate = fields.Boolean(
'Activate Imported Authorization',
help='Creates the strict sequence and activates it immediately. '
'Leave disabled when numbers may already have been issued outside '
'Tryton so the first number can be reviewed.')
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_activate():
return False
class ImportNCF(Wizard):
'Import DGII NCF Authorization'
__name__ = 'ncf.import'
start = StateView('ncf.import.start',
'ncf_manager.ncf_import_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Import', 'import_', 'tryton-ok', default=True),
])
import_ = StateTransition()
def transition_import_(self):
import_dgii_authorizations(
self.start.company, self.start.file_, self.start.filename,
activate=self.start.activate)
return 'end'
__all__ = [
'ImportNCF', 'ImportNCFStart', 'import_dgii_authorizations',
'normalize_dgii_row', 'parse_dgii_authorizations']
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="ncf_import_start_view_form">
<field name="model">ncf.import.start</field>
<field name="type">form</field>
<field name="name">ncf_import_start_form</field>
</record>
<record model="ir.action.wizard" id="act_ncf_import">
<field name="name">Import DGII Authorization</field>
<field name="wiz_name">ncf.import</field>
</record>
<menuitem name="Import DGII Authorization"
parent="main_menu_ncf_manager" action="act_ncf_import"
sequence="20" id="menu_ncf_import"/>
<record model="ir.action-res.group" id="act_ncf_import_group">
<field name="action" ref="act_ncf_import"/>
<field name="group" ref="account.group_account_admin"/>
</record>
</data>
</tryton>
+16 -16
View File
@@ -4,28 +4,28 @@
<office:meta><meta:generator>LibreOffice/26.2.1.2$MacOSX_X86_64 LibreOffice_project/8399f6259d8c87f40e7255cdb3c9b958f5e08948</meta:generator><meta:creation-date>2012-11-15T15:00:35</meta:creation-date><meta:editing-cycles>1</meta:editing-cycles><meta:editing-duration>P0D</meta:editing-duration><meta:document-statistic meta:table-count="13" meta:image-count="2" meta:object-count="0" meta:page-count="6" meta:paragraph-count="253" meta:word-count="666" meta:character-count="8510" meta:non-whitespace-character-count="7652"/><meta:user-defined meta:name="Info 1"/><meta:user-defined meta:name="Info 2"/><meta:user-defined meta:name="Info 3"/><meta:user-defined meta:name="Info 4"/></office:meta>
<office:settings>
<config:config-item-set config:name="ooo:view-settings">
<config:config-item config:name="ViewAreaTop" config:type="long">54878</config:config-item>
<config:config-item config:name="ViewAreaTop" config:type="long">53</config:config-item>
<config:config-item config:name="ViewAreaLeft" config:type="long">0</config:config-item>
<config:config-item config:name="ViewAreaWidth" config:type="long">37102</config:config-item>
<config:config-item config:name="ViewAreaHeight" config:type="long">16522</config:config-item>
<config:config-item config:name="ViewAreaWidth" config:type="long">28942</config:config-item>
<config:config-item config:name="ViewAreaHeight" config:type="long">15201</config:config-item>
<config:config-item config:name="ShowRedlineChanges" config:type="boolean">true</config:config-item>
<config:config-item config:name="InBrowseMode" config:type="boolean">false</config:config-item>
<config:config-item-map-indexed config:name="Views">
<config:config-item-map-entry>
<config:config-item config:name="ViewId" config:type="string">view2</config:config-item>
<config:config-item config:name="ViewLeft" config:type="long">21911</config:config-item>
<config:config-item config:name="ViewTop" config:type="long">65701</config:config-item>
<config:config-item config:name="ViewLeft" config:type="long">19632</config:config-item>
<config:config-item config:name="ViewTop" config:type="long">3383</config:config-item>
<config:config-item config:name="VisibleLeft" config:type="long">0</config:config-item>
<config:config-item config:name="VisibleTop" config:type="long">54878</config:config-item>
<config:config-item config:name="VisibleRight" config:type="long">37100</config:config-item>
<config:config-item config:name="VisibleBottom" config:type="long">71399</config:config-item>
<config:config-item config:name="VisibleTop" config:type="long">53</config:config-item>
<config:config-item config:name="VisibleRight" config:type="long">28940</config:config-item>
<config:config-item config:name="VisibleBottom" config:type="long">15252</config:config-item>
<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>
<config:config-item config:name="ViewLayoutColumns" config:type="short">1</config:config-item>
<config:config-item config:name="ViewLayoutBookMode" config:type="boolean">false</config:config-item>
<config:config-item config:name="ZoomFactor" config:type="short">100</config:config-item>
<config:config-item config:name="IsSelectedFrame" config:type="boolean">false</config:config-item>
<config:config-item config:name="KeepRatio" config:type="boolean">false</config:config-item>
<config:config-item config:name="WindowState" config:type="string">-1884,161,1920,1017;1;,,,;</config:config-item>
<config:config-item config:name="WindowState" config:type="string">0,66,1512,916;4;,,,;</config:config-item>
<config:config-item config:name="AnchoredTextOverflowLegacy" config:type="boolean">false</config:config-item>
<config:config-item config:name="LegacySingleLineFontwork" config:type="boolean">false</config:config-item>
<config:config-item config:name="ConnectorUseSnapRect" config:type="boolean">false</config:config-item>
@@ -92,7 +92,7 @@
<config:config-item config:name="DropCapPunctuation" config:type="boolean">false</config:config-item>
<config:config-item config:name="EmbedLatinScriptFonts" config:type="boolean">true</config:config-item>
<config:config-item config:name="MsWordCompTrailingBlanks" config:type="boolean">false</config:config-item>
<config:config-item config:name="Rsid" config:type="int">7691572</config:config-item>
<config:config-item config:name="Rsid" config:type="int">7570580</config:config-item>
<config:config-item config:name="CharacterCompressionType" config:type="short">0</config:config-item>
<config:config-item config:name="EmbeddedDatabaseName" config:type="string"/>
<config:config-item config:name="ApplyTextAttrToEmptyLineAtEndOfParagraph" config:type="boolean">false</config:config-item>
@@ -1521,7 +1521,7 @@
<table:table-column table:style-name="Table6.C"/>
<table:table-row table:style-name="Table6.1">
<table:table-cell table:style-name="Table6.A1" office:value-type="string">
<text:p text:style-name="P3"><draw:frame draw:style-name="fr1" draw:name="image: ((invoice.company.logo), &apos;image/png&apos;)" text:anchor-type="paragraph" svg:x="0.0083in" svg:y="0.0598in" svg:width="1.4in" svg:height="1.1201in" draw:z-index="6">
<text:p text:style-name="P3"><draw:frame draw:style-name="fr1" draw:name="image: ((invoice.company.logo), &apos;image/png&apos;)" text:anchor-type="paragraph" svg:x="0.0083in" svg:y="0.0598in" svg:width="1.4in" svg:height="1.1201in" draw:z-index="1">
<draw:text-box>
<text:p text:style-name="P4"/>
</draw:text-box>
@@ -1535,7 +1535,7 @@
<text:p text:style-name="P3"/>
</table:table-cell>
<table:table-cell table:style-name="Table6.A1" office:value-type="string">
<text:p text:style-name="P5"><text:line-break/><text:span text:style-name="T1"><text:placeholder text:placeholder-type="text">&lt;invoice.company.rec_name&gt;</text:placeholder></text:span><text:span text:style-name="T2"><text:line-break/></text:span><text:span text:style-name="T3"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.tax_identifier.type_string&gt;</text:placeholder></text:span><text:span text:style-name="T3">:</text:span><text:span text:style-name="T4"> </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.tax_identifier.code&gt;</text:placeholder></text:span><text:span text:style-name="T4"><text:line-break/></text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].street&gt;</text:placeholder></text:span><text:span text:style-name="T4"> <text:s/></text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].name&gt;</text:placeholder></text:span><text:span text:style-name="T4"><text:line-break/></text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].neighborhood.rec_name&gt;</text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].borough.rec_name&gt;</text:placeholder></text:span><text:span text:style-name="T4">, <text:line-break/></text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].subdivision.rec_name&gt;</text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].country.rec_name&gt;</text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].postal_code&gt;</text:placeholder></text:span><text:span text:style-name="T4"><text:line-break/></text:span><text:span text:style-name="T5"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.email&gt;</text:placeholder></text:span><text:span text:style-name="T5"><text:line-break/></text:span><text:span text:style-name="T5"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.website&gt;</text:placeholder></text:span><text:span text:style-name="T5"><text:line-break/></text:span><text:span text:style-name="T5"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.phone&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P5"><text:line-break/><text:span text:style-name="T1"><text:placeholder text:placeholder-type="text">&lt;invoice.company.rec_name&gt;</text:placeholder></text:span><text:span text:style-name="T2"><text:line-break/></text:span><text:span text:style-name="T3"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.tax_identifier.type_string&gt;</text:placeholder></text:span><text:span text:style-name="T3">:</text:span><text:span text:style-name="T4"> </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.tax_identifier.code&gt;</text:placeholder></text:span><text:span text:style-name="T4"><text:line-break/></text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].street_unstructured&gt;</text:placeholder></text:span><text:span text:style-name="T4"><text:line-break/></text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].city&gt;</text:placeholder></text:span><text:span text:style-name="T4">, <text:line-break/></text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].subdivision.rec_name&gt;</text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].country.rec_name&gt;</text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.addresses[0].postal_code&gt;</text:placeholder></text:span><text:span text:style-name="T4"><text:line-break/></text:span><text:span text:style-name="T5"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.email&gt;</text:placeholder></text:span><text:span text:style-name="T5"><text:line-break/></text:span><text:span text:style-name="T5"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.website&gt;</text:placeholder></text:span><text:span text:style-name="T5"><text:line-break/></text:span><text:span text:style-name="T5"><text:placeholder text:placeholder-type="text">&lt;invoice.company.party.phone&gt;</text:placeholder></text:span></text:p>
</table:table-cell>
<table:table-cell table:style-name="Table6.A1" office:value-type="string">
<text:p text:style-name="P6"><text:line-break/><text:placeholder text:placeholder-type="text">&lt;if test=&quot;invoice.type == &apos;in&apos;&quot;&gt;</text:placeholder></text:p>
@@ -1750,7 +1750,7 @@
<text:p text:style-name="P49"><text:placeholder text:placeholder-type="text">&lt;format_currency(line.amount, invoice.party.lang, invoice.currency)&gt;</text:placeholder></text:p>
</table:table-cell>
<table:table-cell table:style-name="Table1.E5" office:value-type="string">
<text:p text:style-name="P50"><text:placeholder text:placeholder-type="text">&lt;format_currency(line.itbis, invoice.party.lang, invoice.currency)&gt;</text:placeholder></text:p>
<text:p text:style-name="P50"><text:placeholder text:placeholder-type="text">&lt;format_currency(line.itbis_invoiced_line, invoice.party.lang, invoice.currency)&gt;</text:placeholder></text:p>
</table:table-cell>
</table:table-row>
<table:table-row>
@@ -2539,7 +2539,7 @@
<text:p text:style-name="P49"><text:placeholder text:placeholder-type="text">&lt;format_currency(line.amount, invoice.party.lang, invoice.currency)&gt;</text:placeholder></text:p>
</table:table-cell>
<table:table-cell table:style-name="Table5.E5" office:value-type="string">
<text:p text:style-name="P50"><text:placeholder text:placeholder-type="text">&lt;format_currency(line.itbis, invoice.party.lang, invoice.currency)&gt;</text:placeholder></text:p>
<text:p text:style-name="P50"><text:placeholder text:placeholder-type="text">&lt;format_currency(line.itbis_invoiced_line, invoice.party.lang, invoice.currency)&gt;</text:placeholder></text:p>
</table:table-cell>
</table:table-row>
<table:table-row>
@@ -2844,7 +2844,7 @@
<text:p text:style-name="P73">Propina:</text:p>
</table:table-cell>
<table:table-cell table:style-name="Table13.B12" office:value-type="string">
<text:p text:style-name="P71"><text:placeholder text:placeholder-type="text">&lt;format_currency(invoice.cdt_invoiced, invoice.party.lang, invoice.currency)&gt;</text:placeholder></text:p>
<text:p text:style-name="P71"><text:placeholder text:placeholder-type="text">&lt;format_currency(invoice.tip, invoice.party.lang, invoice.currency)&gt;</text:placeholder></text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="Table13.1">
@@ -3258,4 +3258,4 @@
<text:p text:style-name="P53"><text:placeholder text:placeholder-type="text">&lt;/if&gt;</text:placeholder></text:p>
</office:text>
</office:body>
</office:document>
</office:document>
+440 -347
View File
File diff suppressed because it is too large Load Diff
-12
View File
@@ -16,18 +16,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">invoice_tree</field>
</record>
<record model="ir.ui.view" id="invoice_line_view_form">
<field name="model">account.invoice.line</field>
<field name="inherit" ref="account_invoice.invoice_line_view_form"/>
<field name="name">invoice_line_form</field>
</record>
<record model="ir.ui.view" id="invoice_line_view_tree">
<field name="model">account.invoice.line</field>
<field name="inherit" ref="account_invoice.invoice_line_view_tree"/>
<field name="name">invoice_line_tree</field>
</record>
<record model="ir.action.report" id="account_invoice.report_invoice">
<field name="name">Factura</field>
<field name="active" eval="True"/>
+711
View File
@@ -0,0 +1,711 @@
#
msgid ""
msgstr ""
"Project-Id-Version: ncf_manager 8.0\n"
"PO-Revision-Date: 2026-07-23 00:00-0400\n"
"Last-Translator: Automatically generated\n"
"Language-Team: Spanish\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgctxt "field:account.configuration.default_ncf_type,default_customer_ncf_type:"
msgid "Default Customer NCF Type"
msgstr "Tipo NCF predeterminado de cliente"
msgctxt "field:account.configuration.default_ncf_type,default_supplier_ncf_type:"
msgid "Default Supplier NCF Type"
msgstr "Tipo NCF predeterminado de proveedor"
msgctxt "field:account.invoice,expense_type:"
msgid "Expense Type"
msgstr "Tipo de gasto"
msgctxt "field:account.invoice,expense_type_string:"
msgid "expense_type"
msgstr "expense_type"
msgctxt "field:account.invoice,expiration_date:"
msgid "Expiration Date"
msgstr "Fecha de vencimiento"
msgctxt "field:account.invoice,id_type:"
msgid "ID Type"
msgstr "Tipo de identificación"
msgctxt "field:account.invoice,isr_withholding_type:"
msgid "ISR Withholding Type"
msgstr "Tipo de retención ISR"
msgctxt "field:account.invoice,isr_withholding_type_string:"
msgid "isr_withholding_type"
msgstr "isr_withholding_type"
msgctxt "field:account.invoice,ncf_cancellation_reason:"
msgid "NCF Cancellation Reason"
msgstr "Motivo de cancelación NCF"
msgctxt "field:account.invoice,ncf_cancellation_type:"
msgid "NCF Cancellation Type"
msgstr "Tipo de cancelación NCF"
msgctxt "field:account.invoice,ncf_cancellation_type_string:"
msgid "ncf_cancellation_type"
msgstr "ncf_cancellation_type"
msgctxt "field:account.invoice,ncf_number:"
msgid "NCF Number"
msgstr "Número NCF"
msgctxt "field:account.invoice,ncf_number_modified:"
msgid "NCF Modified"
msgstr "NCF modificado"
msgctxt "field:account.invoice,ncf_sequence:"
msgid "Tipo NCF"
msgstr "Tipo NCF"
msgctxt "field:account.invoice,ncf_type:"
msgid "NCF Type"
msgstr "Tipo NCF"
msgctxt "field:account.invoice,ncf_type_code:"
msgid "NCF Type Code"
msgstr "Código de tipo NCF"
msgctxt "field:account.invoice,payment_method:"
msgid "Method of Payment"
msgstr "Forma de pago"
msgctxt "field:account.invoice,payment_method_string:"
msgid "payment_method"
msgstr "payment_method"
msgctxt "field:account.invoice,revenue_type:"
msgid "Revenue Type"
msgstr "Tipo de ingreso"
msgctxt "field:account.invoice,revenue_type_string:"
msgid "revenue_type"
msgstr "revenue_type"
msgctxt "field:account.invoice,security_code:"
msgid "Security Code"
msgstr "Código de seguridad"
msgctxt "field:ir.sequence,approved_qty:"
msgid "Approved Quantity"
msgstr "Cantidad aprobada"
msgctxt "field:ir.sequence,expiration_date:"
msgid "Expiration Date"
msgstr "Fecha de vencimiento"
msgctxt "field:ir.sequence,from_number:"
msgid "From Number"
msgstr "Número inicial"
msgctxt "field:ir.sequence,remaining_qty:"
msgid "Remaining Quantity"
msgstr "Cantidad restante"
msgctxt "field:ir.sequence,until_number:"
msgid "Until Number"
msgstr "Número final"
msgctxt "field:ncf.manager,app_date:"
msgid "Application Date"
msgstr "Fecha de solicitud"
msgctxt "field:ncf.manager,app_number:"
msgid "Application Number"
msgstr "Número de solicitud"
msgctxt "field:ncf.manager,auth_number:"
msgid "Authorization Number"
msgstr "Número de autorización"
msgctxt "field:ncf.manager,company:"
msgid "Company"
msgstr "Empresa"
msgctxt "field:ncf.manager,ncf_sequences:"
msgid "pos"
msgstr "pos"
msgctxt "field:ncf.manager,state:"
msgid "State"
msgstr "Estado"
msgctxt "field:ncf.manager,state_string:"
msgid "state"
msgstr "state"
msgctxt "field:ncf.manager.sequence,invoice_type:"
msgid "Invoice Type"
msgstr "Tipo de factura"
msgctxt "field:ncf.manager.sequence,ncf_sequences:"
msgid "NCF Sequences"
msgstr "Secuencias NCF"
msgctxt "field:ncf.manager.sequence,ncf_type:"
msgid "NCF Type"
msgstr "Tipo NCF"
msgctxt "field:ncf.manager.sequence,ncf_type_string:"
msgid "ncf_type"
msgstr "ncf_type"
msgctxt "field:ncf.manager.sequence,pos:"
msgid "Point of Sale"
msgstr "Punto de venta"
msgctxt "field:ncf.manager.sequence,state:"
msgid "State"
msgstr "Estado"
msgctxt "field:ncf.manager.sequence,state_string:"
msgid "state"
msgstr "state"
msgctxt "field:party.party.ncf_type,party:"
msgid "Party"
msgstr "Tercero"
msgctxt "field:res.user,digital_signature:"
msgid "Digital Signature"
msgstr "Firma digital"
msgctxt "field:res.user,z_digital_signature:"
msgid "Digital Signature"
msgstr "Firma digital"
msgctxt "model:account.configuration,string:"
msgid "Account Configuration"
msgstr "Configuración contable"
msgctxt "model:account.configuration.default_ncf_type,string:"
msgid "Account Configuration Default NCF Type"
msgstr "Tipo NCF predeterminado de configuración contable"
msgctxt "model:ir.action,name:account_invoice.report_invoice"
msgid "Invoice"
msgstr "Factura"
msgctxt "model:ir.action,name:act_ncf_manager"
msgid "NCF Manager"
msgstr "Gestor de NCF"
msgctxt "model:ir.message,text:msg_amount_credit_note_negative"
msgid "The Total Amount of the Credit Note must be Negative."
msgstr "El importe total de la nota de crédito debe ser negativo."
msgctxt "model:ir.message,text:msg_amount_invoice_no_negative"
msgid "The Total Amount of the Invoice not must be Negative."
msgstr "El importe total de la factura no debe ser negativo."
msgctxt "model:ir.message,text:msg_duplicated_ncf_number_invoice"
msgid "You are adding an invoice with an already registered NCF."
msgstr "Está agregando una factura con un NCF ya registrado."
msgctxt "model:ir.message,text:msg_inactive_ncf_manager"
msgid "The NCF Manager (point of sale) for invoice \"%(invoice)s\" is inactive."
msgstr "El gestor NCF (punto de venta) de la factura \"%(invoice)s\" está inactivo."
msgctxt "model:ir.message,text:msg_inactive_ncf_sequence"
msgid "The NCF sequence selected for invoice \"%(invoice)s\" is inactive."
msgstr "La secuencia NCF seleccionada para la factura \"%(invoice)s\" está inactiva."
msgctxt "model:ir.message,text:msg_incorrect_lenght_ncf_number_greater_11_under_13"
msgid "You have added the NCF an incorrect characters length greater 11 and under 13."
msgstr "El NCF indicado tiene una longitud incorrecta mayor que 11 y menor que 13 caracteres."
msgctxt "model:ir.message,text:msg_incorrect_lenght_ncf_number_greater_13_under_19"
msgid "You have added the NCF an incorrect characters length greater 13 and under 19."
msgstr "El NCF indicado tiene una longitud incorrecta mayor que 13 y menor que 19 caracteres."
msgctxt "model:ir.message,text:msg_incorrect_lenght_ncf_number_modified"
msgid "You have added an incorrect characters length the modified NCF."
msgstr "El NCF modificado indicado tiene una longitud incorrecta."
msgctxt "model:ir.message,text:msg_incorrect_lenght_ncf_number_under_11"
msgid "You have added the NCF an incorrect characters length under 11."
msgstr "El NCF indicado tiene una longitud incorrecta menor que 11 caracteres."
msgctxt "model:ir.message,text:msg_invalid_encf_number"
msgid "NCF: \"%(number)s\" is not correct or does not correspond to this RNC."
msgstr "El NCF \"%(number)s\" no es correcto o no corresponde a este RNC."
msgctxt "model:ir.message,text:msg_invalid_ncf_number"
msgid "NCF: \"%(number)s\" is not correct or does not correspond to this RNC."
msgstr "El NCF \"%(number)s\" no es correcto o no corresponde a este RNC."
msgctxt "model:ir.message,text:msg_invalid_ncf_sequence_invoice_type"
msgid "The NCF sequence invoice type does not match invoice \"%(invoice)s\"."
msgstr "El tipo de factura de la secuencia NCF no coincide con la factura \"%(invoice)s\"."
msgctxt "model:ir.message,text:msg_invalid_ncf_sequence_type"
msgid "The NCF type on the sequence does not match the NCF type selected on invoice \"%(invoice)s\"."
msgstr "El tipo NCF de la secuencia no coincide con el tipo NCF seleccionado en la factura \"%(invoice)s\"."
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\"."
msgstr "No hay secuencia activa de nota de crédito 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 activa de nota de débito para el tipo \"%(ncf_type)s\" en la empresa \"%(company)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 cancelación DGII antes de cancelar la factura fiscal \"%(invoice)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 cancelación DGII antes de cancelar la factura fiscal \"%(invoice)s\"."
msgctxt "model:ir.message,text:msg_missing_ncf_sequence"
msgid "No active NCF sequence available for invoice \"%(invoice)s\"."
msgstr "No hay secuencia NCF activa disponible para la factura \"%(invoice)s\"."
msgctxt "model:ir.message,text:msg_missing_strict_sequence"
msgid "No strict sequence configured in the selected NCF sequence for invoice \"%(invoice)s\"."
msgstr "No hay secuencia estricta configurada en la secuencia NCF seleccionada para la factura \"%(invoice)s\"."
msgctxt "model:ir.message,text:msg_ncf_running_out"
msgid "There are only \\\"%(remaining_ncf)s\\\" vouchers for this sequence."
msgstr "There are only \\\"%(remaining_ncf)s\\\" vouchers for this sequence."
msgctxt "model:ir.message,text:msg_ncf_sequence_expired"
msgid "This sequence is expired."
msgstr "Esta secuencia está vencida."
msgctxt "model:ir.message,text:msg_ncf_sequence_finished"
msgid "The NCF sequence is exhausted. No more numbers are available."
msgstr "La secuencia NCF está agotada. No hay más números disponibles."
msgctxt "model:ir.message,text:msg_no_expense_type"
msgid "No Expense Type Selected."
msgstr "No se seleccionó tipo de gasto."
msgctxt "model:ir.message,text:msg_no_expiration_date"
msgid "No have added NCF Expiration Date."
msgstr "No se ha indicado fecha de vencimiento del NCF."
msgctxt "model:ir.message,text:msg_no_isr_withholding_type"
msgid "No ISR Withholding Type Selected."
msgstr "No se seleccionó tipo de retención ISR."
msgctxt "model:ir.message,text:msg_no_method_payment"
msgid "No Method Payment Selected."
msgstr "No se seleccionó forma de pago."
msgctxt "model:ir.message,text:msg_no_ncf_number"
msgid "You have not added the NCF Number."
msgstr "No ha indicado el número NCF."
msgctxt "model:ir.message,text:msg_no_ncf_number_modified"
msgid "You have not added the modified NCF."
msgstr "No ha indicado el NCF modificado."
msgctxt "model:ir.message,text:msg_no_ncf_type"
msgid "No NCF Type Selected."
msgstr "No se seleccionó tipo NCF."
msgctxt "model:ir.message,text:msg_no_payment_term"
msgid "No Payment Term Selected."
msgstr "No se seleccionó término de pago."
msgctxt "model:ir.message,text:msg_no_revenue_type"
msgid "No Revenue Type Selected."
msgstr "No se seleccionó tipo de ingreso."
msgctxt "model:ir.message,text:msg_no_security_code"
msgid "You have not added the security code."
msgstr "No ha indicado el código de seguridad."
msgctxt "model:ir.message,text:msg_not_ncf_available"
msgid "There are no more vouchers available for this sequence."
msgstr "No hay más comprobantes disponibles para esta secuencia."
msgctxt "model:ir.message,text:msg_partial_credit_note_refund_not_allowed"
msgid "Partial credit note refund is not allowed."
msgstr "No se permite devolución parcial de nota de crédito."
msgctxt "model:ir.sequence.strict,string:"
msgid "Sequence Strict"
msgstr "Secuencia estricta"
msgctxt "model:ir.sequence.type,name:sequence_type_out_credit_note_04"
msgid "Nota de Crédito"
msgstr "Nota de Crédito"
msgctxt "model:ir.sequence.type,name:sequence_type_out_credit_note_34"
msgid "Nota de Crédito Electrónica"
msgstr "Nota de Crédito Electrónica"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_01"
msgid "Factura de Crédito Fiscal"
msgstr "Factura de Crédito Fiscal"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_02"
msgid "Factura de Consumo"
msgstr "Factura de Consumo"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_03"
msgid "Nota de Débito"
msgstr "Nota de Débito"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_11"
msgid "Comprobante de Compras"
msgstr "Comprobante de Compras"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_12"
msgid "Registro Único de Ingreso"
msgstr "Registro Único de Ingreso"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_13"
msgid "Comprobante para Gasto Menor"
msgstr "Comprobante para Gasto Menor"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_14"
msgid "Comprobante de Regímen Especial"
msgstr "Comprobante de Regímen Especial"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_15"
msgid "Comprobante Gubernamental"
msgstr "Comprobante Gubernamental"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_16"
msgid "Comprobante para exportaciones"
msgstr "Comprobante para exportaciones"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_17"
msgid "Comprobante para Pagos al Exterior"
msgstr "Comprobante para Pagos al Exterior"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_31"
msgid "Factura de Crédito Fiscal Electrónica"
msgstr "Factura de Crédito Fiscal Electrónica"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_32"
msgid "Factura de Consumo Electrónica"
msgstr "Factura de Consumo Electrónica"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_33"
msgid "Nota de Débito Electrónica"
msgstr "Nota de Débito Electrónica"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_41"
msgid "Compras Electrónico"
msgstr "Compras Electrónico"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_43"
msgid "Gastos Menores Electrónico"
msgstr "Gastos Menores Electrónico"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_44"
msgid "Regímenes Especiales Electrónico"
msgstr "Regímenes Especiales Electrónico"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_45"
msgid "Gubernamental Electrónico"
msgstr "Gubernamental Electrónico"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_46"
msgid "Exportación Electrónico"
msgstr "Exportación Electrónico"
msgctxt "model:ir.sequence.type,name:sequence_type_out_invoice_47"
msgid "Pagos al Exterior Electrónico"
msgstr "Pagos al Exterior Electrónico"
msgctxt "model:ir.ui.menu,name:main_menu_ncf_manager"
msgid "NCF Manager"
msgstr "Gestor de NCF"
msgctxt "model:ir.ui.menu,name:menu_ncf_manager"
msgid "NCF Manager"
msgstr "Gestor de NCF"
msgctxt "model:ir.ui.view,name:configuration_view_form"
msgid "configuration_form"
msgstr "configuration_form"
msgctxt "model:ir.ui.view,name:invoice_view_form"
msgid "invoice_form"
msgstr "invoice_form"
msgctxt "model:ir.ui.view,name:invoice_view_tree"
msgid "invoice_tree"
msgstr "invoice_tree"
msgctxt "model:ir.ui.view,name:ncf_manager_sequences_view_form"
msgid "ncf_manager_sequences_form"
msgstr "ncf_manager_sequences_form"
msgctxt "model:ir.ui.view,name:ncf_manager_sequences_view_tree"
msgid "ncf_manager_sequences_tree"
msgstr "ncf_manager_sequences_tree"
msgctxt "model:ir.ui.view,name:ncf_manager_view_form"
msgid "ncf_manager_form"
msgstr "ncf_manager_form"
msgctxt "model:ir.ui.view,name:ncf_manager_view_tree"
msgid "ncf_manager_tree"
msgstr "ncf_manager_tree"
msgctxt "model:ir.ui.view,name:party_view_form"
msgid "party_form"
msgstr "party_form"
msgctxt "model:ir.ui.view,name:sequence_view_form"
msgid "sequence_form"
msgstr "sequence_form"
msgctxt "model:ir.ui.view,name:user_view_form"
msgid "user_form"
msgstr "user_form"
msgctxt "model:ncf.manager,string:"
msgid "NCF Manager"
msgstr "Gestor de NCF"
msgctxt "model:ncf.manager.sequence,string:"
msgid "NCF Manager Sequences"
msgstr "Secuencias del gestor de NCF"
msgctxt "model:party.party.ncf_type,string:"
msgid "Party Ncf Type"
msgstr "Tipo NCF de tercero"
msgctxt "selection:ncf.manager,state:active"
msgid "Active"
msgstr "Activo"
msgctxt "selection:ncf.manager,state:draft"
msgid "Draft"
msgstr "Borrador"
msgctxt "selection:ncf.manager,state:finished"
msgid "Finished"
msgstr "Finalizado"
msgctxt "selection:ncf.manager.sequence,invoice_type:invoice_customer"
msgid "Customer Invoice"
msgstr "Factura de cliente"
msgctxt "selection:ncf.manager.sequence,invoice_type:invoice_supplier"
msgid "Supplier Invoice"
msgstr "Factura de proveedor"
msgctxt "selection:ncf.manager.sequence,state:active"
msgid "Active"
msgstr "Activo"
msgctxt "selection:ncf.manager.sequence,state:draft"
msgid "Draft"
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 "Next Number to Issue"
msgstr "Siguiente número por emitir"
msgctxt "field:ncf.manager.sequence,expiration_date:"
msgid "Expiration Date"
msgstr "Fecha de vencimiento"
msgctxt "field:ncf.manager.sequence,effective_from_number:"
msgid "Authorized From"
msgstr "Desde autorizado"
msgctxt "field:ncf.manager.sequence,effective_until_number:"
msgid "Authorized Until"
msgstr "Hasta autorizado"
msgctxt "field:ncf.manager.sequence,effective_number_next:"
msgid "Next Number to Issue"
msgstr "Siguiente número por emitir"
msgctxt "field:ncf.manager.sequence,effective_expiration_date:"
msgid "Expiration Date"
msgstr "Fecha de vencimiento"
msgctxt "field:ncf.manager.sequence,tree_from_number:"
msgid "Authorized From"
msgstr "Desde autorizado"
msgctxt "field:ncf.manager.sequence,tree_until_number:"
msgid "Authorized Until"
msgstr "Hasta autorizado"
msgctxt "field:ncf.manager.sequence,tree_number_next:"
msgid "Next Number to Issue"
msgstr "Siguiente número por emitir"
msgctxt "field:ncf.manager.sequence,tree_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"
msgctxt "field:ncf.import.start,company:"
msgid "Company"
msgstr "Empresa"
msgctxt "field:ncf.import.start,file_:"
msgid "DGII File"
msgstr "Archivo de la DGII"
msgctxt "field:ncf.import.start,filename:"
msgid "Filename"
msgstr "Nombre del archivo"
msgctxt "field:ncf.import.start,activate:"
msgid "Activate Imported Authorization"
msgstr "Activar autorización importada"
msgctxt "field:ncf.manager,source_filename:"
msgid "Source Filename"
msgstr "Nombre del archivo fuente"
msgctxt "field:ncf.manager,source_hash:"
msgid "Source SHA-256"
msgstr "SHA-256 del archivo fuente"
msgctxt "field:ncf.manager,source_file:"
msgid "DGII Source File"
msgstr "Archivo fuente de la DGII"
msgctxt "field:ncf.manager,imported_by:"
msgid "Imported By"
msgstr "Importado por"
msgctxt "field:ncf.manager,imported_at:"
msgid "Imported At"
msgstr "Fecha de importación"
msgctxt "field:ncf.manager.sequence,auth_number:"
msgid "Authorization Number"
msgstr "Número de autorización"
msgctxt "field:ncf.manager.sequence,authorization_date:"
msgid "Authorization Date"
msgstr "Fecha de autorización"
msgctxt "field:ncf.manager.sequence,dgii_status:"
msgid "DGII Status"
msgstr "Estado en DGII"
msgctxt "field:ncf.manager.sequence,requested_quantity:"
msgid "Requested Quantity"
msgstr "Cantidad solicitada"
msgctxt "field:ncf.manager.sequence,approved_quantity:"
msgid "Approved Quantity"
msgstr "Cantidad aprobada"
msgctxt "field:ncf.manager.sequence,usage_type:"
msgid "Usage Type"
msgstr "Tipo de uso"
msgctxt "field:ncf.manager.sequence,dgii_detail:"
msgid "DGII Detail"
msgstr "Detalle de la DGII"
msgctxt "model:ir.action,name:act_ncf_import"
msgid "Import DGII Authorization"
msgstr "Importar autorización de la DGII"
msgctxt "model:ir.ui.menu,name:menu_ncf_import"
msgid "Import DGII Authorization"
msgstr "Importar autorización de la DGII"
+1044 -474
View File
File diff suppressed because it is too large Load Diff
+198
View File
@@ -0,0 +1,198 @@
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 (Dominican Republic)\n"
"Language: es_DO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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 "Next Number to Issue"
msgstr "Siguiente número por emitir"
msgctxt "field:ncf.manager.sequence,expiration_date:"
msgid "Expiration Date"
msgstr "Fecha de vencimiento"
msgctxt "field:ncf.manager.sequence,effective_from_number:"
msgid "Authorized From"
msgstr "Desde autorizado"
msgctxt "field:ncf.manager.sequence,effective_until_number:"
msgid "Authorized Until"
msgstr "Hasta autorizado"
msgctxt "field:ncf.manager.sequence,effective_number_next:"
msgid "Next Number to Issue"
msgstr "Siguiente número por emitir"
msgctxt "field:ncf.manager.sequence,effective_expiration_date:"
msgid "Expiration Date"
msgstr "Fecha de vencimiento"
msgctxt "field:ncf.manager.sequence,tree_from_number:"
msgid "Authorized From"
msgstr "Desde autorizado"
msgctxt "field:ncf.manager.sequence,tree_until_number:"
msgid "Authorized Until"
msgstr "Hasta autorizado"
msgctxt "field:ncf.manager.sequence,tree_number_next:"
msgid "Next Number to Issue"
msgstr "Siguiente número por emitir"
msgctxt "field:ncf.manager.sequence,tree_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"
msgctxt "field:ncf.import.start,company:"
msgid "Company"
msgstr "Empresa"
msgctxt "field:ncf.import.start,file_:"
msgid "DGII File"
msgstr "Archivo de la DGII"
msgctxt "field:ncf.import.start,filename:"
msgid "Filename"
msgstr "Nombre del archivo"
msgctxt "field:ncf.import.start,activate:"
msgid "Activate Imported Authorization"
msgstr "Activar autorización importada"
msgctxt "field:ncf.manager,source_filename:"
msgid "Source Filename"
msgstr "Nombre del archivo fuente"
msgctxt "field:ncf.manager,source_hash:"
msgid "Source SHA-256"
msgstr "SHA-256 del archivo fuente"
msgctxt "field:ncf.manager,source_file:"
msgid "DGII Source File"
msgstr "Archivo fuente de la DGII"
msgctxt "field:ncf.manager,imported_by:"
msgid "Imported By"
msgstr "Importado por"
msgctxt "field:ncf.manager,imported_at:"
msgid "Imported At"
msgstr "Fecha de importación"
msgctxt "field:ncf.manager.sequence,auth_number:"
msgid "Authorization Number"
msgstr "Número de autorización"
msgctxt "field:ncf.manager.sequence,authorization_date:"
msgid "Authorization Date"
msgstr "Fecha de autorización"
msgctxt "field:ncf.manager.sequence,dgii_status:"
msgid "DGII Status"
msgstr "Estado en DGII"
msgctxt "field:ncf.manager.sequence,requested_quantity:"
msgid "Requested Quantity"
msgstr "Cantidad solicitada"
msgctxt "field:ncf.manager.sequence,approved_quantity:"
msgid "Approved Quantity"
msgstr "Cantidad aprobada"
msgctxt "field:ncf.manager.sequence,usage_type:"
msgid "Usage Type"
msgstr "Tipo de uso"
msgctxt "field:ncf.manager.sequence,dgii_detail:"
msgid "DGII Detail"
msgstr "Detalle de la DGII"
msgctxt "model:ir.action,name:act_ncf_import"
msgid "Import DGII Authorization"
msgstr "Importar autorización de la DGII"
msgctxt "model:ir.ui.menu,name:menu_ncf_import"
msgid "Import DGII Authorization"
msgstr "Importar autorización de la DGII"
msgctxt "wizard_button:ncf.import,start,import_:"
msgid "Import"
msgstr "Importar"
msgctxt "wizard_button:ncf.import,start,end:"
msgid "Cancel"
msgstr "Cancelar"
+121 -22
View File
@@ -69,35 +69,134 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_ncf_sequence_expired">
<field name="text">This sequence is expired.</field>
</record>
<record model="ir.message" id="msg_missing_ncf_sequence">
<field name="text">No active NCF sequence was found for this invoice.</field>
</record>
<record model="ir.message" id="msg_invalid_ncf_sequence_invoice_type">
<field name="text">The selected NCF sequence does not match the invoice type.</field>
</record>
<record model="ir.message" id="msg_invalid_ncf_sequence_type">
<field name="text">The selected NCF sequence does not match the NCF type.</field>
</record>
<record model="ir.message" id="msg_inactive_ncf_sequence">
<field name="text">The selected NCF sequence is inactive.</field>
</record>
<record model="ir.message" id="msg_inactive_ncf_manager">
<field name="text">The selected NCF manager is inactive.</field>
</record>
<record model="ir.message" id="msg_ncf_sequence_finished">
<field name="text">This sequence has no available numbers.</field>
<field name="text">The NCF sequence is exhausted. No more numbers are available.</field>
</record>
<record model="ir.message" id="msg_missing_ncf_sequence">
<field name="text">No active NCF sequence available for invoice "%(invoice)s".</field>
</record>
<record model="ir.message" id="msg_missing_strict_sequence">
<field name="text">The selected NCF configuration has no strict sequence assigned.</field>
</record>
<record model="ir.message" id="msg_partial_credit_note_refund_not_allowed">
<field name="text">Partial credit notes can not be posted as refunds automatically.</field>
<field name="text">No strict sequence configured in the selected NCF sequence for invoice "%(invoice)s".</field>
</record>
<record model="ir.message" id="msg_missing_credit_note_sequence">
<field name="text">No active credit note NCF sequence was found for type "%(ncf_type)s" and company "%(company)s".</field>
<field name="text">No active credit note sequence available for type "%(ncf_type)s" in company "%(company)s".</field>
</record>
<record model="ir.message" id="msg_missing_debit_note_sequence">
<field name="text">No active debit note NCF sequence was found for type "%(ncf_type)s" and 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 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">
<field name="text">The NCF sequence selected for invoice "%(invoice)s" is inactive.</field>
</record>
<record model="ir.message" id="msg_inactive_ncf_manager">
<field name="text">The NCF Manager (point of sale) for invoice "%(invoice)s" is inactive.</field>
</record>
<record model="ir.message" id="msg_invalid_ncf_sequence_invoice_type">
<field name="text">The NCF sequence invoice type does not match invoice "%(invoice)s".</field>
</record>
<record model="ir.message" id="msg_invalid_ncf_sequence_type">
<field name="text">The NCF type on the sequence does not match the NCF type selected on invoice "%(invoice)s".</field>
</record>
<record model="ir.message" id="msg_partial_credit_note_refund_not_allowed">
<field name="text">Partial credit note refund is not allowed.</field>
</record>
<record model="ir.message" id="msg_invalid_ncf_structure">
<field name="text">The fiscal number "%(number)s" must have the DGII B + 10 digits or E + 12 digits structure.</field>
</record>
<record model="ir.message" id="msg_invalid_ncf_type_prefix">
<field name="text">The fiscal number "%(number)s" does not match the expected prefix "%(prefix)s".</field>
</record>
<record model="ir.message" id="msg_ncf_sequence_company_mismatch">
<field name="text">The fiscal sequence and its authorization must belong to the same company.</field>
</record>
<record model="ir.message" id="msg_ncf_sequence_prefix_mismatch">
<field name="text">The fiscal sequence must use prefix "%(prefix)s" for the selected NCF type.</field>
</record>
<record model="ir.message" id="msg_ncf_invalid_padding">
<field name="text">The fiscal sequence must use a padding of %(padding)s digits.</field>
</record>
<record model="ir.message" id="msg_ncf_invalid_range">
<field name="text">The authorized fiscal range must start at 1 or greater and end at or after its start.</field>
</record>
<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>
<record model="ir.message" id="msg_ncf_line_missing_authorization">
<field name="text">Every fiscal sequence line requires its DGII authorization number.</field>
</record>
<record model="ir.message" id="msg_dgii_import_read_error">
<field name="text">The DGII file could not be read: %(error)s.</field>
</record>
<record model="ir.message" id="msg_dgii_import_invalid_file">
<field name="text">The file does not contain the required DGII authorization columns.</field>
</record>
<record model="ir.message" id="msg_dgii_import_not_approved">
<field name="text">DGII authorization "%(authorization)s" is not approved and cannot be imported.</field>
</record>
<record model="ir.message" id="msg_dgii_import_unknown_type">
<field name="text">DGII NCF type "%(type)s" is not supported.</field>
</record>
<record model="ir.message" id="msg_dgii_import_invalid_range">
<field name="text">DGII authorization "%(authorization)s" has an invalid fiscal range or prefix.</field>
</record>
<record model="ir.message" id="msg_dgii_import_quantity_mismatch">
<field name="text">The approved quantity for DGII authorization "%(authorization)s" does not match its range.</field>
</record>
<record model="ir.message" id="msg_dgii_import_company_mismatch">
<field name="text">The DGII file belongs to "%(company)s", which does not match the selected company.</field>
</record>
<record model="ir.message" id="msg_dgii_import_conflict">
<field name="text">DGII authorization "%(authorization)s" already exists with different fiscal data.</field>
</record>
<record model="ir.message" id="msg_dgii_import_closed_request">
<field name="text">DGII request "%(request)s" is no longer in draft and cannot receive another imported authorization.</field>
</record>
<record model="ir.message" id="msg_ncf_authorization_number_duplicate">
<field name="text">DGII authorization number "%(authorization)s" is already registered for this company.</field>
</record>
<record model="ir.message" id="msg_dgii_import_invalid_size">
<field name="text">The DGII import file is empty or exceeds the 5 MB safety limit.</field>
</record>
<record model="ir.message" id="msg_dgii_import_missing_dates">
<field name="text">DGII authorization "%(authorization)s" does not contain the required request and authorization dates.</field>
</record>
</data>
</tryton>
+528 -14
View File
@@ -1,8 +1,14 @@
# 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.model import ModelView, ModelSQL, fields
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, Workflow, fields, Unique
from trytond.pool import Pool
from trytond.pyson import Eval, Id
from trytond.pyson import Eval, If
from trytond.transaction import Transaction
@@ -32,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'
@@ -53,9 +99,16 @@ class NCFManager(ModelSQL, ModelView):
app_number = fields.Char('Application Number', required=True, size=10,
states=_states, depends=_depends)
auth_number = fields.Char('Authorization Number', required=True, size=10,
auth_number = fields.Char('Legacy Authorization Number', size=10,
states=_states, depends=_depends)
source_filename = fields.Char('Source Filename', readonly=True)
source_hash = fields.Char('Source SHA-256', readonly=True)
source_file = fields.Binary(
'DGII Source File', filename='source_filename', readonly=True)
imported_by = fields.Many2One('res.user', 'Imported By', readonly=True)
imported_at = fields.DateTime('Imported At', readonly=True)
ncf_sequences = fields.One2Many('ncf.manager.sequence', 'pos',
'NCF Sequences', context={'company': Eval('company', -1)},
states=_states, depends=_depends)
@@ -64,10 +117,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
@@ -75,7 +135,30 @@ class NCFManager(ModelSQL, ModelView):
super(NCFManager, cls).__setup__()
cls._order = [
('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 = []
if self.company:
parts.append(self.company.rec_name)
if self.app_number:
parts.append(self.app_number)
return ' - '.join(parts) if parts else str(self.id)
@staticmethod
def default_company():
@@ -85,8 +168,91 @@ 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():
raise UserError(gettext(
'ncf_manager.msg_ncf_missing_authorization'))
for line in self.ncf_sequences:
if not line.auth_number:
if self.auth_number:
line.auth_number = self.auth_number
else:
raise UserError(gettext(
'ncf_manager.msg_ncf_line_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,
'source_filename': None,
'source_hash': None,
'source_file': None,
'imported_by': None,
'imported_at': None,
})
return super().copy(managers, default=default)
class NCFManagerSequences(Workflow, ModelSQL, ModelView):
'NCF Manager Sequences'
__name__ = 'ncf.manager.sequence'
@@ -107,11 +273,110 @@ 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)
auth_number = fields.Char(
'Authorization Number', size=10, states=_states, depends=_depends)
authorization_date = fields.DateTime(
'Authorization Date', states=_states, depends=_depends)
dgii_status = fields.Char('DGII Status', readonly=True)
requested_quantity = fields.Integer('Requested Quantity', readonly=True)
approved_quantity = fields.Integer('Approved Quantity', readonly=True)
usage_type = fields.Char('Usage Type', readonly=True)
dgii_detail = fields.Text('DGII Detail', readonly=True)
import_fingerprint = fields.Char('Import Fingerprint', readonly=True)
source_filename = fields.Char('Source Filename', readonly=True)
source_hash = fields.Char('Source SHA-256', readonly=True)
source_file = fields.Binary(
'DGII Source File', filename='source_filename', readonly=True)
imported_by = fields.Many2One('res.user', 'Imported By', readonly=True)
imported_at = fields.DateTime('Imported At', readonly=True)
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)),
If(Eval('state') == 'draft',
('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(
'Next 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'])
effective_from_number = fields.Function(
fields.Integer('Authorized From', states={
'invisible': Eval('sequence_mode') == 'create',
}), 'get_effective_sequence_values')
effective_until_number = fields.Function(
fields.Integer('Authorized Until', states={
'invisible': Eval('sequence_mode') == 'create',
}), 'get_effective_sequence_values')
effective_number_next = fields.Function(
fields.Integer('Next Number to Issue', states={
'invisible': Eval('sequence_mode') == 'create',
}),
'get_effective_sequence_values')
effective_expiration_date = fields.Function(
fields.Date('Expiration Date', states={
'invisible': Eval('sequence_mode') == 'create',
}), 'get_effective_sequence_values')
# Tree-specific values must remain visible for both assisted draft lines
# and lines backed by an activated strict sequence. The effective fields
# above retain their form-only visibility rules.
tree_from_number = fields.Function(
fields.Integer('Authorized From'), 'get_effective_sequence_values')
tree_until_number = fields.Function(
fields.Integer('Authorized Until'), 'get_effective_sequence_values')
tree_number_next = fields.Function(
fields.Integer('Next Number to Issue'),
'get_effective_sequence_values')
tree_expiration_date = fields.Function(
fields.Date('Expiration Date'), 'get_effective_sequence_values')
invoice_type = fields.Selection([
('invoice_customer', 'Customer Invoice'),
@@ -123,17 +388,266 @@ 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
@fields.depends(
'ncf_sequences', 'from_number', 'until_number', 'number_next',
'expiration_date')
def on_change_ncf_sequences(self):
if not self.ncf_sequences:
return
self.from_number = self.ncf_sequences.from_number
self.until_number = self.ncf_sequences.until_number
self.number_next = self.ncf_sequences.number_next
self.expiration_date = self.ncf_sequences.expiration_date
@classmethod
def get_effective_sequence_values(cls, records, names):
values = {name: {} for name in names}
mapping = {
'effective_from_number': 'from_number',
'effective_until_number': 'until_number',
'effective_number_next': 'number_next',
'effective_expiration_date': 'expiration_date',
'tree_from_number': 'from_number',
'tree_until_number': 'until_number',
'tree_number_next': 'number_next',
'tree_expiration_date': 'expiration_date',
}
for record in records:
sequence = record.ncf_sequences
for name in names:
source = mapping[name]
value = (getattr(sequence, source, None) if sequence
else getattr(record, source, None))
values[name][record.id] = value
return values
def get_rec_name(self, name):
if not self.ncf_type_string:
return ''
return self.ncf_type_string.split('-')[0]
@classmethod
def validate_fields(cls, records, field_names):
super().validate_fields(records, field_names)
if field_names & {
'pos', 'ncf_sequences', 'ncf_type', 'auth_number'}:
for record in records:
record._validate_fiscal_sequence()
record._validate_unique_authorization()
def _validate_unique_authorization(self):
if not self.auth_number or not self.pos or not self.pos.company:
return
if self.__class__.search([
('id', '!=', self.id),
('pos.company', '=', self.pos.company.id),
('auth_number', '=', self.auth_number),
], limit=1):
raise UserError(gettext(
'ncf_manager.msg_ncf_authorization_number_duplicate',
authorization=self.auth_number))
def _validate_fiscal_sequence(self):
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 = 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.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),
('invoice_type', '=', self.invoice_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)
+38 -6
View File
@@ -41,15 +41,15 @@ this repository contains the full copyright notices and license terms. -->
id="menu_ncf_manager" action="act_ncf_manager"/>
<record model="ir.model.access" id="access_ncf_manager">
<field name="model" search="[('model', '=', 'ncf.manager')]"/>
<field name="model">ncf.manager</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_ncf_manager_admin">
<field name="model" search="[('model', '=', 'ncf.manager')]"/>
<field name="model">ncf.manager</field>
<field name="group" ref="account.group_account_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
@@ -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>
@@ -70,15 +91,15 @@ this repository contains the full copyright notices and license terms. -->
</record>
<record model="ir.model.access" id="access_ncf_manager_sequences">
<field name="model" search="[('model', '=', 'ncf.manager.sequence')]"/>
<field name="model">ncf.manager.sequence</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_ncf_manager_sequences_admin">
<field name="model" search="[('model', '=', 'ncf.manager.sequence')]"/>
<field name="model">ncf.manager.sequence</field>
<field name="group" ref="account.group_account_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
@@ -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>
+27 -5
View File
@@ -6,6 +6,7 @@ from sql import Literal, Null
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from trytond import backend
from trytond.i18n import gettext
from trytond.model import ModelSQL, ValueMixin, fields
from trytond.modules.company.model import (
@@ -20,6 +21,12 @@ import logging
logger = logging.getLogger(__name__)
def _backend_name():
name = backend.name
return name() if callable(name) else name
customer_ncf_type = fields.Selection(
NCF_TYPE,
"Customer NCF Type",
@@ -35,8 +42,6 @@ supplier_ncf_type = fields.Selection(
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
ncf_types = fields.One2Many(
'party.party.ncf_type', 'party', "NCF Types")
customer_ncf_type = fields.MultiValue(customer_ncf_type)
supplier_ncf_type = fields.MultiValue(supplier_ncf_type)
@@ -49,11 +54,19 @@ class Party(metaclass=PoolMeta):
@classmethod
def default_customer_ncf_type(cls, **pattern):
return None
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
return config.get_multivalue(
'default_customer_ncf_type', **pattern)
@classmethod
def default_supplier_ncf_type(cls, **pattern):
return None
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
return config.get_multivalue(
'default_supplier_ncf_type', **pattern)
class PartyNcfType(ModelSQL, ValueMixin):
@@ -87,6 +100,9 @@ class PartyNcfType(ModelSQL, ValueMixin):
@classmethod
def _ncf_column_exists(cls, cursor, table_name, column_name):
if _backend_name() == 'sqlite':
cursor.execute(f'PRAGMA table_info("{table_name}")')
return any(row[1] == column_name for row in cursor.fetchall())
cursor.execute("""
SELECT 1
FROM information_schema.columns
@@ -97,6 +113,12 @@ class PartyNcfType(ModelSQL, ValueMixin):
@classmethod
def _ncf_column_udt_name(cls, cursor, table_name, column_name):
if _backend_name() == 'sqlite':
cursor.execute(f'PRAGMA table_info("{table_name}")')
for row in cursor.fetchall():
if row[1] == column_name:
return (row[2] or '').lower()
return None
cursor.execute("""
SELECT udt_name
FROM information_schema.columns
@@ -131,7 +153,7 @@ class PartyNcfType(ModelSQL, ValueMixin):
udt_name = cls._ncf_column_udt_name(
cursor, table_name, old_column)
if udt_name in {'int2', 'int4', 'int8'}:
if udt_name in {'int2', 'int4', 'int8', 'integer'}:
logger.info(
"Renaming legacy %s.%s integer column to %s before "
"Tryton creates the new Selection column",
-20
View File
@@ -1,20 +0,0 @@
from trytond.pool import PoolMeta
class InvoiceReport(metaclass=PoolMeta):
"""Add the NCF label required by the fiscal invoice template."""
__name__ = 'account.invoice'
@classmethod
def get_context(cls, records, header, data):
context = super().get_context(records, header, data)
invoice = context['invoice']
context['ncf_type_name'] = cls._get_ncf_type_name(invoice)
return context
@staticmethod
def _get_ncf_type_name(invoice):
if not invoice.ncf_type:
return ''
return invoice.__class__._ncf_type_label_map().get(
invoice.ncf_type, '')
+73 -142
View File
@@ -1,18 +1,43 @@
# 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 string import Template
import time
from sql import Literal, For
import unicodedata
from trytond.exceptions import UserError
from trytond.model.exceptions import ValidationError
from trytond.model import ModelView, ModelSQL, DeactivableMixin, fields, Check
from trytond.pyson import Eval, And
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
from trytond.model import fields
from trytond.pyson import Eval
from trytond.pool import PoolMeta
from trytond.i18n import gettext
from trytond import backend
# Maps sequence_type.name → (prefix, padding)
_NCF_TYPE_MAP = {
'Factura de Crédito Fiscal': ('B01', 8),
'Factura de Consumo': ('B02', 8),
'Nota de Débito': ('B03', 8),
'Nota de Crédito': ('B04', 8),
'Comprobante de Compras': ('B11', 8),
'Registro Único de Ingreso': ('B12', 8),
'Comprobante para Gasto Menor': ('B13', 8),
'Comprobante de Regímen Especial': ('B14', 8),
'Comprobante Gubernamental': ('B15', 8),
'Comprobante para exportaciones': ('B16', 8),
'Comprobante para Pagos al Exterior': ('B17', 8),
'Factura de Crédito Fiscal Electrónica': ('E31', 10),
'Factura de Consumo Electrónica': ('E32', 10),
'Nota de Débito Electrónica': ('E33', 10),
'Nota de Crédito Electrónica': ('E34', 10),
'Compras Electrónico': ('E41', 10),
'Gastos Menores Electrónico': ('E43', 10),
'Regímenes Especiales Electrónico': ('E44', 10),
'Gubernamental Electrónico': ('E45', 10),
'Exportación Electrónico': ('E46', 10),
'Pagos al Exterior Electrónico': ('E47', 10),
}
NCF_PREFIXES = tuple(prefix for prefix, _padding in _NCF_TYPE_MAP.values())
def _sequence_type_entry(sequence_type):
name = unicodedata.normalize('NFC', sequence_type.name or '')
return _NCF_TYPE_MAP.get(name)
class Sequence(metaclass=PoolMeta):
@@ -64,146 +89,23 @@ class Sequence(metaclass=PoolMeta):
@fields.depends('sequence_type')
def on_change_with_name(self, name=None):
if not self.sequence_type or not self.sequence_type.name:
return None
if self.sequence_type.name == 'Factura de Crédito Fiscal':
return 'Factura de Crédito Fiscal'
elif self.sequence_type.name == 'Factura de Consumo':
return 'Factura de Consumo'
elif self.sequence_type.name == 'Nota de Débito':
return 'Nota de Débito'
elif self.sequence_type.name == 'Nota de Crédito':
return 'Nota de Crédito'
elif self.sequence_type.name == 'Comprobante de Compras':
return 'Comprobante de Compras'
elif self.sequence_type.name == 'Registro Único de Ingreso':
return 'Registro Único de Ingreso'
elif self.sequence_type.name == 'Comprobante para Gasto Menor':
return 'Comprobante para Gasto Menor'
elif self.sequence_type.name == 'Comprobante de Regímen Especial':
return 'Comprobante de Regímen Especial'
elif self.sequence_type.name == 'Comprobante Gubernamental':
return 'Comprobante Gubernamental'
elif self.sequence_type.name == 'Comprobante para exportaciones':
return 'Comprobante para exportaciones'
elif self.sequence_type.name == 'Comprobante para Pagos al Exterior':
return 'Comprobante para Pagos al Exterior'
elif self.sequence_type.name == 'Factura de Crédito Fiscal Electrónica':
return 'Factura de Crédito Fiscal Electrónica'
elif self.sequence_type.name == 'Factura de Consumo Electrónica':
return 'Factura de Consumo Electrónica'
elif self.sequence_type.name == 'Nota de Débito Electrónica':
return 'Nota de Débito Electrónica'
elif self.sequence_type.name == 'Nota de Crédito Electrónica':
return 'Nota de Crédito Electrónica'
elif self.sequence_type.name == 'Compras Electrónico':
return 'Compras Electrónico'
elif self.sequence_type.name == 'Gastos Menores Electrónico':
return 'Gastos Menores Electrónico'
elif self.sequence_type.name == 'Regímenes Especiales Electrónico':
return 'Regímenes Especiales Electrónico'
elif self.sequence_type.name == 'Gubernamental Electrónico':
return 'Gubernamental Electrónico'
elif self.sequence_type.name == 'Exportación Electrónico':
return 'Exportación Electrónico'
elif self.sequence_type.name == 'Pagos al Exterior Electrónico':
return 'Pagos al Exterior Electrónico'
if self.sequence_type:
return self.sequence_type.name
return None
@fields.depends('sequence_type')
def on_change_with_prefix(self, name=None):
if not self.sequence_type or not self.sequence_type.name:
return None
if self.sequence_type.name == 'Factura de Crédito Fiscal':
return 'B01'
elif self.sequence_type.name == 'Factura de Consumo':
return 'B02'
elif self.sequence_type.name == 'Nota de Débito':
return 'B03'
elif self.sequence_type.name == 'Nota de Crédito':
return 'B04'
elif self.sequence_type.name == 'Comprobante de Compras':
return 'B11'
elif self.sequence_type.name == 'Registro Único de Ingreso':
return 'B12'
elif self.sequence_type.name == 'Comprobante para Gasto Menor':
return 'B13'
elif self.sequence_type.name == 'Comprobante de Regímen Especial':
return 'B14'
elif self.sequence_type.name == 'Comprobante Gubernamental':
return 'B15'
elif self.sequence_type.name == 'Comprobante para exportaciones':
return 'B16'
elif self.sequence_type.name == 'Comprobante para Pagos al Exterior':
return 'B17'
elif self.sequence_type.name == 'Factura de Crédito Fiscal Electrónica':
return 'E31'
elif self.sequence_type.name == 'Factura de Consumo Electrónica':
return 'E32'
elif self.sequence_type.name == 'Nota de Débito Electrónica':
return 'E33'
elif self.sequence_type.name == 'Nota de Crédito Electrónica':
return 'E34'
elif self.sequence_type.name == 'Compras Electrónico':
return 'E41'
elif self.sequence_type.name == 'Gastos Menores Electrónico':
return 'E43'
elif self.sequence_type.name == 'Regímenes Especiales Electrónico':
return 'E44'
elif self.sequence_type.name == 'Gubernamental Electrónico':
return 'E45'
elif self.sequence_type.name == 'Exportación Electrónico':
return 'E46'
elif self.sequence_type.name == 'Pagos al Exterior Electrónico':
return 'E47'
return None
entry = _sequence_type_entry(self.sequence_type)
return entry[0] if entry else None
@fields.depends('sequence_type')
def on_change_with_padding(self, name=None):
if not self.sequence_type or not self.sequence_type.name:
return 0
if self.sequence_type.name == 'Factura de Crédito Fiscal':
return 8
elif self.sequence_type.name == 'Factura de Consumo':
return 8
elif self.sequence_type.name == 'Nota de Débito':
return 8
elif self.sequence_type.name == 'Nota de Crédito':
return 8
elif self.sequence_type.name == 'Comprobante de Compras':
return 8
elif self.sequence_type.name == 'Registro Único de Ingreso':
return 8
elif self.sequence_type.name == 'Comprobante para Gasto Menor':
return 8
elif self.sequence_type.name == 'Comprobante de Regímen Especial':
return 8
elif self.sequence_type.name == 'Comprobante Gubernamental':
return 8
elif self.sequence_type.name == 'Comprobante para exportaciones':
return 8
elif self.sequence_type.name == 'Comprobante para Pagos al Exterior':
return 8
elif self.sequence_type.name == 'Factura de Crédito Fiscal Electrónica':
return 10
elif self.sequence_type.name == 'Factura de Consumo Electrónica':
return 10
elif self.sequence_type.name == 'Nota de Débito Electrónica':
return 10
elif self.sequence_type.name == 'Nota de Crédito Electrónica':
return 10
elif self.sequence_type.name == 'Compras Electrónico':
return 10
elif self.sequence_type.name == 'Gastos Menores Electrónico':
return 10
elif self.sequence_type.name == 'Regímenes Especiales Electrónico':
return 10
elif self.sequence_type.name == 'Gubernamental Electrónico':
return 10
elif self.sequence_type.name == 'Exportación Electrónico':
return 10
elif self.sequence_type.name == 'Pagos al Exterior Electrónico':
return 10
entry = _sequence_type_entry(self.sequence_type)
return entry[1] if entry else 0
@fields.depends('from_number', 'until_number')
def on_change_with_approved_qty(self, name=None):
@@ -212,12 +114,41 @@ class Sequence(metaclass=PoolMeta):
@fields.depends('number_next', 'until_number')
def on_change_with_remaining_qty(self, name=None):
if self.until_number and self.number_next:
if self.until_number is not None and self.number_next is not None:
return self.until_number - self.number_next + 1
@classmethod
def validate_fields(cls, records, field_names):
super().validate_fields(records, field_names)
if field_names & {
'prefix', 'padding', 'from_number', 'until_number',
'number_next'}:
for record in records:
record._validate_ncf_range()
def _validate_ncf_range(self):
if self.prefix not in NCF_PREFIXES:
return
expected_padding = 10 if self.prefix.startswith('E') else 8
if self.padding != expected_padding:
raise UserError(gettext(
'ncf_manager.msg_ncf_invalid_padding',
padding=expected_padding))
if self.from_number is None or self.until_number is None:
return
if self.from_number < 1 or self.until_number < self.from_number:
raise UserError(gettext('ncf_manager.msg_ncf_invalid_range'))
if (self.number_next is not None
and self.number_next < self.from_number):
raise UserError(gettext('ncf_manager.msg_ncf_next_before_range'))
class SequenceStrict(Sequence):
"Sequence Strict"
__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)
-6
View File
@@ -9,12 +9,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">sequence_form</field>
</record>
<!-- <record model="ir.ui.view" id="sequence_view_tree">
<field name="model">ir.sequence</field>
<field name="inherit" ref="ir.sequence_view_tree"/>
<field name="name">sequence_list</field>
</record> -->
<!-- NCF Sequences Types -->
<record model="ir.sequence.type" id="sequence_type_out_invoice_01">
+7 -1
View File
@@ -6,7 +6,12 @@ from configparser import ConfigParser
from setuptools import find_packages, setup
MODULE2PREFIX = {}
MODULE2PREFIX = {
'account_do': 'unpaismejor',
'account_credit_note': 'unpaismejor',
'account_debit_note': 'unpaismejor',
'account_invoice_do': 'unpaismejor',
}
def read(fname):
@@ -71,6 +76,7 @@ setup(name=name,
version=version,
description='This module implements the administration and management of tax receipt numbers.',
long_description=read('README.rst'),
long_description_content_type='text/x-rst',
author='Solutema SRL',
author_email='info@solutema.com',
url='https://www.solutema.com/',
+809 -17
View File
@@ -1,32 +1,824 @@
import datetime
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
import unittest
from trytond.tests.test_tryton import ModuleTestCase
from trytond.modules.ncf_manager.report import InvoiceReport
from trytond.exceptions import UserError
from trytond.modules.ncf_manager.sequence import Sequence
from trytond.modules.ncf_manager.invoice import Invoice
from trytond.modules.ncf_manager.importer import import_dgii_authorizations
from trytond.pool import Pool
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.transaction import Transaction
class NcfManagerTestCase(ModuleTestCase):
"Test Ncf Manager module"
module = 'ncf_manager'
def test_fiscal_report_overrides_single_invoice_action(self):
report_xml = (Path(__file__).parent.parent / 'invoice.xml').read_text()
self.assertIn('id="account_invoice.report_invoice"', report_xml)
self.assertIn('ncf_manager/invoice.fodt', report_xml)
self.assertNotIn('ir.action.keyword', report_xml)
def _create_company(self):
pool = Pool()
Company = pool.get('company.company')
Currency = pool.get('currency.currency')
Party = pool.get('party.party')
def test_fiscal_report_resolves_ncf_label(self):
class Invoice:
ncf_type = '31'
currency, = Currency.create([{
'name': 'Dominican Peso',
'code': 'DOP',
'symbol': 'RD$',
'digits': 2,
'rounding': '0.01',
}])
party, = Party.create([{
'name': 'Empresa NCF',
'addresses': [('create', [{}])],
}])
company, = Company.create([{
'party': party.id,
'currency': currency.id,
}])
return company
@staticmethod
def _ncf_type_label_map():
return {'31': 'Factura de Crédito Fiscal'}
def _create_chart(self, company):
pool = Pool()
Account = pool.get('account.account')
AccountTemplate = pool.get('account.account.template')
CreateChart = pool.get('account.create_chart', type='wizard')
Journal = pool.get('account.journal')
ModelData = pool.get('ir.model.data')
session_id, _start, _end = CreateChart.create()
chart = CreateChart(session_id)
chart.account.account_template = AccountTemplate(ModelData.get_id(
'account_do', 'do_account_root_en'))
chart.account.company = company
chart.transition_create_account()
receivable, = Account.search([
('company', '=', company.id),
('code', '=', '110201'),
], limit=1)
payable, = Account.search([
('company', '=', company.id),
('code', '=', '210101'),
], limit=1)
revenue, = Account.search([
('company', '=', company.id),
('code', '=', '4102'),
], limit=1)
chart.properties.company = company
chart.properties.account_receivable = receivable
chart.properties.account_payable = payable
with Transaction().set_context(company=company.id):
chart.transition_create_properties()
CreateChart.delete(session_id)
journal, = Journal.search([('type', '=', 'revenue')], limit=1)
return receivable, revenue, journal
def _create_fiscalyear(self, company, today=None):
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
StrictSequence = pool.get('ir.sequence.strict')
SequenceType = pool.get('ir.sequence.type')
today = today or datetime.date.today()
year = today.year
move_type, = SequenceType.search([
('name', '=', 'Account Move'),
], limit=1)
invoice_type, = SequenceType.search([
('name', '=', 'Invoice'),
], limit=1)
move_sequence, = StrictSequence.create([{
'name': 'FY 2026 Moves',
'sequence_type': move_type.id,
'company': company.id,
}])
invoice_sequence, = StrictSequence.create([{
'name': 'FY 2026 Invoices',
'sequence_type': invoice_type.id,
'company': company.id,
}])
fiscalyear, = FiscalYear.create([{
'name': str(year),
'start_date': datetime.date(year, 1, 1),
'end_date': datetime.date(year, 12, 31),
'company': company.id,
'move_sequence': move_sequence.id,
}])
fiscalyear.invoice_sequences = [InvoiceSequence(
company=company,
in_invoice_sequence=invoice_sequence,
in_credit_note_sequence=invoice_sequence,
out_invoice_sequence=invoice_sequence,
out_credit_note_sequence=invoice_sequence)]
fiscalyear.save()
FiscalYear.create_period([fiscalyear])
def _create_payment_term(self):
PaymentTerm = Pool().get('account.invoice.payment_term')
term, = PaymentTerm.create([{
'name': 'Contado',
'lines': [('create', [{'type': 'remainder'}])],
}])
return term
def _create_ncf_sequence(self, company, today=None):
pool = Pool()
ModelData = pool.get('ir.model.data')
Manager = pool.get('ncf.manager')
ManagerSequence = pool.get('ncf.manager.sequence')
StrictSequence = pool.get('ir.sequence.strict')
SequenceType = pool.get('ir.sequence.type')
today = today or datetime.date.today()
sequence_type = SequenceType(ModelData.get_id(
'ncf_manager', 'sequence_type_out_invoice_01'))
strict_sequence, = StrictSequence.create([{
'name': 'B01 Factura Credito Fiscal',
'sequence_type': sequence_type.id,
'company': company.id,
'prefix': 'B01',
'padding': 8,
'number_next': 1,
'number_increment': 1,
'from_number': 1,
'until_number': 2,
'expiration_date': datetime.date(today.year, 12, 31),
}])
manager, = Manager.create([{
'company': company.id,
'app_date': datetime.date(today.year, 1, 1),
'app_number': '0000000001',
'auth_number': '0000000001',
'state': 'active',
}])
with Transaction().set_context(company=company.id):
sequence, = ManagerSequence.create([{
'pos': manager.id,
'ncf_type': '1',
'ncf_sequences': strict_sequence.id,
'invoice_type': 'invoice_customer',
'state': 'active',
}])
return sequence
def _create_active_e31_range(self, company, invoice_type,
from_number=1, until_number=100):
pool = Pool()
Manager = pool.get('ncf.manager')
ManagerSequence = pool.get('ncf.manager.sequence')
ModelData = pool.get('ir.model.data')
StrictSequence = pool.get('ir.sequence.strict')
SequenceType = pool.get('ir.sequence.type')
today = datetime.date.today()
sequence_type = SequenceType(ModelData.get_id(
'ncf_manager', 'sequence_type_out_invoice_31'))
strict_sequence, = StrictSequence.create([{
'name': f'E31 {invoice_type}',
'sequence_type': sequence_type.id,
'company': company.id,
'prefix': 'E31',
'padding': 10,
'number_next': from_number,
'number_increment': 1,
'from_number': from_number,
'until_number': until_number,
'expiration_date': datetime.date(today.year + 1, 12, 31),
}])
manager, = Manager.create([{
'company': company.id,
'app_date': today,
'app_number': 'APP0000001',
'auth_number': 'AUT0000001',
'state': 'active',
}])
line, = ManagerSequence.create([{
'pos': manager.id,
'ncf_type': '31',
'ncf_sequences': strict_sequence.id,
'invoice_type': invoice_type,
'state': 'active',
}])
return line
@staticmethod
def _dgii_xml(ncf_type='31', authorization='6005423851',
start='E310000000001', end='E310000000100', quantity='100'):
return f'''<SolicitudNCF>
<Solicitud_6009912073 xmlns="http://tempuri.org/dsCFiscales.xsd">
<NUM_SOL>6009912073</NUM_SOL>
<NUM_AUTOR>{authorization}</NUM_AUTOR>
<FECHA_AUTOR>2026-08-07T13:45:02-04:00</FECHA_AUTOR>
<COMP>FACTURA ELECTRÓNICA</COMP>
<OBSERVACIONES>Empresa NCF</OBSERVACIONES>
<NUMERO_DESDE>{start}</NUMERO_DESDE>
<NUMERO_HASTA>{end}</NUMERO_HASTA>
<CODTIPO>{ncf_type}</CODTIPO>
<FECHA_SOLICITUD>2026-08-07T13:36:23-04:00</FECHA_SOLICITUD>
<ESTATUS>APROBADA</ESTATUS>
<CANTIDAD_SOLICITADA>{quantity}</CANTIDAD_SOLICITADA>
<TIPO_USO>ELECTRÓNICA</TIPO_USO>
<CANTIDAD_APROBADA>{quantity}</CANTIDAD_APROBADA>
<FECHA_VIGENCIA>2027-12-31T00:00:00-04:00</FECHA_VIGENCIA>
<DETALLE_LOG>Autorizada por DGII</DETALLE_LOG>
</Solicitud_6009912073>
</SolicitudNCF>'''.encode()
@with_transaction()
def test_customer_invoice_post_assigns_b01_ncf(self):
'Test full NCF assignment workflow on posted customer invoice'
pool = Pool()
Invoice = pool.get('account.invoice')
Party = pool.get('party.party')
today = datetime.date.today()
company = self._create_company()
receivable, revenue, journal = self._create_chart(company)
self._create_fiscalyear(company, today=today)
payment_term = self._create_payment_term()
ncf_sequence = self._create_ncf_sequence(company, today=today)
party, = Party.create([{
'name': 'Cliente NCF',
'addresses': [('create', [{}])],
}])
with Transaction().set_context(
company=company.id, date=today):
invoice, = Invoice.create([{
'type': 'out',
'company': company.id,
'party': party.id,
'invoice_address': party.addresses[0].id,
'account': receivable.id,
'currency': company.currency.id,
'invoice_date': today,
'accounting_date': today,
'journal': journal.id,
'payment_term': payment_term.id,
'ncf_type': '1',
'ncf_sequence': ncf_sequence.id,
'revenue_type': '01',
'payment_method': '04',
'lines': [('create', [{
'type': 'line',
'description': 'Servicio',
'quantity': 1,
'unit_price': Decimal('500.00'),
'account': revenue.id,
}])],
}])
Invoice.post([invoice])
invoice = Invoice(invoice.id)
self.assertEqual(invoice.state, 'posted')
self.assertEqual(invoice.ncf_number, 'B0100000001')
self.assertEqual(invoice.ncf_sequence, ncf_sequence)
self.assertEqual(
InvoiceReport._get_ncf_type_name(Invoice()),
'Factura de Crédito Fiscal')
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.effective_from_number, 100)
self.assertEqual(line.effective_until_number, 109)
self.assertEqual(line.effective_number_next, 100)
self.assertEqual(
line.effective_expiration_date,
datetime.date(today.year, 12, 31))
self.assertEqual(line.tree_from_number, 100)
self.assertEqual(line.tree_until_number, 109)
self.assertEqual(line.tree_number_next, 100)
self.assertEqual(
line.tree_expiration_date,
datetime.date(today.year, 12, 31))
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)
@with_transaction()
def test_overlap_is_independent_by_invoice_direction(self):
'A supplier range must not block the company outgoing authorization'
pool = Pool()
Manager = pool.get('ncf.manager')
company = self._create_company()
today = datetime.date.today()
supplier_line = self._create_active_e31_range(
company, 'invoice_supplier', until_number=2_000_000_000)
manager, = Manager.create([{
'company': company.id,
'app_date': today,
'app_number': '6009912073',
'auth_number': '6005423851',
'ncf_sequences': [('create', [{
'invoice_type': 'invoice_customer',
'ncf_type': '31',
'sequence_mode': 'create',
'from_number': 1,
'number_next': 1,
'until_number': 100,
'expiration_date': datetime.date(
today.year + 1, 12, 31),
}])],
}])
Manager.activate([manager])
manager = Manager(manager.id)
self.assertEqual(manager.state, 'active')
self.assertEqual(manager.ncf_sequences[0].state, 'active')
self.assertEqual(supplier_line.state, 'active')
@with_transaction()
def test_overlap_still_blocks_same_invoice_direction(self):
'Two outgoing authorizations may not reserve the same E31 numbers'
pool = Pool()
Manager = pool.get('ncf.manager')
company = self._create_company()
today = datetime.date.today()
self._create_active_e31_range(
company, 'invoice_customer', from_number=1, until_number=100)
manager, = Manager.create([{
'company': company.id,
'app_date': today,
'app_number': '6009912073',
'auth_number': '6005423851',
'ncf_sequences': [('create', [{
'invoice_type': 'invoice_customer',
'ncf_type': '31',
'sequence_mode': 'create',
'from_number': 50,
'number_next': 50,
'until_number': 150,
'expiration_date': datetime.date(
today.year + 1, 12, 31),
}])],
}])
with self.assertRaises(UserError):
Manager.activate([manager])
@with_transaction()
def test_finish_preserves_bound_legacy_migration_sequence(self):
'Finishing must not invalidate an immutable historical sequence'
pool = Pool()
Manager = pool.get('ncf.manager')
StrictSequence = pool.get('ir.sequence.strict')
company = self._create_company()
line = self._create_ncf_sequence(company)
strict_sequence = line.ncf_sequences
StrictSequence.write([strict_sequence], {
'prefix': 'MIG1',
'padding': 9,
})
Manager.finish([line.pos])
line = line.__class__(line.id)
self.assertEqual(line.state, 'finished')
self.assertEqual(line.ncf_sequences.id, strict_sequence.id)
self.assertEqual(line.ncf_sequences.prefix, 'MIG1')
@with_transaction()
def test_import_dgii_authorization_is_idempotent(self):
pool = Pool()
Manager = pool.get('ncf.manager')
company = self._create_company()
data = self._dgii_xml()
managers, lines = import_dgii_authorizations(
company, data, 'authorization.xml')
self.assertEqual(len(managers), 1)
self.assertEqual(len(lines), 1)
manager = managers[0]
line = lines[0]
self.assertEqual(manager.app_number, '6009912073')
self.assertEqual(line.auth_number, '6005423851')
self.assertEqual(line.ncf_type, '31')
self.assertEqual(line.from_number, 1)
self.assertEqual(line.until_number, 100)
self.assertEqual(line.expiration_date, datetime.date(2027, 12, 31))
self.assertEqual(line.tree_from_number, 1)
self.assertEqual(line.tree_until_number, 100)
self.assertEqual(line.tree_number_next, 1)
self.assertEqual(
line.tree_expiration_date, datetime.date(2027, 12, 31))
managers, lines = import_dgii_authorizations(
company, data, 'authorization-again.xml')
self.assertEqual(managers, [])
self.assertEqual(lines, [])
self.assertEqual(Manager.search_count([
('app_number', '=', '6009912073')]), 1)
@with_transaction()
def test_import_new_authorization_for_active_request(self):
'A new NUM_AUTOR may reuse NUM_SOL without reopening the active one'
pool = Pool()
Manager = pool.get('ncf.manager')
Line = pool.get('ncf.manager.sequence')
company = self._create_company()
first_data = self._dgii_xml()
first_managers, _ = import_dgii_authorizations(
company, first_data, 'authorization-31.xml', activate=True)
first_manager, = first_managers
self.assertEqual(Manager(first_manager.id).state, 'active')
second_data = self._dgii_xml(
ncf_type='34', authorization='6005423852',
start='E340000000001', end='E340000001000', quantity='1000')
second_managers, second_lines = import_dgii_authorizations(
company, second_data, 'authorization-34.xml')
self.assertEqual(len(second_managers), 1)
self.assertEqual(len(second_lines), 1)
self.assertNotEqual(second_managers[0].id, first_manager.id)
self.assertEqual(second_managers[0].state, 'draft')
self.assertEqual(second_lines[0].auth_number, '6005423852')
self.assertEqual(Manager(first_manager.id).state, 'active')
self.assertEqual(Manager.search_count([
('company', '=', company.id),
('app_number', '=', '6009912073'),
]), 2)
self.assertEqual(Line.search_count([
('pos.company', '=', company.id),
('auth_number', 'in', ['6005423851', '6005423852']),
]), 2)
managers, lines = import_dgii_authorizations(
company, second_data, 'authorization-34-again.xml')
self.assertEqual(managers, [])
self.assertEqual(lines, [])
del ModuleTestCase
class NcfManagerUnitTestCase(unittest.TestCase):
@staticmethod
def invoice_with_identifier(code, type=None, code_compact=None):
identifier = SimpleNamespace(code=code, type=type)
if code_compact is not None:
identifier.code_compact = code_compact
return SimpleNamespace(
party=object(), party_tax_identifier=identifier)
def test_id_type_accepts_formatted_rnc(self):
invoice = self.invoice_with_identifier('1-01-01063-2')
self.assertEqual(Invoice.get_id_type(invoice, 'id_type'), 1)
def test_id_type_accepts_formatted_cedula(self):
invoice = self.invoice_with_identifier('001-1391820-5')
self.assertEqual(Invoice.get_id_type(invoice, 'id_type'), 2)
def test_id_type_uses_dominican_identifier_type(self):
rnc = self.invoice_with_identifier('formatted', type='do_vat')
cedula = self.invoice_with_identifier('formatted', type='do_cedula')
self.assertEqual(Invoice.get_id_type(rnc, 'id_type'), 1)
self.assertEqual(Invoice.get_id_type(cedula, 'id_type'), 2)
def test_id_type_normalizes_migrated_compact_code(self):
invoice = self.invoice_with_identifier(
'display value', code_compact='001-1391820-5')
self.assertEqual(Invoice.get_id_type(invoice, 'id_type'), 2)
def test_id_type_returns_none_without_identifier(self):
invoice = SimpleNamespace(party=object(), party_tax_identifier=None)
self.assertIsNone(Invoice.get_id_type(invoice, 'id_type'))
def test_invoice_report_uses_current_tax_field_names(self):
path = Path(__file__).parents[1] / 'invoice.fodt'
template = path.read_text(encoding='utf-8')
self.assertNotIn('line.itbis_line', template)
self.assertEqual(template.count('line.itbis_invoiced_line'), 2)
self.assertEqual(template.count('if test=&quot;invoice.tip'), 2)
self.assertEqual(template.count('format_currency(invoice.tip'), 2)
def test_sequence_defaults_match_ncf_type(self):
sequence = SimpleNamespace(
sequence_type=SimpleNamespace(name='Factura de Crédito Fiscal'))
self.assertEqual(Sequence.on_change_with_prefix(sequence), 'B01')
self.assertEqual(Sequence.on_change_with_padding(sequence), 8)
def test_sequence_approved_and_remaining_quantities(self):
sequence = SimpleNamespace(
from_number=1,
until_number=10,
number_next=4)
self.assertEqual(Sequence.on_change_with_approved_qty(sequence), 10)
self.assertEqual(Sequence.on_change_with_remaining_qty(sequence), 7)
def test_ncf_structure_and_normalization(self):
invoice = SimpleNamespace(
ncf_number=' b0100000001 ', ncf_type='1')
Invoice._ncf_validate_number(invoice)
self.assertEqual(invoice.ncf_number, 'B0100000001')
invoice.ncf_number = 'E310000000001'
invoice.ncf_type = '31'
Invoice._ncf_validate_number(invoice)
def test_ncf_rejects_invalid_structure_or_type(self):
invoice = SimpleNamespace(ncf_number='B010001', ncf_type='1')
with self.assertRaises(UserError):
Invoice._ncf_validate_number(invoice)
def test_legacy_supplier_ncf_is_preserved_for_migration(self):
invoice = SimpleNamespace(
type='in', ncf_number='A010010010100000001', ncf_type='1')
Invoice._ncf_validate_number(invoice)
self.assertEqual(invoice.ncf_number, 'A010010010100000001')
invoice.ncf_number = 'B0200000001'
with self.assertRaises(UserError):
Invoice._ncf_validate_number(invoice)
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):
original_enabled = Invoice._ncf_electronic_invoicing_enabled
Invoice._ncf_electronic_invoicing_enabled = classmethod(
lambda cls: False)
try:
physical = SimpleNamespace(ncf_type='1')
electronic = SimpleNamespace(ncf_type='31')
self.assertEqual(
Invoice._ncf_resolve_debit_note_code(physical), '3')
self.assertEqual(
Invoice._ncf_resolve_debit_note_code(electronic), '3')
finally:
Invoice._ncf_electronic_invoicing_enabled = original_enabled
def test_debit_note_preparation_uses_physical_without_electronic_module(self):
electronic_sequence = SimpleNamespace(ncf_type='33')
physical_sequence = SimpleNamespace(ncf_type='3')
original_find_active_sequence = Invoice._ncf_find_active_sequence
original_enabled = Invoice._ncf_electronic_invoicing_enabled
def find_sequence(cls, invoice, ncf_code):
if ncf_code == '33':
return electronic_sequence
if ncf_code == '3':
return physical_sequence
return None
Invoice._ncf_find_active_sequence = classmethod(find_sequence)
Invoice._ncf_electronic_invoicing_enabled = classmethod(
lambda cls: False)
try:
origin = SimpleNamespace(
ncf_number='B0100000001',
ncf_type='1',
payment_method='04',
revenue_type='01',
isr_withholding_type=None)
debit_note = SimpleNamespace(
company=SimpleNamespace(rec_name='Solutema'),
type='out',
ncf_number_modified=None,
ncf_type=None,
ncf_sequence=None,
payment_method=None,
revenue_type=None,
isr_withholding_type=None)
Invoice._ncf_prepare_debit_note_from_origin(origin, debit_note)
finally:
Invoice._ncf_find_active_sequence = original_find_active_sequence
Invoice._ncf_electronic_invoicing_enabled = original_enabled
self.assertEqual(debit_note.ncf_number_modified, 'B0100000001')
self.assertEqual(debit_note.ncf_type, '3')
self.assertEqual(debit_note.ncf_sequence, physical_sequence)
self.assertEqual(debit_note.payment_method, '04')
self.assertEqual(debit_note.revenue_type, '01')
def test_debit_note_preparation_prefers_electronic_when_module_is_active(self):
electronic_sequence = SimpleNamespace(ncf_type='33')
original_find_active_sequence = Invoice._ncf_find_active_sequence
original_enabled = Invoice._ncf_electronic_invoicing_enabled
def find_sequence(cls, invoice, ncf_code):
return electronic_sequence if ncf_code == '33' else None
Invoice._ncf_find_active_sequence = classmethod(find_sequence)
Invoice._ncf_electronic_invoicing_enabled = classmethod(
lambda cls: True)
try:
origin = SimpleNamespace(
ncf_number='B0100000001',
ncf_type='1',
payment_method='04',
revenue_type='01',
isr_withholding_type=None)
debit_note = SimpleNamespace(
company=SimpleNamespace(rec_name='Solutema'),
type='out',
ncf_number_modified=None,
ncf_type=None,
ncf_sequence=None,
payment_method=None,
revenue_type=None,
isr_withholding_type=None)
Invoice._ncf_prepare_debit_note_from_origin(origin, debit_note)
finally:
Invoice._ncf_find_active_sequence = original_find_active_sequence
Invoice._ncf_electronic_invoicing_enabled = original_enabled
self.assertEqual(debit_note.ncf_number_modified, 'B0100000001')
self.assertEqual(debit_note.ncf_type, '33')
self.assertEqual(debit_note.ncf_sequence, electronic_sequence)
def test_debit_note_preparation_falls_back_to_physical_sequence(self):
physical_sequence = SimpleNamespace(ncf_type='3')
original_find_active_sequence = Invoice._ncf_find_active_sequence
original_enabled = Invoice._ncf_electronic_invoicing_enabled
def find_sequence(cls, invoice, ncf_code):
return physical_sequence if ncf_code == '3' else None
Invoice._ncf_find_active_sequence = classmethod(find_sequence)
Invoice._ncf_electronic_invoicing_enabled = classmethod(
lambda cls: True)
try:
origin = SimpleNamespace(
ncf_number='B0100000001',
ncf_type='1',
payment_method='04',
revenue_type='01',
isr_withholding_type=None)
debit_note = SimpleNamespace(
company=SimpleNamespace(rec_name='Solutema'),
type='out',
ncf_number_modified=None,
ncf_type=None,
ncf_sequence=None,
payment_method=None,
revenue_type=None,
isr_withholding_type=None)
Invoice._ncf_prepare_debit_note_from_origin(origin, debit_note)
finally:
Invoice._ncf_find_active_sequence = original_find_active_sequence
Invoice._ncf_electronic_invoicing_enabled = original_enabled
self.assertEqual(debit_note.ncf_number_modified, 'B0100000001')
self.assertEqual(debit_note.ncf_type, '3')
self.assertEqual(debit_note.ncf_sequence, physical_sequence)
def test_credit_note_preparation_uses_physical_for_b_origin(self):
electronic_sequence = SimpleNamespace(ncf_type='34')
physical_sequence = SimpleNamespace(ncf_type='4')
original_find_active_sequence = Invoice._ncf_find_active_sequence
original_enabled = Invoice._ncf_electronic_invoicing_enabled
def find_sequence(cls, invoice, ncf_code):
if ncf_code == '34':
return electronic_sequence
if ncf_code == '4':
return physical_sequence
return None
Invoice._ncf_find_active_sequence = classmethod(find_sequence)
Invoice._ncf_electronic_invoicing_enabled = classmethod(
lambda cls: True)
try:
origin = SimpleNamespace(
ncf_number='B0100000001',
ncf_type='1')
credit_note = SimpleNamespace(
company=SimpleNamespace(rec_name='Solutema'),
type='out',
ncf_number_modified=None,
ncf_type=None,
ncf_sequence=None,
payment_method=None,
revenue_type=None)
Invoice._ncf_prepare_credit_note_from_origin(origin, credit_note)
finally:
Invoice._ncf_find_active_sequence = original_find_active_sequence
Invoice._ncf_electronic_invoicing_enabled = original_enabled
self.assertEqual(credit_note.ncf_number_modified, 'B0100000001')
self.assertEqual(credit_note.ncf_type, '4')
self.assertEqual(credit_note.ncf_sequence, physical_sequence)
self.assertEqual(credit_note.payment_method, '06')
def test_modified_ncf_validation_applies_to_debit_notes(self):
debit_note = SimpleNamespace(
ncf_type='3',
ncf_number_modified='B0100000001',
type='out')
Invoice.check_ncf_number_modified(debit_note)
def test_fiscal_classification_validation_order_for_customer_invoice(self):
calls = []
invoice = SimpleNamespace(
type='out',
check_revenue_type=lambda: calls.append('revenue_type'),
check_expense_type=lambda: calls.append('expense_type'),
check_isr_withholding_type=lambda: calls.append(
'isr_withholding_type'),
check_payment_method=lambda: calls.append('payment_method'))
Invoice._ncf_check_fiscal_classification(invoice)
self.assertEqual(calls, [
'revenue_type',
'isr_withholding_type',
'payment_method',
])
def test_fiscal_classification_validation_order_for_supplier_invoice(self):
calls = []
invoice = SimpleNamespace(
type='in',
check_revenue_type=lambda: calls.append('revenue_type'),
check_expense_type=lambda: calls.append('expense_type'),
check_isr_withholding_type=lambda: calls.append(
'isr_withholding_type'),
check_payment_method=lambda: calls.append('payment_method'))
Invoice._ncf_check_fiscal_classification(invoice)
self.assertEqual(calls, [
'expense_type',
'isr_withholding_type',
'payment_method',
])
+94
View File
@@ -0,0 +1,94 @@
from types import SimpleNamespace
import inspect
import unittest
from trytond.modules.ncf_manager.ncf_manager import (
NCFManager, NCFManagerSequences)
from trytond.modules.ncf_manager.sequence import Sequence
from trytond.modules.ncf_manager.importer import (
normalize_dgii_row, parse_dgii_authorizations)
from trytond.modules.ncf_manager.invoice import Invoice
class NcfManagerUnitTestCase(unittest.TestCase):
HEADERS = (
'NUM_SOL,NUM_AUTOR,FECHA_AUTOR,COMP,OBSERVACIONES,NUMERO_DESDE,'
'NUMERO_HASTA,CANTIDAD,CODTIPO,FECHA_SOLICITUD,ESTATUS,'
'CANTIDAD_SOLICITADA,TIPO_USO,RNC_IMPRENTA,IMPRENTA,'
'CANTIDAD_APROBADA,FECHA_VIGENCIA,DETALLE_LOG')
ROW = (
'6009912073,6005423851,07/08/2026 13:45:02,CRÉDITO ELECTRÓNICO,'
'SOLUTEMA SRL,E310000000001,E310000000100,,31,'
'07/08/2026 13:36:23,APROBADA,1000,ELECTRÓNICA,,,100,'
'31/12/2027 0:00:00,Aviso, con comas')
def test_manager_defaults_and_rec_name(self):
self.assertEqual(NCFManager.default_state(), 'draft')
manager = SimpleNamespace()
manager.id = 42
manager.company = SimpleNamespace(rec_name='ACME')
manager.app_number = 'APP001'
self.assertEqual(NCFManager.get_rec_name(manager, 'name'), 'ACME - APP001')
def test_sequence_defaults_and_rec_name(self):
self.assertEqual(NCFManagerSequences.default_state(), 'draft')
sequence = SimpleNamespace()
sequence.ncf_type_string = 'Factura de Credito Fiscal - B01'
self.assertEqual(
NCFManagerSequences.get_rec_name(sequence, 'name'),
'Factura de Credito Fiscal ')
def test_ir_sequence_on_change_values_for_ncf_type(self):
sequence = SimpleNamespace()
sequence.sequence_type = SimpleNamespace(name='Factura de Consumo')
self.assertEqual(Sequence.on_change_with_prefix(sequence), 'B02')
self.assertEqual(Sequence.on_change_with_padding(sequence), 8)
self.assertEqual(Sequence.on_change_with_name(sequence), 'Factura de Consumo')
def test_ir_sequence_quantities(self):
sequence = SimpleNamespace()
sequence.from_number = 10
sequence.until_number = 20
sequence.number_next = 12
self.assertEqual(Sequence.on_change_with_approved_qty(sequence), 11)
self.assertEqual(Sequence.on_change_with_remaining_qty(sequence), 9)
def test_unknown_ncf_type_has_empty_sequence_defaults(self):
sequence = SimpleNamespace()
sequence.sequence_type = SimpleNamespace(name='Unknown')
self.assertIsNone(Sequence.on_change_with_prefix(sequence))
self.assertEqual(Sequence.on_change_with_padding(sequence), 0)
def test_decomposed_electronic_sequence_name_is_supported(self):
sequence = SimpleNamespace()
sequence.sequence_type = SimpleNamespace(name='Compras Electro\u0301nico')
self.assertEqual(Sequence.on_change_with_prefix(sequence), 'E41')
self.assertEqual(Sequence.on_change_with_padding(sequence), 10)
def test_amount_searcher_filters_the_requested_product_type(self):
source = inspect.getsource(
Invoice._search_amount_invoiced_by_product_type)
self.assertIn('template.type == product_type', source)
self.assertIn("line.type == 'line'", source)
def test_csv_parser_preserves_unquoted_detail(self):
source_format, rows = parse_dgii_authorizations(
f'{self.HEADERS}\r\n{self.ROW}\r\n'.encode())
self.assertEqual(source_format, 'csv')
self.assertEqual(rows[0]['NUM_AUTOR'], '6005423851')
self.assertEqual(rows[0]['DETALLE_LOG'], 'Aviso, con comas')
normalized = normalize_dgii_row(rows[0])
self.assertEqual(normalized['from_number'], 1)
self.assertEqual(normalized['until_number'], 100)
def test_html_xls_parser(self):
cells = ''.join(f'<th>{header}</th>'
for header in self.HEADERS.split(','))
values = self.ROW.split(',', len(self.HEADERS.split(',')) - 1)
row = ''.join(f'<td>{value}</td>' for value in values)
source_format, rows = parse_dgii_authorizations(
f'<div><table><tr>{cells}</tr><tr>{row}</tr></table></div>'.encode())
self.assertEqual(source_format, 'xls_html')
self.assertEqual(rows[0]['CODTIPO'], '31')
+1 -1
View File
@@ -1,5 +1,5 @@
[tox]
envlist = {py37,py38,py39,py310}-{sqlite,postgresql}
envlist = {py311,py312,py313}-{sqlite,postgresql}
[testenv]
extras = test
+7 -2
View File
@@ -1,11 +1,16 @@
[tryton]
version=7.0.2
version=7.0.3
depends:
account_do
account_invoice_do
account_credit_note
account_debit_note
xml:
sequence.xml
configuration.xml
ncf_manager.xml
importer.xml
invoice.xml
party.xml
party.xml
user.xml
message.xml
+17
View File
@@ -0,0 +1,17 @@
from trytond.model import fields
from trytond.pool import PoolMeta
class User(metaclass=PoolMeta):
__name__ = 'res.user'
digital_signature = fields.Binary(
'Digital Signature', help='Signature image used on invoice reports.')
# Alias used by invoice.fodt template
z_digital_signature = fields.Function(
fields.Binary('Digital Signature'),
'get_z_digital_signature')
def get_z_digital_signature(self, name):
return self.digital_signature
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<tryton>
<data>
<record model="ir.ui.view" id="user_view_form">
<field name="model">res.user</field>
<field name="inherit" ref="res.user_view_form"/>
<field name="name">user_form</field>
</record>
</data>
</tryton>
+26 -7
View File
@@ -4,6 +4,8 @@ this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/page[@id='invoice']/group[@id='taxes_amount_state']/group[@id='amount_state']/group[@id='amount']/label[@name='tax_amount']" position="replace"/>
<xpath expr="/form/notebook/page[@id='invoice']/group[@id='taxes_amount_state']/group[@id='amount_state']/group[@id='amount']/field[@name='tax_amount']" position="replace"/>
<xpath expr="/form/notebook/page[@id='invoice']/group[@id='taxes_amount_state']/group[@id='amount_state']/group[@id='amount']/label[@name='source_tax_amount']" position="replace"/>
<xpath expr="/form/notebook/page[@id='invoice']/group[@id='taxes_amount_state']/group[@id='amount_state']/group[@id='amount']/field[@name='source_tax_amount']" position="replace"/>
<xpath expr="/form/field[@name='reference']" position="after">
<newline/>
@@ -24,19 +26,29 @@ this repository contains the full copyright notices and license terms. -->
</xpath>
<xpath expr="/form/notebook/page[@id='invoice']/group[@id='taxes_amount_state']/group[@id='amount_state']/group[@id='amount']/field[@name='untaxed_amount']" position="after">
<label name="itbis_invoiced" xalign="1.0" xexpand="1"/>
<xpath expr="/form/notebook/page[@id='invoice']/group[@id='taxes_amount_state']/group[@id='amount_state']/group[@id='amount']/field[@name='source_untaxed_amount']" position="after">
<newline/>
<label name="itbis_invoiced" xalign="1.0" xexpand="1" xfill="0"/>
<field name="itbis_invoiced" xalign="1.0" xexpand="0"/>
<label name="itbis_withholding" xalign="1.0" xexpand="1"/>
<newline/>
<label name="itbis_withholding" xalign="1.0" xexpand="1" xfill="0"/>
<field name="itbis_withholding" xalign="1.0" xexpand="0"/>
<label name="isc_invoiced" xalign="1.0" xexpand="1"/>
<newline/>
<label name="itbis_withholding_acquirer" xalign="1.0" xexpand="1" xfill="0"/>
<field name="itbis_withholding_acquirer" xalign="1.0" xexpand="0"/>
<newline/>
<label name="isc_invoiced" xalign="1.0" xexpand="1" xfill="0"/>
<field name="isc_invoiced" xalign="1.0" xexpand="0"/>
<label name="cdt_invoiced" xalign="1.0" xexpand="1"/>
<newline/>
<label name="cdt_invoiced" xalign="1.0" xexpand="1" xfill="0"/>
<field name="cdt_invoiced" xalign="1.0" xexpand="0"/>
<label name="isr_withholding" xalign="1.0" xexpand="1"/>
<newline/>
<label name="isr_withholding" xalign="1.0" xexpand="1" xfill="0"/>
<field name="isr_withholding" xalign="1.0" xexpand="0"/>
<label name="tip" xalign="1.0" xexpand="1"/>
<newline/>
<label name="tip" xalign="1.0" xexpand="1" xfill="0"/>
<field name="tip" xalign="1.0" xexpand="0"/>
<newline/>
</xpath>
<xpath expr="/form/notebook/page[@id='payment']" position="after">
@@ -82,5 +94,12 @@ this repository contains the full copyright notices and license terms. -->
<newline/>
<newline/>
</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>
</data>
+10
View File
@@ -9,4 +9,14 @@ this repository contains the full copyright notices and license terms. -->
<field name="ncf_number"/>
<field name="ncf_number_modified"/>
</xpath>
<xpath
expr="/tree/field[@name=&quot;untaxed_amount&quot;]"
position="after">
<field name="itbis_invoiced" optional="1"/>
<field name="itbis_withholding" optional="1"/>
<field name="isc_invoiced" optional="1"/>
<field name="cdt_invoiced" optional="1"/>
<field name="isr_withholding" optional="1"/>
<field name="tip" optional="1"/>
</xpath>
</data>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="company"/>
<field name="company"/>
<label name="filename"/>
<field name="filename"/>
<label name="file_"/>
<field name="file_"/>
<newline/>
<label name="activate"/>
<field name="activate"/>
<label string="Keep activation disabled if any number may already have been issued outside Tryton."
id="activation_warning" colspan="4"/>
</form>
+37 -10
View File
@@ -3,15 +3,42 @@
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"/>
</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="source_filename"/>
<field name="source_filename"/>
<label name="source_hash"/>
<field name="source_hash"/>
<label name="source_file"/>
<field name="source_file"/>
<newline/>
<label name="imported_by"/>
<field name="imported_by"/>
<label name="imported_at"/>
<field name="imported_at"/>
<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>
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="invoice_type"/>
<field name="invoice_type"/>
<label name="ncf_type"/>
<field name="ncf_type"/>
<label name="ncf_sequences"/>
<field name="ncf_sequences"/>
<newline/>
<label name="state"/>
<field name="state"/>
</form>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="invoice_type"/>
<field name="ncf_type" xexpand="1"/>
<field name="ncf_sequences" xexpand="1"/>
<field name="state"/>
</tree>
+52 -1
View File
@@ -4,11 +4,62 @@ this repository contains the full copyright notices and license terms. -->
<form>
<label name="invoice_type"/>
<field name="invoice_type"/>
<label name="auth_number"/>
<field name="auth_number"/>
<label name="authorization_date"/>
<field name="authorization_date"/>
<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="effective_from_number"/>
<field name="effective_from_number"/>
<label name="effective_until_number"/>
<field name="effective_until_number"/>
<label name="effective_number_next"/>
<field name="effective_number_next"/>
<label name="effective_expiration_date"/>
<field name="effective_expiration_date"/>
<label name="dgii_status"/>
<field name="dgii_status"/>
<label name="requested_quantity"/>
<field name="requested_quantity"/>
<label name="approved_quantity"/>
<field name="approved_quantity"/>
<label name="usage_type"/>
<field name="usage_type"/>
<separator name="dgii_detail" colspan="4"/>
<field name="dgii_detail" colspan="4"/>
<label name="source_filename"/>
<field name="source_filename"/>
<label name="source_hash"/>
<field name="source_hash"/>
<label name="source_file"/>
<field name="source_file"/>
<label name="imported_by"/>
<field name="imported_by"/>
<label name="imported_at"/>
<field name="imported_at"/>
<label name="state"/>
<field 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>
+8
View File
@@ -3,7 +3,15 @@
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="invoice_type"/>
<field name="auth_number"/>
<field name="ncf_type"/>
<field name="expected_prefix"/>
<field name="ncf_sequences"/>
<field name="tree_from_number"/>
<field name="tree_until_number"/>
<field name="tree_number_next"/>
<field name="tree_expiration_date"/>
<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>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<!-- <data>
<xpath
expr="/tree/field[@name=&quot;type&quot;]"
position="after">
<field name="until_number"/>
</xpath>
</data> -->
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<data>
<xpath expr="//notebook" position="inside">
<page string="Firma Digital" id="page_signature">
<label name="digital_signature"/>
<field name="digital_signature" widget="image"/>
</page>
</xpath>
</data>