Files
ncf_manager/party.py
T

249 lines
8.1 KiB
Python
Raw Normal View History

2026-07-22 21:27:53 -04:00
# 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 sql import Literal, Null
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from trytond import backend
from trytond.i18n import gettext
from trytond.model import ModelSQL, ValueMixin, fields
from trytond.modules.company.model import (
CompanyMultiValueMixin, CompanyValueMixin)
from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, If
from trytond.tools import grouped_slice, reduce_ids
from trytond.transaction import Transaction
from trytond.modules.ncf_manager.ncf_manager import NCF_TYPE
import logging
logger = logging.getLogger(__name__)
def _backend_name():
name = backend.name
return name() if callable(name) else name
customer_ncf_type = fields.Selection(
NCF_TYPE,
"Customer NCF Type",
sort=False
)
supplier_ncf_type = fields.Selection(
NCF_TYPE,
"Supplier NCF Type",
sort=False
)
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
customer_ncf_type = fields.MultiValue(customer_ncf_type)
supplier_ncf_type = fields.MultiValue(supplier_ncf_type)
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field in {'customer_ncf_type', 'supplier_ncf_type'}:
return pool.get('party.party.ncf_type')
return super(Party, cls).multivalue_model(field)
@classmethod
def default_customer_ncf_type(cls, **pattern):
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
return config.get_multivalue(
'default_customer_ncf_type', **pattern)
@classmethod
def default_supplier_ncf_type(cls, **pattern):
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
return config.get_multivalue(
'default_supplier_ncf_type', **pattern)
class PartyNcfType(ModelSQL, ValueMixin):
"Party Ncf Type"
__name__ = 'party.party.ncf_type'
party = fields.Many2One(
'party.party', "Party", ondelete='CASCADE')
customer_ncf_type = customer_ncf_type
supplier_ncf_type = supplier_ncf_type
@classmethod
def __register__(cls, module_name):
"""
Migrate legacy party NCF fields safely.
Old structure:
party_party_ncf_type.customer_ncf_type = ncf.manager.sequence.id
party_party_ncf_type.supplier_ncf_type = ncf.manager.sequence.id
New structure:
party_party_ncf_type.customer_ncf_type = NCF type code
party_party_ncf_type.supplier_ncf_type = NCF type code
The old integer columns must be renamed before super().__register__(),
otherwise Tryton tries to convert int4 -> varchar directly.
"""
cls._ncf_prepare_legacy_party_columns()
super().__register__(module_name)
cls._ncf_migrate_party_ncf_types_from_legacy_columns()
@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_party_columns(cls):
"""
Rename old integer columns before Tryton creates the new Selection
columns.
"""
cursor = Transaction().connection.cursor()
table_name = 'party_party_ncf_type'
columns = (
('customer_ncf_type', 'customer_ncf_type_legacy_sequence_id'),
('supplier_ncf_type', 'supplier_ncf_type_legacy_sequence_id'),
)
for old_column, legacy_column in columns:
if not cls._ncf_column_exists(cursor, table_name, old_column):
continue
if cls._ncf_column_exists(cursor, table_name, legacy_column):
continue
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_party_ncf_types_from_legacy_columns(cls):
"""
Convert legacy party NCF sequence IDs into real NCF type values.
Example:
customer_ncf_type_legacy_sequence_id = 1
ncf_manager_sequence.id = 1
ncf_manager_sequence.ncf_type = '31'
Result:
customer_ncf_type = '31'
"""
cursor = Transaction().connection.cursor()
table_name = 'party_party_ncf_type'
columns = (
('customer_ncf_type', 'customer_ncf_type_legacy_sequence_id'),
('supplier_ncf_type', 'supplier_ncf_type_legacy_sequence_id'),
)
for new_column, legacy_column in columns:
if not cls._ncf_column_exists(cursor, table_name, legacy_column):
logger.info(
"No legacy column %s found in %s. Skipping migration.",
legacy_column,
table_name,
)
continue
logger.info(
"Migrating party field %s from legacy sequence ID to "
"fiscal NCF type code",
new_column,
)
cursor.execute(f"""
UPDATE {table_name} AS party_ncf
SET {new_column} = seq.ncf_type
FROM ncf_manager_sequence AS seq
WHERE party_ncf.{legacy_column} IS NOT NULL
AND party_ncf.{legacy_column} = seq.id
AND seq.ncf_type IS NOT NULL
AND seq.ncf_type <> ''
AND (
party_ncf.{new_column} IS NULL
OR party_ncf.{new_column} = ''
OR party_ncf.{new_column} <> seq.ncf_type
)
""")
logger.info(
"Migrated %s rows for party field %s",
cursor.rowcount,
new_column,
)
cursor.execute(f"""
SELECT COUNT(*)
FROM {table_name} AS party_ncf
LEFT JOIN ncf_manager_sequence AS seq
ON party_ncf.{legacy_column} = seq.id
WHERE party_ncf.{legacy_column} IS NOT NULL
AND seq.id IS NULL
""")
unmatched = cursor.fetchone()[0]
if unmatched:
logger.warning(
"%s rows in %s have legacy values in %s that do not "
"match any ncf_manager_sequence.id",
unmatched,
table_name,
legacy_column,
)