1810 lines
62 KiB
Python
Executable File
1810 lines
62 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 trytond.model import fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval, If, And
|
|
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__)
|
|
|
|
|
|
_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"),
|
|
]
|
|
|
|
_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')
|
|
|
|
_CREDIT_NOTE_CODES = ('4', '34')
|
|
_DEBIT_NOTE_CODES = ('3', '33')
|
|
|
|
_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',
|
|
]
|
|
|
|
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')
|
|
|
|
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')
|
|
|
|
# itbis_taken_cost = fields.Function(Monetary(
|
|
# "ITBIS Taken Cost", currency='currency', digits='currency'),
|
|
# 'get_itbis_taken_cost', searcher='search_itbis_taken_cost')
|
|
|
|
# untaxed_amount_invoiced = fields.Function(fields.Numeric(
|
|
# 'Untaxed Invoiced', digits=(16,
|
|
# Eval('currency_digits', 2)), depends=['currency_digits']),
|
|
# 'get_untaxed_amount_invoiced')
|
|
|
|
# itbis_amount_invoiced = fields.Function(fields.Numeric(
|
|
# 'ITBIS Invoiced', digits=(16,
|
|
# Eval('currency_digits', 2)), depends=['currency_digits']),
|
|
# 'get_itbis_amount_invoiced')
|
|
|
|
# total_amount_invoiced = fields.Function(fields.Numeric(
|
|
# 'Total Invoiced', digits=(16,
|
|
# Eval('currency_digits', 2)), depends=['currency_digits']),
|
|
# 'get_total_amount_invoiced')
|
|
|
|
|
|
@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()
|
|
|
|
@classmethod
|
|
def _ncf_column_exists(cls, cursor, table_name, column_name):
|
|
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):
|
|
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'}:
|
|
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 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 NCF type for 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
|
|
|
|
party_ncf_type = self._ncf_get_party_configured_ncf_type(field_name)
|
|
if party_ncf_type:
|
|
return party_ncf_type
|
|
|
|
return self._ncf_get_configuration_default_ncf_type(field_name)
|
|
|
|
def _ncf_get_party_configured_ncf_type(self, field_name):
|
|
"""
|
|
Return only the value explicitly saved for the party.
|
|
|
|
Reading the MultiValue field directly also applies the model default.
|
|
For NCF types that would make every party look as if it had the
|
|
configured global default. The invoice must distinguish a party
|
|
override from the account configuration fallback.
|
|
"""
|
|
if not getattr(self.party, 'id', None):
|
|
return getattr(self.party, field_name, None) or None
|
|
|
|
PartyNcfType = Pool().get('party.party.ncf_type')
|
|
records = PartyNcfType.search([
|
|
('party', '=', self.party.id),
|
|
], limit=1)
|
|
|
|
if records:
|
|
return getattr(records[0], field_name, None) or None
|
|
|
|
return None
|
|
|
|
def _ncf_get_configuration_default_ncf_type(self, field_name):
|
|
"Return the NCF type configured globally for parties without override."
|
|
Configuration = Pool().get('account.configuration')
|
|
config = Configuration(1)
|
|
|
|
if field_name == 'customer_ncf_type':
|
|
config_field = 'default_customer_ncf_type'
|
|
elif field_name == 'supplier_ncf_type':
|
|
config_field = 'default_supplier_ncf_type'
|
|
else:
|
|
return None
|
|
|
|
company_id = (
|
|
self.company.id if self.company
|
|
else Transaction().context.get('company'))
|
|
|
|
if company_id:
|
|
with Transaction().set_context(company=company_id):
|
|
return config.get_multivalue(config_field) or None
|
|
|
|
return config.get_multivalue(config_field) 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):
|
|
pool = Pool()
|
|
InvoiceTax = pool.get('account.invoice.tax')
|
|
Move = pool.get('account.move')
|
|
MoveLine = pool.get('account.move.line')
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
# untaxed_amount = dict((i.id, _ZERO) for i in invoices)
|
|
amount_invoiced_services = dict((i.id, _ZERO) for i in invoices)
|
|
tax_amount = dict((i.id, _ZERO) for i in invoices)
|
|
total_amount = dict((i.id, _ZERO) for i in invoices)
|
|
|
|
invoices_move = set()
|
|
invoices_no_move = set()
|
|
for invoice in invoices:
|
|
if invoice.move:
|
|
invoices_move.add(invoice.id)
|
|
else:
|
|
invoices_no_move.add(invoice.id)
|
|
invoices_move = cls.browse(invoices_move)
|
|
invoices_no_move = cls.browse(invoices_no_move)
|
|
|
|
for invoice in invoices_move:
|
|
amount_invoiced_services[invoice.id] = sum(
|
|
(line.amount for line in invoice.lines
|
|
if line.type == 'line' and line.product_type == 'service'), _ZERO)
|
|
total_amount[invoice.id] = (
|
|
amount_invoiced_services[invoice.id] + tax_amount[invoice.id])
|
|
|
|
for invoice in invoices_no_move:
|
|
amount_invoiced_services[invoice.id] = sum(
|
|
(line.amount for line in invoice.lines
|
|
if line.type == 'line' and line.product_type == 'service'), _ZERO)
|
|
total_amount[invoice.id] = (
|
|
amount_invoiced_services[invoice.id] + tax_amount[invoice.id])
|
|
|
|
result = {
|
|
'amount_invoiced_services': amount_invoiced_services,
|
|
'tax_amount': tax_amount,
|
|
'total_amount': total_amount,
|
|
}
|
|
for key in list(result.keys()):
|
|
return result
|
|
|
|
@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
|
|
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 get_amount_invoiced_goods(cls, invoices, names):
|
|
pool = Pool()
|
|
InvoiceTax = pool.get('account.invoice.tax')
|
|
Move = pool.get('account.move')
|
|
MoveLine = pool.get('account.move.line')
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
# untaxed_amount = dict((i.id, _ZERO) for i in invoices)
|
|
amount_invoiced_goods = dict((i.id, _ZERO) for i in invoices)
|
|
tax_amount = dict((i.id, _ZERO) for i in invoices)
|
|
total_amount = dict((i.id, _ZERO) for i in invoices)
|
|
|
|
invoices_move = set()
|
|
invoices_no_move = set()
|
|
for invoice in invoices:
|
|
if invoice.move:
|
|
invoices_move.add(invoice.id)
|
|
else:
|
|
invoices_no_move.add(invoice.id)
|
|
invoices_move = cls.browse(invoices_move)
|
|
invoices_no_move = cls.browse(invoices_no_move)
|
|
|
|
for invoice in invoices_move:
|
|
amount_invoiced_goods[invoice.id] = sum(
|
|
(line.amount for line in invoice.lines
|
|
if line.type == 'line' and line.product_type == 'goods'), _ZERO)
|
|
total_amount[invoice.id] = (
|
|
amount_invoiced_goods[invoice.id] + tax_amount[invoice.id])
|
|
|
|
for invoice in invoices_no_move:
|
|
amount_invoiced_goods[invoice.id] = sum(
|
|
(line.amount for line in invoice.lines
|
|
if line.type == 'line' and line.product_type == 'goods'), _ZERO)
|
|
total_amount[invoice.id] = (
|
|
amount_invoiced_goods[invoice.id] + tax_amount[invoice.id])
|
|
|
|
result = {
|
|
'amount_invoiced_goods': amount_invoiced_goods,
|
|
'tax_amount': tax_amount,
|
|
'total_amount': total_amount,
|
|
}
|
|
for key in list(result.keys()):
|
|
return result
|
|
|
|
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_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_itbis_invoiced(cls, name, clause):
|
|
pool = Pool()
|
|
Rule = pool.get('ir.rule')
|
|
Tax = pool.get('account.invoice.tax')
|
|
type_name = cls.itbis_invoiced._field.sql_type().base
|
|
tax = Tax.__table__()
|
|
|
|
_, operator, value = clause
|
|
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 = tax.select(tax.invoice,
|
|
where=tax.invoice.in_(invoice_query),
|
|
group_by=tax.invoice,
|
|
having=Operator(Coalesce(Sum(tax.amount), 0).cast(type_name),
|
|
value))
|
|
return [('id', 'in', query)]
|
|
|
|
@classmethod
|
|
def search_itbis_withholding(cls, name, clause):
|
|
pool = Pool()
|
|
Rule = pool.get('ir.rule')
|
|
Tax = pool.get('account.invoice.tax')
|
|
type_name = cls.itbis_withholding._field.sql_type().base
|
|
tax = Tax.__table__()
|
|
|
|
_, operator, value = clause
|
|
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 = tax.select(tax.invoice,
|
|
where=tax.invoice.in_(invoice_query),
|
|
group_by=tax.invoice,
|
|
having=Operator(Coalesce(Sum(tax.amount), 0).cast(type_name),
|
|
value))
|
|
return [('id', 'in', query)]
|
|
|
|
@classmethod
|
|
def search_isc_invoiced(cls, name, clause):
|
|
pool = Pool()
|
|
Rule = pool.get('ir.rule')
|
|
Tax = pool.get('account.invoice.tax')
|
|
type_name = cls.isc_invoiced._field.sql_type().base
|
|
tax = Tax.__table__()
|
|
|
|
_, operator, value = clause
|
|
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 = tax.select(tax.invoice,
|
|
where=tax.invoice.in_(invoice_query),
|
|
group_by=tax.invoice,
|
|
having=Operator(Coalesce(Sum(tax.amount), 0).cast(type_name),
|
|
value))
|
|
return [('id', 'in', query)]
|
|
|
|
@classmethod
|
|
def search_cdt_invoiced(cls, name, clause):
|
|
pool = Pool()
|
|
Rule = pool.get('ir.rule')
|
|
Tax = pool.get('account.invoice.tax')
|
|
type_name = cls.cdt_invoiced._field.sql_type().base
|
|
tax = Tax.__table__()
|
|
|
|
_, operator, value = clause
|
|
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 = tax.select(tax.invoice,
|
|
where=tax.invoice.in_(invoice_query),
|
|
group_by=tax.invoice,
|
|
having=Operator(Coalesce(Sum(tax.amount), 0).cast(type_name),
|
|
value))
|
|
return [('id', 'in', query)]
|
|
|
|
@classmethod
|
|
def search_isr_withholding(cls, name, clause):
|
|
pool = Pool()
|
|
Rule = pool.get('ir.rule')
|
|
Tax = pool.get('account.invoice.tax')
|
|
type_name = cls.isr_withholding._field.sql_type().base
|
|
tax = Tax.__table__()
|
|
|
|
_, operator, value = clause
|
|
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 = tax.select(tax.invoice,
|
|
where=tax.invoice.in_(invoice_query),
|
|
group_by=tax.invoice,
|
|
having=Operator(Coalesce(Sum(tax.amount), 0).cast(type_name),
|
|
value))
|
|
return [('id', 'in', query)]
|
|
|
|
@classmethod
|
|
def search_tip(cls, name, clause):
|
|
pool = Pool()
|
|
Rule = pool.get('ir.rule')
|
|
Tax = pool.get('account.invoice.tax')
|
|
type_name = cls.tip._field.sql_type().base
|
|
tax = Tax.__table__()
|
|
|
|
_, operator, value = clause
|
|
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 = tax.select(tax.invoice,
|
|
where=tax.invoice.in_(invoice_query),
|
|
group_by=tax.invoice,
|
|
having=Operator(Coalesce(Sum(tax.amount), 0).cast(type_name),
|
|
value))
|
|
return [('id', 'in', query)]
|
|
|
|
@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
|
|
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 _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 which NCF/e-NCF type must be used for the credit note.
|
|
|
|
- If the issuing company has e-CF enabled, use 34 even when the
|
|
affected invoice is a legacy NCF such as B01.
|
|
- Otherwise, if the original invoice is electronic, use 34.
|
|
- Otherwise use 4.
|
|
"""
|
|
if getattr(origin.company, 'ecf_engine_enabled', False):
|
|
return '34'
|
|
|
|
origin_code = cls._ncf_code(origin)
|
|
if origin_code in _ECF_ORIGIN_CODES:
|
|
return '34'
|
|
return '4'
|
|
|
|
|
|
@classmethod
|
|
def _ncf_resolve_debit_note_code(cls, origin):
|
|
"""
|
|
Resolve which NCF/e-NCF type must be used for the debit note.
|
|
|
|
- If the issuing company has e-CF enabled, use 33 even when the
|
|
affected invoice is a legacy NCF such as B01.
|
|
- Otherwise, if the original invoice is electronic, use 33.
|
|
- Otherwise use 3.
|
|
"""
|
|
if getattr(origin.company, 'ecf_engine_enabled', False):
|
|
return '33'
|
|
|
|
origin_code = cls._ncf_code(origin)
|
|
if origin_code in _ECF_ORIGIN_CODES:
|
|
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_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: 4 or 34.
|
|
- 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 = cls._ncf_resolve_credit_note_code(origin)
|
|
credit_note.ncf_type = ncf_code
|
|
|
|
if credit_note.type == 'out':
|
|
sequence = cls._ncf_find_active_sequence(credit_note, ncf_code)
|
|
|
|
if not sequence:
|
|
raise UserError(gettext(
|
|
'ncf_manager.msg_missing_credit_note_sequence',
|
|
ncf_type=ncf_code,
|
|
company=credit_note.company.rec_name))
|
|
|
|
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, values=None):
|
|
"""
|
|
Fiscal preparation for debit notes generated from an existing invoice.
|
|
"""
|
|
if not origin.ncf_number:
|
|
return
|
|
|
|
values = values or {}
|
|
|
|
debit_note.ncf_number_modified = origin.ncf_number
|
|
|
|
ncf_code = cls._ncf_resolve_debit_note_code(origin)
|
|
debit_note.ncf_type = ncf_code
|
|
|
|
if debit_note.type == 'out':
|
|
sequence = cls._ncf_find_active_sequence(debit_note, ncf_code)
|
|
|
|
if not sequence:
|
|
raise UserError(gettext(
|
|
'ncf_manager.msg_missing_debit_note_sequence',
|
|
ncf_type=ncf_code,
|
|
company=debit_note.company.rec_name))
|
|
|
|
debit_note.ncf_sequence = sequence
|
|
|
|
strict_sequence = sequence.ncf_sequences
|
|
if strict_sequence and not getattr(debit_note, 'expiration_date', None):
|
|
debit_note.expiration_date = strict_sequence.expiration_date
|
|
|
|
if hasattr(debit_note, 'revenue_type') and debit_note.type == 'out':
|
|
debit_note.revenue_type = debit_note.revenue_type or '01'
|
|
|
|
@classmethod
|
|
def _debit_prepare_note_from_origin(cls, origin, debit_note, values):
|
|
super()._debit_prepare_note_from_origin(origin, debit_note, values)
|
|
cls._ncf_prepare_debit_note_from_origin(origin, debit_note, values)
|
|
|
|
@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',
|
|
ncf_type=ncf_code,
|
|
company=invoice.company.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',
|
|
ncf_type=self.ncf_type,
|
|
company=self.company.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'))
|
|
|
|
def check_ncf_number_modified(self):
|
|
"""
|
|
Validate modified NCF only for fiscal/electronic credit notes.
|
|
"""
|
|
if not self.ncf_type:
|
|
return
|
|
|
|
if self.ncf_type not in _CREDIT_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'))
|
|
|
|
@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)
|
|
|
|
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="cancel_reason"]', 'states', {
|
|
'invisible': Eval('type') == 'in',
|
|
}),
|
|
]
|
|
|
|
|
|
@classmethod
|
|
def _post(cls, invoices):
|
|
"""
|
|
Validate and assign NCF before posting.
|
|
"""
|
|
cls.set_ncf_number(invoices)
|
|
|
|
for invoice in invoices:
|
|
if not invoice.ncf_type:
|
|
raise UserError(gettext('ncf_manager.msg_no_ncf_type'))
|
|
|
|
ncf_code = invoice.ncf_type
|
|
is_credit_note = cls._ncf_is_credit_note(invoice)
|
|
is_without_ncf = ncf_code == '99'
|
|
|
|
if invoice.type == 'out':
|
|
if is_credit_note:
|
|
invoice.check_credit_note()
|
|
invoice.check_ncf_number_modified()
|
|
invoice.check_payment_term()
|
|
invoice.check_payment_method()
|
|
invoice.check_revenue_type()
|
|
else:
|
|
invoice.check_invoice()
|
|
invoice.check_payment_term()
|
|
invoice.check_payment_method()
|
|
invoice.check_revenue_type()
|
|
|
|
else:
|
|
if is_without_ncf:
|
|
continue
|
|
|
|
if is_credit_note:
|
|
invoice.check_credit_note()
|
|
invoice.check_ncf_number_modified()
|
|
invoice.check_payment_term()
|
|
invoice.check_payment_method()
|
|
invoice.check_expense_type()
|
|
else:
|
|
invoice.check_payment_term()
|
|
invoice.check_expense_type()
|
|
invoice.check_isr_withholding_type()
|
|
invoice.check_payment_method()
|
|
|
|
super()._post(invoices)
|