Homologate fiscal management with Tryton 8.0
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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']
|
||||
@@ -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
@@ -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), 'image/png')" 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), 'image/png')" 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"><invoice.company.rec_name></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"><invoice.company.party.tax_identifier.type_string></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"><invoice.company.party.tax_identifier.code></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"><invoice.company.party.addresses[0].street></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"><invoice.company.party.addresses[0].name></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"><invoice.company.party.addresses[0].neighborhood.rec_name></text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text"><invoice.company.party.addresses[0].borough.rec_name></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"><invoice.company.party.addresses[0].subdivision.rec_name></text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text"><invoice.company.party.addresses[0].country.rec_name></text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text"><invoice.company.party.addresses[0].postal_code></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"><invoice.company.party.email></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"><invoice.company.party.website></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"><invoice.company.party.phone></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"><invoice.company.rec_name></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"><invoice.company.party.tax_identifier.type_string></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"><invoice.company.party.tax_identifier.code></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"><invoice.company.party.addresses[0].street_unstructured></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"><invoice.company.party.addresses[0].city></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"><invoice.company.party.addresses[0].subdivision.rec_name></text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text"><invoice.company.party.addresses[0].country.rec_name></text:placeholder></text:span><text:span text:style-name="T4">. </text:span><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text"><invoice.company.party.addresses[0].postal_code></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"><invoice.company.party.email></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"><invoice.company.party.website></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"><invoice.company.party.phone></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"><if test="invoice.type == 'in'"></text:placeholder></text:p>
|
||||
@@ -1750,7 +1750,7 @@
|
||||
<text:p text:style-name="P49"><text:placeholder text:placeholder-type="text"><format_currency(line.amount, invoice.party.lang, invoice.currency)></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"><format_currency(line.itbis, invoice.party.lang, invoice.currency)></text:placeholder></text:p>
|
||||
<text:p text:style-name="P50"><text:placeholder text:placeholder-type="text"><format_currency(line.itbis_invoiced_line, invoice.party.lang, invoice.currency)></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"><format_currency(line.amount, invoice.party.lang, invoice.currency)></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"><format_currency(line.itbis, invoice.party.lang, invoice.currency)></text:placeholder></text:p>
|
||||
<text:p text:style-name="P50"><text:placeholder text:placeholder-type="text"><format_currency(line.itbis_invoiced_line, invoice.party.lang, invoice.currency)></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"><format_currency(invoice.cdt_invoiced, invoice.party.lang, invoice.currency)></text:placeholder></text:p>
|
||||
<text:p text:style-name="P71"><text:placeholder text:placeholder-type="text"><format_currency(invoice.tip, invoice.party.lang, invoice.currency)></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"></if></text:placeholder></text:p>
|
||||
</office:text>
|
||||
</office:body>
|
||||
</office:document>
|
||||
</office:document>
|
||||
|
||||
+440
-347
File diff suppressed because it is too large
Load Diff
-12
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+198
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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="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',
|
||||
])
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = {py37,py38,py39,py310}-{sqlite,postgresql}
|
||||
envlist = {py311,py312,py313}-{sqlite,postgresql}
|
||||
|
||||
[testenv]
|
||||
extras = test
|
||||
|
||||
+7
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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="untaxed_amount"]"
|
||||
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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="type"]"
|
||||
position="after">
|
||||
<field name="until_number"/>
|
||||
</xpath>
|
||||
</data> -->
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user