Files
ncf_manager/invoice.py
T

1838 lines
63 KiB
Python
Executable File

# -*- coding: utf-8 -*-
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from datetime import date
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from sql.functions import Round
from trytond import backend
from trytond.model import ModelView, Workflow, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, If, And, Bool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.modules.ncf_manager.ncf_manager import NCF_TYPE
from trytond.modules.currency.fields import Monetary
import logging
logger = logging.getLogger(__name__)
def _backend_name():
name = backend.name
return name() if callable(name) else name
_REVENUE_TYPE = [
('', ''),
('01', "01 - Ingresos por Operaciones (No Financieros)"),
('02', "02 - Ingresos Financieros"),
('03', "03 - Ingresos Extraordinarios"),
('04', "04 - Ingresos por Arrendamientos"),
('05', "05 - Ingresos por Venta de Activo Depreciable"),
('06', "06 - Otros Ingresos"),
]
_EXPENSE_TYPE = [
('', ''),
('01', "01 - Gastos de Personal"),
('02', "02 - Gastos por Trabajos, Suministros y Servicios"),
('03', "03 - Arrendamientos"),
('04', "04 - Gastos de Activos Fijo"),
('05', "05 - Gastos de Representación"),
('06', "06 - Otras Deducciones Admitidas"),
('07', "07 - Gastos Financieros"),
('08', "08 - Gastos Extraordinarios"),
('09', "09 - Compras y Gastos que forman parte del Costo de Venta"),
('10', "10 - Adquisiciones de Activos"),
('11', "11 - Gastos de Seguros"),
]
_PAYMENT_METHOD = [
('', ''),
('01', "01 - Efectivo"),
('02', "02 - Cheques/Transferencias/Depósito"),
('03', "03 - Tarjeta Crédito/Débito"),
('04', "04 - Compra a Crédito"),
('05', "05 - Permuta"),
('06', "06 - Nota de Crédito"),
('07', "07 - Mixto"),
]
_ISR_WITHHOLDING_TYPE = [
('', ''),
('01', "01 - Alquileres"),
('02', "02 - Honorarios por Servicios"),
('03', "03 - Otras Rentas"),
('04', "04 - Otras Rentas (Rentas Presuntas)"),
('05', "05 - Intereses Pagados a Personas Jurídicas Residentes"),
('06', "06 - Intereses Pagados a Personas Físicas Residentes"),
('07', "07 - Retención por Proveedores del Estado"),
('08', "08 - Juegos Telefónicos"),
('09', "09 - Retenciones Subsector Ganadería de Carne Bovina"),
]
_CANCELLATION_TYPE = [
('', ''),
('01', "01 Deterioro de Factura Pre-Impresa"),
('02', "02 Errores de Impresión (Factura Pre-Impresa)"),
('03', "03 Impresión Defectuosa"),
('04', "04 Corrección de la Información"),
('05', "05 Cambio de Productos"),
('06', "06 Devolución de Productos"),
('07', "07 Omisión de Productos"),
('08', "08 Errores en Secuencias de NCF"),
('09', "09 Por Cese de Operaciones"),
('10', "10 Pérdida O Hurto De Talonario(S)"),
]
_ZERO = Decimal('0.00')
_DEBIT_NOTE_CODES = ('3', '33')
_CREDIT_NOTE_CODES = ('4', '34')
_MODIFIED_NOTE_CODES = _DEBIT_NOTE_CODES + _CREDIT_NOTE_CODES
_ECF_ORIGIN_CODES = (
'31', '32', '33', '34',
'41', '43', '44', '45', '46', '47',
)
_ECF_CODES = [
'31', '32', '33', '34',
'41', '43', '44', '45', '46', '47',
]
_ELECTRONIC_INVOICE_MODULES = (
'account_invoice_electronic',
'account_invoice_electronic_do',
'dgii_electronic_invoice',
'electronic_invoice_do',
'ncf_electronic',
)
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
ncf_type = fields.Selection(
NCF_TYPE,
'NCF Type',
sort=False,
states={
'required': And(Eval('type') == 'out', Eval('state') != 'draft'),
'readonly': True,
},
depends=['state', 'type', 'company'],
help='Fiscal receipt type resolved from the selected NCF sequence.'
)
ncf_sequence = fields.Many2One('ncf.manager.sequence', 'Tipo NCF',
domain=[
If(
Eval('state') == 'draft',
[
('state', '=', 'active'),
If(
Eval('type') == 'out',
('invoice_type', '=', 'invoice_customer'),
('invoice_type', '=', 'invoice_supplier')
)
],
[]
)
],
states={
'required': And(Eval('type') == 'out', Eval('state') != 'draft'),
'readonly': Eval('state') != 'draft',
},
depends=['state', 'type'])
ncf_type_code = fields.Function(fields.Char('NCF Type Code'),
'on_change_with_ncf_type_code')
ncf_number = fields.Char('NCF Number', size=19,
states={
'readonly': (Eval('type') == 'out') | (Eval('state') != 'draft'),
},
depends=['state', 'type'])
ncf_number_modified = fields.Char('NCF Modified', size=19,
states={
'readonly': Eval('state') != 'draft',
'invisible': ~Eval('ncf_type_code').in_(['3', '33', '4', '34']),
}, depends=['state'])
security_code = fields.Char(
'Security Code',
states={
'readonly': Eval('state') != 'draft',
'invisible': ~Eval('ncf_type').in_(_ECF_CODES),
},
depends=['state', 'ncf_type'],
)
itbis_invoiced = fields.Function(Monetary(
"ITBIS Invoiced", currency='currency', digits='currency',
states={
'invisible': ~Eval('itbis_invoiced', 0),
}), 'on_change_with_itbis_invoiced', searcher='search_itbis_invoiced')
itbis_withholding = fields.Function(Monetary(
"ITBIS Withholding", currency='currency', digits='currency',
states={
'invisible': ~Eval('itbis_withholding', 0),
}), 'on_change_with_itbis_withholding', searcher='search_itbis_withholding')
itbis_withholding_acquirer = fields.Function(Monetary(
"ITBIS Withholding Acquirer", currency='currency', digits='currency',
states={
'invisible': ~Eval('itbis_withholding_acquirer', 0),
}), 'on_change_with_itbis_withholding_acquirer',
searcher='search_itbis_withholding_acquirer')
isc_invoiced = fields.Function(Monetary(
"ISC Invoiced", currency='currency', digits='currency',
states={
'invisible': ~Eval('isc_invoiced', 0),
}), 'on_change_with_isc_invoiced', searcher='search_isc_invoiced')
cdt_invoiced = fields.Function(Monetary(
"CDT Invoiced", currency='currency', digits='currency',
states={
'invisible': ~Eval('cdt_invoiced', 0),
}), 'on_change_with_cdt_invoiced', searcher='search_cdt_invoiced')
isr_withholding = fields.Function(Monetary(
"ISR Withholding", currency='currency', digits='currency',
states={
'invisible': ~Eval('isr_withholding', 0),
}), 'on_change_with_isr_withholding', searcher='search_isr_withholding')
tip = fields.Function(Monetary(
"Tip", currency='currency', digits='currency',
states={
'invisible': ~Eval('tip', 0),
}), 'on_change_with_tip', searcher='search_tip')
id_type = fields.Function(fields.Integer('ID Type',
help='The identifier type of party in the invoice.'),
'get_id_type')
expiration_date = fields.Date('Expiration Date')
amount_invoiced_services = fields.Function(Monetary(
"Amount Invoiced in Services", currency='currency', digits='currency'),
'get_amount_invoiced_services', searcher='search_amount_invoiced_services')
amount_invoiced_goods = fields.Function(Monetary(
"Amount Invoiced in Goods", currency='currency', digits='currency'),
'get_amount_invoiced_goods', searcher='search_amount_invoiced_goods')
revenue_type = fields.Selection(_REVENUE_TYPE, 'Revenue Type',
states={
'readonly': Eval('state').in_(['posted', 'paid', 'cancelled']),
'invisible': Eval('type') == 'in',
},
depends=['state'])
revenue_type_string = revenue_type.translated('revenue_type')
expense_type = fields.Selection(_EXPENSE_TYPE, 'Expense Type',
states={
'readonly': Eval('state').in_(['posted', 'paid', 'cancelled']),
'invisible': Eval('type') == 'out',
},
depends=['state'])
expense_type_string = expense_type.translated('expense_type')
payment_method = fields.Selection(_PAYMENT_METHOD, 'Method of Payment',
states={
'readonly': Eval('state').in_(['posted', 'paid', 'cancelled']),
},
depends=['state'])
payment_method_string = payment_method.translated('payment_method')
payment_method_cash = fields.Function(Monetary(
"Cash", currency='currency', digits='currency'),
'on_change_with_payment_method_cash')
payment_method_check = fields.Function(Monetary(
"Checks/Transfers/Deposit", currency='currency', digits='currency'),
'on_change_with_payment_method_check')
payment_method_card = fields.Function(Monetary(
"Credit/Debit Card", currency='currency', digits='currency'),
'on_change_with_payment_method_card')
payment_method_purchase_on_credit = fields.Function(Monetary(
"Purchase on Credit", currency='currency', digits='currency'),
'on_change_with_payment_method_purchase_on_credit')
payment_method_barter = fields.Function(Monetary(
"Barter", currency='currency', digits='currency'),
'on_change_with_payment_method_barter')
payment_method_credit_note = fields.Function(Monetary(
"Credit Note'", currency='currency', digits='currency'),
'on_change_with_payment_method_credit_note')
payment_method_mixed_payment = fields.Function(Monetary(
'Mixed Payment', currency='currency', digits='currency'),
'on_change_with_payment_method_mixed_payment')
isr_withholding_type = fields.Selection(_ISR_WITHHOLDING_TYPE, 'ISR Withholding Type',
states={
'readonly': Eval('state').in_(['posted', 'paid', 'cancelled']),
'invisible': Eval('type') == 'out',
},
depends=['state'])
isr_withholding_type_string = isr_withholding_type.translated(
'isr_withholding_type')
ncf_cancellation_type = fields.Selection(
_CANCELLATION_TYPE, 'NCF Cancellation Type',
states={
'required': (
(Eval('state') == 'cancelled')
& (Eval('type') == 'out')
& Bool(Eval('ncf_number'))
& ~Eval('context', {}).get('invoice_force_cancel', False)),
'readonly': Eval('state') == 'cancelled',
'invisible': Eval('type') == 'in',
},
depends=['state', 'type', 'ncf_number'])
ncf_cancellation_type_string = ncf_cancellation_type.translated(
'ncf_cancellation_type')
ncf_cancellation_reason = fields.Text(
'NCF Cancellation Reason',
states={
'required': (
(Eval('state') == 'cancelled')
& (Eval('type') == 'out')
& Bool(Eval('ncf_number'))
& ~Eval('context', {}).get('invoice_force_cancel', False)),
'readonly': Eval('state') == 'cancelled',
'invisible': Eval('type') == 'in',
},
depends=['state', 'type', 'ncf_number'])
@classmethod
def __setup__(cls):
super().__setup__()
cls._check_modify_exclude.update({
'ncf_cancellation_type',
'ncf_cancellation_reason',
})
@classmethod
def __register__(cls, module_name):
"""
Migrate legacy account_invoice.ncf_type safely.
Old structure:
account_invoice.ncf_type = ncf.manager.sequence.id as integer
New structure:
account_invoice.ncf_type = fiscal NCF type code as varchar
account_invoice.ncf_sequence = ncf.manager.sequence.id
"""
cls._ncf_prepare_legacy_invoice_ncf_type_column()
super().__register__(module_name)
cls._ncf_migrate_invoice_ncf_type_from_legacy_column()
cls._ncf_migrate_legacy_cancellation_fields()
@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
WHERE table_name = %s
AND column_name = %s
""", (table_name, column_name))
return cursor.fetchone() is not None
@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
WHERE table_name = %s
AND column_name = %s
""", (table_name, column_name))
row = cursor.fetchone()
return row[0] if row else None
@classmethod
def _ncf_prepare_legacy_invoice_ncf_type_column(cls):
"""
Rename old integer ncf_type column before Tryton tries to migrate it.
This avoids:
Unable to migrate column ncf_type on table account_invoice
from int4 to varchar.
"""
cursor = Transaction().connection.cursor()
table_name = 'account_invoice'
old_column = 'ncf_type'
legacy_column = 'ncf_type_legacy_sequence_id'
if not cls._ncf_column_exists(cursor, table_name, old_column):
return
if cls._ncf_column_exists(cursor, table_name, legacy_column):
return
udt_name = cls._ncf_column_udt_name(cursor, table_name, old_column)
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",
table_name,
old_column,
legacy_column,
)
cursor.execute(f"""
ALTER TABLE {table_name}
RENAME COLUMN {old_column} TO {legacy_column}
""")
@classmethod
def _ncf_migrate_invoice_ncf_type_from_legacy_column(cls):
"""
Convert legacy sequence IDs into real NCF type values.
Example:
account_invoice.ncf_type_legacy_sequence_id = 2
ncf_manager_sequence.id = 2
ncf_manager_sequence.ncf_type = '34'
Result:
account_invoice.ncf_type = '34'
account_invoice.ncf_sequence = 2
"""
cursor = Transaction().connection.cursor()
table_name = 'account_invoice'
legacy_column = 'ncf_type_legacy_sequence_id'
if not cls._ncf_column_exists(cursor, table_name, legacy_column):
logger.info(
"No legacy invoice NCF column found. Skipping invoice NCF migration."
)
return
logger.info(
"Starting migration of account_invoice legacy ncf_type sequence IDs "
"to fiscal NCF type codes"
)
cursor.execute("""
UPDATE account_invoice AS invoice
SET ncf_sequence = seq.id,
ncf_type = seq.ncf_type
FROM ncf_manager_sequence AS seq
WHERE invoice.ncf_type_legacy_sequence_id IS NOT NULL
AND invoice.ncf_type_legacy_sequence_id = seq.id
AND seq.ncf_type IS NOT NULL
AND seq.ncf_type <> ''
AND (
invoice.ncf_type IS NULL
OR invoice.ncf_type = ''
OR invoice.ncf_type <> seq.ncf_type
)
""")
logger.info(
"Migrated %s account_invoice rows from legacy ncf_type sequence IDs",
cursor.rowcount,
)
cursor.execute("""
SELECT COUNT(*)
FROM account_invoice AS invoice
LEFT JOIN ncf_manager_sequence AS seq
ON invoice.ncf_type_legacy_sequence_id = seq.id
WHERE invoice.ncf_type_legacy_sequence_id IS NOT NULL
AND seq.id IS NULL
""")
unmatched = cursor.fetchone()[0]
if unmatched:
logger.warning(
"%s account_invoice rows have legacy ncf_type values that do "
"not match any ncf_manager_sequence.id",
unmatched,
)
@classmethod
def _ncf_migrate_legacy_cancellation_fields(cls):
"""
Copy data from the former account_invoice_cancel_reason module.
The old module used generic cancellation_type/cancellation_reason
columns. ncf_manager stores those values as fiscal DGII cancellation
data so they can later feed the NCF annulment report.
"""
cursor = Transaction().connection.cursor()
table_name = 'account_invoice'
field_pairs = (
('cancellation_type', 'ncf_cancellation_type'),
('cancellation_reason', 'ncf_cancellation_reason'),
)
for old_column, new_column in field_pairs:
if not cls._ncf_column_exists(cursor, table_name, old_column):
continue
if not cls._ncf_column_exists(cursor, table_name, new_column):
continue
cursor.execute(f"""
UPDATE {table_name}
SET {new_column} = {old_column}
WHERE {old_column} IS NOT NULL
AND {old_column} <> ''
AND ({new_column} IS NULL OR {new_column} = '')
""")
@classmethod
def search_rec_name(cls, name, clause):
domain = super().search_rec_name(name, clause)
return domain + [
('ncf_number', *clause[1:]),
('ncf_number_modified', *clause[1:]),
]
@staticmethod
def _ncf_type_label_map():
"""
Return NCF/e-CF labels indexed by fiscal code.
This keeps invoice.ncf_type as a simple fiscal code while allowing
the selection list to be generated dynamically from active and valid
ncf.manager.sequence records.
"""
return dict(NCF_TYPE)
@classmethod
def _ncf_invoice_type_from_type(cls, invoice_type):
"""
Convert account.invoice.type into ncf.manager.sequence.invoice_type.
Mapping:
- account.invoice.type = 'out' -> ncf.manager.sequence.invoice_type = 'invoice_customer'
- account.invoice.type = 'in' -> ncf.manager.sequence.invoice_type = 'invoice_supplier'
Important:
Do not return invoice_supplier by default. If invoice_type is empty,
unknown or not loaded yet, return None to avoid showing wrong NCF types.
"""
if invoice_type == 'out':
return 'invoice_customer'
if invoice_type == 'in':
return 'invoice_supplier'
return None
@classmethod
def _get_available_ncf_type_domain(
cls, company_id=None, sequence_invoice_type=None):
"""
Build the domain used to find NCF types available for invoices.
sequence_invoice_type must be:
- invoice_customer
- invoice_supplier
"""
today = Transaction().context.get('date') or date.today()
domain = [
('state', '=', 'active'),
('pos.state', '=', 'active'),
('ncf_type', '!=', ''),
('ncf_sequences', '!=', None),
[
'OR',
('ncf_sequences.expiration_date', '=', None),
('ncf_sequences.expiration_date', '>=', today),
],
]
if company_id:
domain.append(('pos.company', '=', company_id))
if sequence_invoice_type:
domain.append(('invoice_type', '=', sequence_invoice_type))
return domain
@fields.depends('company', 'type')
def get_available_ncf_types(self):
"""
Dynamic values for invoice.ncf_type.
This method must use the current invoice values, not only the transaction
context, because the invoice type that controls the fiscal sequence is the
value from the current account.invoice record.
Expected behavior:
- type = 'out' -> show only ncf.manager.sequence records with
invoice_type = 'invoice_customer'
- type = 'in' -> show only ncf.manager.sequence records with
invoice_type = 'invoice_supplier'
"""
pool = Pool()
NCFSequence = pool.get('ncf.manager.sequence')
label_map = self.__class__._ncf_type_label_map()
values = [('', '')]
company_id = self.company.id if self.company else None
sequence_invoice_type = self.__class__._ncf_invoice_type_from_type(
self.type)
if not company_id or not sequence_invoice_type:
return values
sequences = NCFSequence.search(
self.__class__._get_available_ncf_type_domain(
company_id=company_id,
sequence_invoice_type=sequence_invoice_type,
),
order=[
('ncf_type', 'ASC'),
('id', 'ASC'),
],
)
seen = set()
for sequence in sequences:
strict_sequence = sequence.ncf_sequences
if not strict_sequence:
continue
if (
strict_sequence.until_number is not None
and strict_sequence.number_next is not None
and strict_sequence.number_next > strict_sequence.until_number
):
continue
ncf_type = sequence.ncf_type
if not ncf_type:
continue
if ncf_type in seen:
continue
label = label_map.get(ncf_type)
if not label:
continue
values.append((ncf_type, label))
seen.add(ncf_type)
return values
@fields.depends('ncf_type')
def on_change_with_ncf_type_code(self, name=None):
return self.ncf_type or None
@fields.depends('company', 'type', 'ncf_type', 'ncf_sequence', 'expiration_date')
def on_change_ncf_type(self):
"""
Keep compatibility when ncf_type is assigned automatically from party
defaults or from migration logic.
"""
if not self.ncf_type:
self.ncf_sequence = None
self.expiration_date = None
return
sequence = self._ncf_get_active_sequence()
if not sequence:
self.ncf_sequence = None
self.expiration_date = None
return
self.ncf_sequence = sequence
if sequence.ncf_sequences:
self.expiration_date = sequence.ncf_sequences.expiration_date
else:
self.expiration_date = None
@fields.depends('company', 'type', 'ncf_type', 'ncf_sequence', 'expiration_date')
def on_change_ncf_sequence(self):
"""
When the fiscal sequence changes, resolve the fiscal NCF type from it.
The user selects the available sequence.
The system fills invoice.ncf_type automatically.
"""
if not self.ncf_sequence:
self.ncf_type = None
self.expiration_date = None
return
expected_invoice_type = self.__class__._ncf_invoice_type_from_type(
self.type)
if (
expected_invoice_type
and self.ncf_sequence.invoice_type != expected_invoice_type
):
self.ncf_sequence = None
self.ncf_type = None
self.expiration_date = None
return
self.ncf_type = self.ncf_sequence.ncf_type
if self.ncf_sequence.ncf_sequences:
self.expiration_date = self.ncf_sequence.ncf_sequences.expiration_date
else:
self.expiration_date = None
def _ncf_get_active_sequence(self):
"""
Return the active ncf.manager.sequence related to the selected ncf_type.
"""
if not self.ncf_type or not self.company:
return None
return self.__class__._ncf_find_active_sequence(self, self.ncf_type)
def _ncf_clear_fiscal_sequence_fields(self):
"""
Clear NCF/e-CF fields related to the selected fiscal sequence.
"""
self.ncf_type = None
self.ncf_sequence = None
self.expiration_date = None
def _ncf_get_party_default_ncf_type(self):
"""
Return the default NCF type configured on the selected party according
to the current invoice type.
Mapping:
- invoice.type = 'out' -> party.customer_ncf_type
- invoice.type = 'in' -> party.supplier_ncf_type
The party fields are MultiValue fields, so the company context must be
considered when reading them.
"""
if not self.party or not self.type:
return None
if self.type == 'out':
field_name = 'customer_ncf_type'
elif self.type == 'in':
field_name = 'supplier_ncf_type'
else:
return None
company_id = self.company.id if self.company else Transaction().context.get('company')
if getattr(self.party, 'id', None) and company_id:
Party = Pool().get('party.party')
with Transaction().set_context(company=company_id):
party = Party(self.party.id)
return getattr(party, field_name, None) or None
return getattr(self.party, field_name, None) or None
def _ncf_apply_party_default_ncf_type(self):
"""
Apply the party default NCF type to the invoice.
The selected NCF type is accepted only if there is an active and valid
ncf.manager.sequence for:
- invoice company
- invoice type
- selected party default NCF type
If the party default does not have a valid active sequence, the invoice
fiscal fields are cleared to avoid assigning an invalid NCF type.
"""
party_ncf_type = self._ncf_get_party_default_ncf_type()
if not party_ncf_type:
self._ncf_clear_fiscal_sequence_fields()
return
self.ncf_type = party_ncf_type
sequence = self._ncf_get_active_sequence()
if not sequence:
self._ncf_clear_fiscal_sequence_fields()
return
self.ncf_sequence = sequence
if sequence.ncf_sequences:
self.expiration_date = sequence.ncf_sequences.expiration_date
else:
self.expiration_date = None
@fields.depends(
'party',
'company',
'type',
'ncf_type',
'ncf_sequence',
'expiration_date',
'_parent_party.customer_ncf_type',
'_parent_party.supplier_ncf_type',
)
def on_change_party(self):
"""
When the party changes, assign the default NCF type configured on the
party according to the invoice type.
Customer invoice:
type = 'out' -> party.customer_ncf_type
Supplier invoice:
type = 'in' -> party.supplier_ncf_type
"""
super().on_change_party()
self._ncf_apply_party_default_ncf_type()
@fields.depends('company', 'type', 'ncf_type', 'ncf_sequence', 'expiration_date')
def on_change_company(self):
if self.ncf_type:
sequence = self._ncf_get_active_sequence()
if not sequence:
self.ncf_type = None
self.ncf_sequence = None
self.expiration_date = None
else:
self.ncf_sequence = sequence
if sequence.ncf_sequences:
self.expiration_date = sequence.ncf_sequences.expiration_date
@fields.depends(
'company',
'type',
'party',
'journal',
'account',
'payment_term',
'invoice_address',
'party_tax_identifier',
'ncf_type',
'ncf_sequence',
'expiration_date',
methods=['set_journal', 'on_change_party'],
)
def on_change_type(self):
"""
Preserve the core account_invoice behavior and then apply NCF logic.
Core behavior:
- type = 'out' assigns the revenue journal.
- type = 'in' assigns the expense journal.
- party/account/payment term values are refreshed.
NCF behavior:
- clears fiscal sequence values when the selected sequence does not match
the new invoice type.
"""
super().on_change_type()
if not self.ncf_sequence:
self.ncf_type = None
self.expiration_date = None
return
expected_invoice_type = self.__class__._ncf_invoice_type_from_type(
self.type)
if (
expected_invoice_type
and self.ncf_sequence.invoice_type != expected_invoice_type
):
self.ncf_sequence = None
self.ncf_type = None
self.expiration_date = None
return
self.ncf_type = self.ncf_sequence.ncf_type
if self.ncf_sequence.ncf_sequences:
self.expiration_date = self.ncf_sequence.ncf_sequences.expiration_date
else:
self.expiration_date = None
def _ncf_validate_selected_sequence(self):
"""
Validate that the selected NCF sequence matches the invoice company,
invoice type, active state and selected NCF type.
"""
if not self.ncf_type:
return
if not self.ncf_sequence:
raise UserError(gettext(
'ncf_manager.msg_missing_ncf_sequence',
invoice=self.rec_name,
))
expected_invoice_type = self.__class__._ncf_invoice_type_from_type(
self.type)
if (
expected_invoice_type
and self.ncf_sequence.invoice_type != expected_invoice_type
):
raise UserError(gettext(
'ncf_manager.msg_invalid_ncf_sequence_invoice_type',
invoice=self.rec_name,
))
if self.ncf_sequence.ncf_type != self.ncf_type:
raise UserError(gettext(
'ncf_manager.msg_invalid_ncf_sequence_type',
invoice=self.rec_name,
))
if self.ncf_sequence.state != 'active':
raise UserError(gettext(
'ncf_manager.msg_inactive_ncf_sequence',
invoice=self.rec_name,
))
if self.ncf_sequence.pos.state != 'active':
raise UserError(gettext(
'ncf_manager.msg_inactive_ncf_manager',
invoice=self.rec_name,
))
strict_sequence = self.ncf_sequence.ncf_sequences
if not strict_sequence:
raise UserError(gettext(
'ncf_manager.msg_missing_ncf_sequence',
invoice=self.rec_name,
))
today = Transaction().context.get('date') or date.today()
if (
strict_sequence.expiration_date
and strict_sequence.expiration_date < today
):
raise UserError(gettext(
'ncf_manager.msg_ncf_sequence_expired',
invoice=self.rec_name,
))
if (
strict_sequence.until_number is not None
and strict_sequence.number_next is not None
and strict_sequence.number_next > strict_sequence.until_number
):
raise UserError(gettext(
'ncf_manager.msg_ncf_sequence_finished',
invoice=self.rec_name,
))
@fields.depends('payment_method', 'total_amount')
def on_change_with_payment_method_cash(self, name=None):
if self.payment_method and self.payment_method == '01':
return self.total_amount
@fields.depends('payment_method', 'total_amount')
def on_change_with_payment_method_check(self, name=None):
if self.payment_method and self.payment_method == '02':
return self.total_amount
@fields.depends('payment_method', 'total_amount')
def on_change_with_payment_method_card(self, name=None):
if self.payment_method and self.payment_method == '03':
return self.total_amount
@fields.depends('payment_method', 'total_amount')
def on_change_with_payment_method_purchase_on_credit(self, name=None):
if self.payment_method and self.payment_method == '04':
return self.total_amount
@fields.depends('payment_method', 'total_amount')
def on_change_with_payment_method_barter(self, name=None):
if self.payment_method and self.payment_method == '05':
return self.total_amount
@fields.depends('payment_method', 'total_amount')
def on_change_with_payment_method_credit_note(self, name=None):
if self.payment_method and self.payment_method == '06':
return self.total_amount
@fields.depends('payment_method', 'total_amount')
def on_change_with_payment_method_mixed_payment(self, name=None):
if self.payment_method and self.payment_method == '07':
return self.total_amount
# def get_untaxed_amount_invoiced(self, name=None):
# amount = _ZERO
# amount = self.untaxed_amount
# return abs(amount)
# def get_itbis_amount_invoiced(self, name=None):
# amount = _ZERO
# amount = self.itbis_invoiced
# return abs(amount)
# def get_total_amount_invoiced(self, name=None):
# amount = _ZERO
# amount = self.total_amount
# return abs(amount)
@classmethod
def get_amount_invoiced_services(cls, invoices, names):
amount_invoiced_services = {i.id: _ZERO for i in invoices}
for invoice in invoices:
amount_invoiced_services[invoice.id] = sum(
(line.amount for line in invoice.lines
if line.type == 'line' and line.product_type == 'service'),
_ZERO)
return {'amount_invoiced_services': amount_invoiced_services}
@classmethod
def search_amount_invoiced_services(cls, name, clause):
pool = Pool()
Rule = pool.get('ir.rule')
Line = pool.get('account.invoice.line')
Invoice = pool.get('account.invoice')
Currency = pool.get('currency.currency')
type_name = cls.amount_invoiced_services._field.sql_type().base
line = Line.__table__()
invoice = Invoice.__table__()
currency = Currency.__table__()
_, operator, value = clause
if value is None:
return []
invoice_query = Rule.query_get('account.invoice')
Operator = fields.SQL_OPERATORS[operator]
# SQLite uses float for sum
if _backend_name() == 'sqlite':
value = float(value)
query = line.join(invoice,
condition=(invoice.id == line.invoice)
).join(currency,
condition=(currency.id == invoice.currency)
).select(line.invoice,
where=line.invoice.in_(
invoice_query),
group_by=line.invoice,
having=Operator(Coalesce(Sum(
Round((line.quantity * line.unit_price).cast(
type_name),
currency.digits)), 0).cast(type_name),
value))
return [('id', 'in', query)]
@classmethod
def search_amount_invoiced_goods(cls, name, clause):
pool = Pool()
Rule = pool.get('ir.rule')
Line = pool.get('account.invoice.line')
Invoice = pool.get('account.invoice')
Currency = pool.get('currency.currency')
type_name = cls.amount_invoiced_goods._field.sql_type().base
line = Line.__table__()
invoice = Invoice.__table__()
currency = Currency.__table__()
_, operator, value = clause
if value is None:
return []
invoice_query = Rule.query_get('account.invoice')
Operator = fields.SQL_OPERATORS[operator]
if _backend_name() == 'sqlite':
value = float(value)
query = line.join(invoice,
condition=(invoice.id == line.invoice)
).join(currency,
condition=(currency.id == invoice.currency)
).select(line.invoice,
where=line.invoice.in_(invoice_query),
group_by=line.invoice,
having=Operator(Coalesce(Sum(
Round((line.quantity * line.unit_price).cast(
type_name),
currency.digits)), 0).cast(type_name),
value))
return [('id', 'in', query)]
@classmethod
def get_amount_invoiced_goods(cls, invoices, names):
amount_invoiced_goods = {i.id: _ZERO for i in invoices}
for invoice in invoices:
amount_invoiced_goods[invoice.id] = sum(
(line.amount for line in invoice.lines
if line.type == 'line' and line.product_type == 'goods'),
_ZERO)
return {'amount_invoiced_goods': amount_invoiced_goods}
def get_id_type(self, name):
if self.party:
if not self.party_tax_identifier:
return None
code_length = len(self.party_tax_identifier.code)
if code_length == 9:
return 1
elif code_length == 11:
return 2
else:
return 3
@fields.depends('taxes', 'lines')
def on_change_with_itbis_invoiced(self, name=None):
itbis_invoiced = _ZERO
for tax_line in self.taxes:
if tax_line.tax and tax_line.tax.tax_kind == 'itbis':
itbis_invoiced += tax_line.amount
return abs(itbis_invoiced)
@fields.depends('taxes', 'lines')
def on_change_with_isc_invoiced(self, name=None):
isc_invoiced = _ZERO
for tax_line in self.taxes:
if tax_line.tax and tax_line.tax.tax_kind == 'isc':
isc_invoiced += tax_line.amount
return abs(isc_invoiced)
@fields.depends('taxes', 'lines')
def on_change_with_cdt_invoiced(self, name=None):
cdt_invoiced = _ZERO
for tax_line in self.taxes:
if tax_line.tax and tax_line.tax.tax_kind == 'cdt':
cdt_invoiced += tax_line.amount
return abs(cdt_invoiced)
@fields.depends('taxes', 'lines')
def on_change_with_itbis_withholding(self, name=None):
itbis_withholding = _ZERO
for tax_line in self.taxes:
if tax_line.tax and tax_line.tax.tax_kind == 'itbis_withholding':
itbis_withholding += tax_line.amount
return abs(itbis_withholding)
@fields.depends('taxes', 'lines')
def on_change_with_itbis_withholding_acquirer(self, name=None):
total = _ZERO
for tax_line in self.taxes:
if (tax_line.tax
and tax_line.tax.tax_kind == 'itbis_withholding_acquirer'):
total += tax_line.amount
return abs(total)
@fields.depends('taxes', 'lines')
def on_change_with_isr_withholding(self, name=None):
isr_withholding = _ZERO
for tax_line in self.taxes:
if tax_line.tax and tax_line.tax.tax_kind == 'isr_withholding':
isr_withholding += tax_line.amount
return abs(isr_withholding)
@fields.depends('taxes', 'lines')
def on_change_with_tip(self, name=None):
tip = _ZERO
for tax_line in self.taxes:
if tax_line.tax and tax_line.tax.tax_kind == 'tip':
tip += tax_line.amount
return abs(tip)
@classmethod
def _search_tax_amount_by_kind(cls, field, tax_kind, clause):
pool = Pool()
Rule = pool.get('ir.rule')
InvoiceTax = pool.get('account.invoice.tax')
AccountTax = pool.get('account.tax')
type_name = field.sql_type().base
invoice_tax = InvoiceTax.__table__()
account_tax = AccountTax.__table__()
_, operator, value = clause
invoice_query = Rule.query_get('account.invoice')
Operator = fields.SQL_OPERATORS[operator]
query = invoice_tax.join(
account_tax,
condition=(account_tax.id == invoice_tax.tax),
).select(
invoice_tax.invoice,
where=(
invoice_tax.invoice.in_(invoice_query)
& (account_tax.tax_kind == tax_kind)
),
group_by=invoice_tax.invoice,
having=Operator(
Coalesce(Sum(invoice_tax.amount), 0).cast(type_name),
value,
),
)
return [('id', 'in', query)]
@classmethod
def search_itbis_invoiced(cls, name, clause):
return cls._search_tax_amount_by_kind(
cls.itbis_invoiced._field, 'itbis', clause)
@classmethod
def search_itbis_withholding(cls, name, clause):
return cls._search_tax_amount_by_kind(
cls.itbis_withholding._field, 'itbis_withholding', clause)
@classmethod
def search_itbis_withholding_acquirer(cls, name, clause):
return cls._search_tax_amount_by_kind(
cls.itbis_withholding_acquirer._field,
'itbis_withholding_acquirer', clause)
@classmethod
def search_isc_invoiced(cls, name, clause):
return cls._search_tax_amount_by_kind(
cls.isc_invoiced._field, 'isc', clause)
@classmethod
def search_cdt_invoiced(cls, name, clause):
return cls._search_tax_amount_by_kind(
cls.cdt_invoiced._field, 'cdt', clause)
@classmethod
def search_isr_withholding(cls, name, clause):
return cls._search_tax_amount_by_kind(
cls.isr_withholding._field, 'isr_withholding', clause)
@classmethod
def search_tip(cls, name, clause):
return cls._search_tax_amount_by_kind(
cls.tip._field, 'tip', clause)
@classmethod
def _ncf_invoice_type(cls, invoice):
"""
Return ncf.manager.sequence invoice_type according to account.invoice.type.
"""
return cls._ncf_invoice_type_from_type(invoice.type)
@classmethod
def _ncf_code(cls, invoice):
"""
Return the fiscal/e-CF code from invoice.ncf_type.
"""
return invoice.ncf_type or None
@classmethod
def _ncf_is_credit_note(cls, invoice):
"""
Return True when the invoice is fiscally a credit note.
"""
return cls._ncf_code(invoice) in _CREDIT_NOTE_CODES
@classmethod
def _ncf_is_debit_note(cls, invoice):
"""
Return True when the invoice is fiscally a debit note.
"""
return cls._ncf_code(invoice) in _DEBIT_NOTE_CODES
@classmethod
def _ncf_resolve_credit_note_code(cls, origin):
"""
Resolve the preferred NCF/e-NCF type for a credit note.
Electronic credit notes are only allowed when the electronic invoicing
module is active.
"""
if cls._ncf_electronic_invoicing_enabled():
return '34'
return '4'
@classmethod
def _ncf_resolve_debit_note_code(cls, origin):
"""
Resolve the preferred NCF/e-NCF type for a debit note.
Electronic debit notes are only allowed when the electronic invoicing
module is active.
"""
if cls._ncf_electronic_invoicing_enabled():
return '33'
return '3'
@classmethod
def _ncf_find_active_sequence(cls, invoice, ncf_code):
"""
Find active and valid ncf.manager.sequence for the invoice company,
invoice type and selected fiscal NCF/e-CF type.
"""
if not ncf_code or not invoice.company:
return None
NCFSequence = Pool().get('ncf.manager.sequence')
today = Transaction().context.get('date') or date.today()
sequence_invoice_type = cls._ncf_invoice_type(invoice)
if not sequence_invoice_type:
return None
sequences = NCFSequence.search([
('ncf_type', '=', ncf_code),
('invoice_type', '=', sequence_invoice_type),
('pos.company', '=', invoice.company.id),
('pos.state', '=', 'active'),
('state', '=', 'active'),
('ncf_sequences', '!=', None),
[
'OR',
('ncf_sequences.expiration_date', '=', None),
('ncf_sequences.expiration_date', '>=', today),
],
], order=[
('id', 'ASC'),
])
for sequence in sequences:
strict_sequence = sequence.ncf_sequences
if not strict_sequence:
continue
if (
strict_sequence.until_number is not None
and strict_sequence.number_next is not None
and strict_sequence.number_next > strict_sequence.until_number
):
continue
return sequence
return None
@classmethod
def _ncf_electronic_invoicing_enabled(cls):
"""
Return whether electronic invoicing is enabled for NCF selection.
ncf_manager alone must only issue physical B NCFs. Electronic E NCFs
are enabled by an electronic invoicing module. A concrete electronic
invoicing module may also override this hook if its technical name is
not in _ELECTRONIC_INVOICE_MODULES.
"""
Module = Pool().get('ir.module')
return bool(Module.search([
('name', 'in', list(_ELECTRONIC_INVOICE_MODULES)),
('state', '=', 'activated'),
], limit=1))
@classmethod
def _ncf_note_code_candidates(cls, origin, electronic_code, physical_code):
"""
Return note NCF codes in preference order.
ncf_manager alone issues B notes. When electronic invoicing is active,
E notes are preferred and B notes remain a fallback.
"""
if not cls._ncf_electronic_invoicing_enabled():
return [physical_code]
preferred = (
electronic_code
if cls._ncf_code(origin) in _ECF_ORIGIN_CODES
else physical_code)
candidates = [electronic_code, physical_code]
if preferred in candidates:
candidates.remove(preferred)
candidates.insert(0, preferred)
return candidates
@classmethod
def _ncf_find_note_sequence(
cls, origin, note, electronic_code, physical_code):
"""
Return the selected NCF code and active sequence for a debit/credit note.
"""
for ncf_code in cls._ncf_note_code_candidates(
origin, electronic_code, physical_code):
sequence = cls._ncf_find_active_sequence(note, ncf_code)
if sequence:
return ncf_code, sequence
return None, None
@classmethod
def _ncf_prepare_credit_note_from_origin(cls, origin, credit_note):
"""
Fiscal preparation for credit notes generated from an existing invoice.
This is the specific fiscal responsibility of ncf_manager:
- Set ncf_number_modified from the original invoice ncf_number.
- Assign the correct credit-note NCF type: 34 or 4.
- Set fiscal payment method to credit note when available.
"""
if not origin.ncf_number:
return
credit_note.ncf_number_modified = origin.ncf_number
ncf_code, sequence = cls._ncf_find_note_sequence(
origin, credit_note, '34', '4')
if not sequence:
raise UserError(gettext(
'ncf_manager.msg_missing_credit_note_sequence',
ncf_type='/'.join(cls._ncf_note_code_candidates(
origin, '34', '4')),
company=credit_note.company.rec_name))
credit_note.ncf_type = ncf_code
credit_note.ncf_sequence = sequence
if hasattr(credit_note, 'payment_method'):
credit_note.payment_method = '06'
if hasattr(credit_note, 'revenue_type') and credit_note.type == 'out':
credit_note.revenue_type = credit_note.revenue_type or '01'
@classmethod
def _ncf_prepare_debit_note_from_origin(cls, origin, debit_note):
"""
Fiscal preparation for debit notes generated from an existing invoice.
This is the specific fiscal responsibility of ncf_manager:
- Set ncf_number_modified from the original invoice ncf_number.
- Assign the correct debit-note NCF type: 33 or 3.
- Preserve fiscal report fields from the original invoice when present.
"""
if not origin.ncf_number:
return
debit_note.ncf_number_modified = origin.ncf_number
ncf_code, sequence = cls._ncf_find_note_sequence(
origin, debit_note, '33', '3')
if not sequence:
raise UserError(gettext(
'ncf_manager.msg_missing_debit_note_sequence',
ncf_type='/'.join(cls._ncf_note_code_candidates(
origin, '33', '3')),
company=debit_note.company.rec_name))
debit_note.ncf_type = ncf_code
debit_note.ncf_sequence = sequence
if hasattr(debit_note, 'payment_method'):
debit_note.payment_method = getattr(origin, 'payment_method', None)
if hasattr(debit_note, 'revenue_type') and debit_note.type == 'out':
debit_note.revenue_type = (
getattr(origin, 'revenue_type', None)
or debit_note.revenue_type
or '01')
if hasattr(debit_note, 'expense_type') and debit_note.type == 'in':
debit_note.expense_type = (
getattr(origin, 'expense_type', None)
or debit_note.expense_type)
if hasattr(debit_note, 'isr_withholding_type'):
debit_note.isr_withholding_type = getattr(
origin, 'isr_withholding_type', None)
@classmethod
def _debit_prepare_note_from_origin(cls, origin, debit_note, values):
super_method = getattr(super(), '_debit_prepare_note_from_origin', None)
if super_method:
super_method(origin, debit_note, values)
cls._ncf_prepare_debit_note_from_origin(origin, debit_note)
@classmethod
def credit(cls, invoices, refund=False, **values):
"""
Extend credit-note creation with fiscal NCF behavior.
This method must live in ncf_manager, not in account_credit_note.
It works for:
- Credit note from form view.
- Credit note from tree view.
- Full credit note.
- Partial credit note created by account_credit_note.
Important:
We call super(..., refund=False) first because ncf_number_modified and
ncf_type must be assigned before posting the generated credit note.
"""
credit_mode = values.get('credit_mode') or 'full'
if refund and credit_mode in ('percent', 'amount'):
raise UserError(gettext(
'ncf_manager.msg_partial_credit_note_refund_not_allowed'))
new_invoices = super().credit(invoices, refund=False, **values)
to_save = []
for origin, credit_note in zip(invoices, new_invoices):
cls._ncf_prepare_credit_note_from_origin(origin, credit_note)
to_save.append(credit_note)
if to_save:
cls.save(to_save)
if refund:
cls.post(new_invoices)
for origin, credit_note in zip(invoices, new_invoices):
if origin.state != 'posted':
raise UserError(gettext(
'account_invoice.msg_invoice_credit_refund_not_posted',
invoice=origin.rec_name))
origin.cancel_move = credit_note.move
cls.save(invoices)
cls.cancel(invoices)
return new_invoices
@classmethod
def set_ncf_number(cls, invoices):
"""
Assign NCF/e-NCF number to invoices before posting.
The user selects invoice.ncf_type as a simple Selection value.
The system resolves invoice.ncf_sequence automatically from the active
ncf.manager.sequence configuration.
"""
for invoice in invoices:
if invoice.ncf_number:
continue
if not invoice.ncf_type:
raise UserError(gettext('ncf_manager.msg_no_ncf_type'))
ncf_code = invoice.ncf_type
if invoice.type == 'in' and ncf_code == '99':
continue
ncf_sequence = invoice.ncf_sequence or cls._ncf_find_active_sequence(
invoice, ncf_code)
if not ncf_sequence:
raise UserError(gettext(
'ncf_manager.msg_missing_ncf_sequence',
invoice=invoice.rec_name,
))
strict_sequence = ncf_sequence.ncf_sequences
if not strict_sequence:
raise UserError(gettext(
'ncf_manager.msg_missing_strict_sequence'))
invoice.ncf_sequence = ncf_sequence
if not invoice.expiration_date:
invoice.expiration_date = strict_sequence.expiration_date
invoice.ncf_number, invoice.sequence = invoice.get_next_ncf_number(
ncf_sequence=ncf_sequence)
cls.save(invoices)
def get_next_ncf_number(self, pattern=None, ncf_sequence=None):
"""
Return next fiscal number and sequence id used.
"""
if not self.ncf_type:
raise UserError(gettext('ncf_manager.msg_no_ncf_type'))
ncf_sequence = ncf_sequence or self.ncf_sequence or self._ncf_get_active_sequence()
if not ncf_sequence:
raise UserError(gettext(
'ncf_manager.msg_missing_ncf_sequence',
invoice=self.rec_name,
))
strict_sequence = ncf_sequence.ncf_sequences
if not strict_sequence:
raise UserError(gettext(
'ncf_manager.msg_missing_strict_sequence'))
today = Transaction().context.get('date') or date.today()
with Transaction().set_context(company=self.company.id):
if (
strict_sequence.until_number is not None
and strict_sequence.number_next is not None
and strict_sequence.number_next > strict_sequence.until_number
):
raise UserError(gettext(
'ncf_manager.msg_ncf_sequence_finished'))
if strict_sequence.expiration_date and today > strict_sequence.expiration_date:
raise UserError(gettext(
'ncf_manager.msg_ncf_sequence_expired'))
return strict_sequence.get(), strict_sequence.id
def check_credit_note(self):
if self.total_amount > 0:
raise UserError(gettext(
'ncf_manager.msg_amount_credit_note_negative'))
def check_invoice(self):
if self.total_amount < 0:
raise UserError(gettext(
'ncf_manager.msg_amount_invoice_no_negative'))
def check_payment_term(self):
if not self.payment_term:
raise UserError(gettext(
'ncf_manager.msg_no_payment_term'))
def check_revenue_type(self):
if not self.revenue_type and self.type == 'out':
raise UserError(gettext(
'ncf_manager.msg_no_revenue_type'))
def check_expense_type(self):
if not self.expense_type and self.type == 'in':
raise UserError(gettext(
'ncf_manager.msg_no_expense_type'))
def check_payment_method(self):
if not self.payment_method:
raise UserError(gettext(
'ncf_manager.msg_no_method_payment'))
def check_isr_withholding_type(self):
if self.isr_withholding > _ZERO and not self.isr_withholding_type:
raise UserError(gettext(
'ncf_manager.msg_no_isr_withholding_type'))
def check_no_expiration_date(self):
if self.type == 'in':
if not self.expiration_date:
raise UserError(gettext(
'ncf_manager.msg_no_expiration_date'))
@classmethod
def _ncf_check_fiscal_classification(cls, invoice):
"""
Validate fiscal classification fields in DGII-friendly order.
"""
if invoice.type == 'out':
invoice.check_revenue_type()
else:
invoice.check_expense_type()
invoice.check_isr_withholding_type()
invoice.check_payment_method()
def check_ncf_number_modified(self):
"""
Validate modified NCF only for fiscal/electronic debit/credit notes.
"""
if not self.ncf_type:
return
if self.ncf_type not in _MODIFIED_NOTE_CODES:
return
if not self.ncf_number_modified:
raise UserError(gettext(
'ncf_manager.msg_no_ncf_number_modified'))
if self.type == 'in':
length = len(self.ncf_number_modified or '')
if length < 11:
raise UserError(gettext(
'ncf_manager.msg_incorrect_lenght_ncf_number_modified'))
if 11 < length < 13:
raise UserError(gettext(
'ncf_manager.msg_incorrect_lenght_ncf_number_modified'))
if 13 < length < 19:
raise UserError(gettext(
'ncf_manager.msg_incorrect_lenght_ncf_number_modified'))
@staticmethod
def _ncf_is_fiscal_cancellation_required(invoice):
return (
invoice.type == 'out'
and bool(getattr(invoice, 'ncf_number', None)))
@classmethod
def _ncf_check_cancellation_information(cls, invoice):
if not cls._ncf_is_fiscal_cancellation_required(invoice):
return
if not invoice.ncf_cancellation_type:
raise UserError(gettext(
'ncf_manager.msg_missing_ncf_cancellation_type',
invoice=invoice.rec_name))
if not invoice.ncf_cancellation_reason:
raise UserError(gettext(
'ncf_manager.msg_missing_ncf_cancellation_reason',
invoice=invoice.rec_name))
@classmethod
def copy(cls, invoices, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('ncf_type', None)
default.setdefault('ncf_sequence', None)
default.setdefault('ncf_number', None)
default.setdefault('ncf_number_modified', None)
default.setdefault('expiration_date', None)
default.setdefault('security_code', None)
default.setdefault('ncf_cancellation_type', None)
default.setdefault('ncf_cancellation_reason', None)
return super(Invoice, cls).copy(invoices, default=default)
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('/tree/field[@name="expense_type"]', 'tree_invisible',
Eval('type') == 'out'),
('/tree/field[@name="revenue_type"]', 'tree_invisible',
Eval('type') == 'in'),
('/form/notebook/page[@id="tax_information"]/separator[@id="revenue"]', 'states', {
'invisible': Eval('type') == 'in',
}),
('/form/notebook/page[@id="tax_information"]/separator[@id="expense"]', 'states', {
'invisible': Eval('type') == 'out',
}),
('/form/notebook/page[@id="ncf_cancellation"]', 'states', {
'invisible': (
(Eval('type') == 'in') | ~Bool(Eval('ncf_number'))),
}),
]
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, invoices):
if not Transaction().context.get('invoice_force_cancel'):
for invoice in invoices:
cls._ncf_check_cancellation_information(invoice)
super().cancel(invoices)
@classmethod
def _post(cls, invoices):
cls.set_ncf_number(invoices)
for invoice in invoices:
ncf_code = invoice.ncf_type
is_debit_note = cls._ncf_is_debit_note(invoice)
is_credit_note = cls._ncf_is_credit_note(invoice)
is_modified_note = is_debit_note or is_credit_note
is_without_ncf = ncf_code == '99'
if invoice.type == 'out':
invoice._ncf_validate_selected_sequence()
if is_credit_note:
invoice.check_credit_note()
invoice.check_ncf_number_modified()
cls._ncf_check_fiscal_classification(invoice)
invoice.check_payment_term()
else:
invoice.check_invoice()
if is_modified_note:
invoice.check_ncf_number_modified()
cls._ncf_check_fiscal_classification(invoice)
invoice.check_payment_term()
else:
if is_without_ncf:
continue
if is_credit_note:
invoice.check_credit_note()
invoice.check_ncf_number_modified()
invoice.check_no_expiration_date()
cls._ncf_check_fiscal_classification(invoice)
invoice.check_payment_term()
else:
if is_modified_note:
invoice.check_ncf_number_modified()
invoice.check_no_expiration_date()
cls._ncf_check_fiscal_classification(invoice)
invoice.check_payment_term()
super()._post(invoices)
class InvoiceReport(metaclass=PoolMeta):
__name__ = 'account.invoice'
@classmethod
def get_context(cls, records, header, data):
context = super().get_context(records, header, data)
invoice = context.get('invoice')
ncf_type_map = dict(NCF_TYPE)
ncf_type_name = ''
if invoice and invoice.ncf_type:
ncf_type_name = ncf_type_map.get(invoice.ncf_type, '')
context['ncf_type_name'] = ncf_type_name
return context