From 052b3328e4371211359e042ff4a5d3edeea2e248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Fri, 2 Jan 2015 14:04:55 +0100 Subject: [PATCH] Use One2Many for analytic accounts issue4217 review11511002 --- CHANGELOG | 2 + __init__.py | 3 +- account.py | 252 ++++++++++++++++----------- account.xml | 11 ++ view/analytic_account_entry_form.xml | 11 ++ view/analytic_account_entry_tree.xml | 8 + 6 files changed, 186 insertions(+), 101 deletions(-) create mode 100644 view/analytic_account_entry_form.xml create mode 100644 view/analytic_account_entry_tree.xml diff --git a/CHANGELOG b/CHANGELOG index 0e40e321b5..f8ac0b78f0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Use One2Many for analytic accounts + Version 3.4.0 - 2014-10-20 * Bug fixes (see mercurial logs for details) diff --git a/__init__.py b/__init__.py index 0d1252f3d1..5490b45901 100644 --- a/__init__.py +++ b/__init__.py @@ -10,8 +10,7 @@ def register(): Pool.register( Account, OpenChartAccountStart, - AccountSelection, - AccountAccountSelection, + AnalyticAccountEntry, Line, MoveLine, module='analytic_account', type_='model') diff --git a/account.py b/account.py index 86b3807b4e..7e2647c3c4 100644 --- a/account.py +++ b/account.py @@ -1,11 +1,11 @@ # 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 -import copy from sql import Column from sql.aggregate import Sum from sql.conditionals import Coalesce +from trytond import backend from trytond.model import ModelView, ModelSQL, fields from trytond.wizard import Wizard, StateView, StateAction, Button from trytond.pyson import Eval, PYSONEncoder @@ -13,7 +13,7 @@ from trytond.transaction import Transaction from trytond.pool import Pool __all__ = ['Account', 'OpenChartAccountStart', 'OpenChartAccount', - 'AccountSelection', 'AccountAccountSelection'] + 'AnalyticAccountEntry', 'AnalyticMixin'] class Account(ModelSQL, ModelView): @@ -274,63 +274,6 @@ class Account(ModelSQL, ModelView): else: return [(cls._rec_name,) + tuple(clause[1:])] - @classmethod - def convert_view(cls, tree): - res = tree.xpath('//field[@name=\'analytic_accounts\']') - if not res: - return - element_accounts = res[0] - - root_accounts = cls.search([ - ('parent', '=', None), - ]) - if not root_accounts: - element_accounts.getparent().getparent().remove( - element_accounts.getparent()) - return - for account in root_accounts: - newelement = copy.copy(element_accounts) - newelement.tag = 'label' - newelement.set('name', 'analytic_account_' + str(account.id)) - element_accounts.addprevious(newelement) - newelement = copy.copy(element_accounts) - newelement.set('name', 'analytic_account_' + str(account.id)) - element_accounts.addprevious(newelement) - parent = element_accounts.getparent() - parent.remove(element_accounts) - - @classmethod - def analytic_accounts_fields_get(cls, field, fields_names=None, - states=None, required_states=None): - res = {} - if fields_names is None: - fields_names = [] - if states is None: - states = {} - - encoder = PYSONEncoder() - - root_accounts = cls.search([ - ('parent', '=', None), - ]) - for account in root_accounts: - name = 'analytic_account_' + str(account.id) - if name in fields_names or not fields_names: - res[name] = field.copy() - field_states = states.copy() - if account.mandatory: - if required_states: - field_states['required'] = required_states - else: - field_states['required'] = True - res[name]['states'] = encoder.encode(field_states) - res[name]['string'] = account.name - res[name]['relation'] = cls.__name__ - res[name]['domain'] = PYSONEncoder().encode([ - ('root', '=', account.id), - ('type', '=', 'normal')]) - return res - class OpenChartAccountStart(ModelView): 'Open Chart of Accounts' @@ -360,55 +303,166 @@ class OpenChartAccount(Wizard): return 'end' -class AccountSelection(ModelSQL, ModelView): - 'Analytic Account Selection' - __name__ = 'analytic_account.account.selection' +class AnalyticAccountEntry(ModelView, ModelSQL): + 'Analytic Account Entry' + __name__ = 'analytic.account.entry' + origin = fields.Reference('Origin', selection='get_origin', select=True) + root = fields.Many2One('analytic_account.account', 'Root Analytic', + domain=[('type', '=', 'root')]) + account = fields.Many2One('analytic_account.account', 'Account', + ondelete='RESTRICT', required=True, + states={ + 'required': Eval('required', False), + }, + domain=[ + ('root', '=', Eval('root')), + ('type', '=', 'normal'), + ], + depends=['root', 'required']) + required = fields.Function(fields.Boolean('Required'), + 'on_change_with_required') - accounts = fields.Many2Many( - 'analytic_account.account-analytic_account.account.selection', - 'selection', 'account', 'Accounts') + @classmethod + def __register__(cls, module_name): + pool = Pool() + Account = pool.get('analytic_account.account') + TableHandler = backend.get('TableHandler') + cursor = Transaction().cursor + + # Migration from 3.4: use origin as the key for One2Many + migration_3_4 = False + old_table = 'analytic_account_account_selection_rel' + if TableHandler.table_exist(cursor, old_table): + TableHandler.table_rename(cursor, old_table, cls._table) + migration_3_4 = True + + super(AnalyticAccountEntry, cls).__register__(module_name) + + # Migration from 3.4: set root value + if migration_3_4: + account = Account.__table__() + cursor.execute(*account.select(account.id, account.root, + where=account.type != 'root')) + entry = cls.__table__() + for account_id, root_id in cursor.fetchall(): + cursor.execute(*entry.update( + columns=[entry.root], + values=[root_id], + where=entry.account == account_id)) @classmethod def __setup__(cls): - super(AccountSelection, cls).__setup__() + super(AnalyticAccountEntry, cls).__setup__() + cls._sql_constraints += [ + ('root_origin_uniq', 'UNIQUE(origin, root)', + 'Only one account is allowed per analytic root and origin.'), + ] + + @classmethod + def _get_origin(cls): + return [] + + @classmethod + def get_origin(cls): + Model = Pool().get('ir.model') + models = cls._get_origin() + models = Model.search([ + ('model', 'in', models), + ]) + return [(None, '')] + [(m.model, m.name) for m in models] + + @fields.depends('root') + def on_change_with_required(self, name=None): + if self.root: + return self.root.mandatory + return False + + +class AnalyticMixin(ModelSQL): + + analytic_accounts = fields.One2Many('analytic.account.entry', 'origin', + 'Analytic Accounts', + size=Eval('analytic_accounts_size', 0), + depends=['analytic_accounts_size']) + analytic_accounts_size = fields.Function(fields.Integer( + 'Analytic Accounts Size'), 'get_analytic_accounts_size') + + @classmethod + def __setup__(cls): + super(AnalyticMixin, cls).__setup__() cls._error_messages.update({ - 'root_account': ('Can not have many accounts with the same ' - 'root or a missing mandatory root account on "%s".'), + 'root_account': ('Some mandatory root account are missing ' + 'on "%(name)s"'), }) @classmethod - def validate(cls, selections): - super(AccountSelection, cls).validate(selections) - cls.check_root(selections) + def __register__(cls, module_name): + pool = Pool() + AccountEntry = pool.get('analytic.account.entry') + TableHandler = backend.get('TableHandler') + cursor = Transaction().cursor + + super(AnalyticMixin, cls).__register__(module_name) + + handler = TableHandler(cursor, cls, module_name) + # Migration from 3.4: analytic accounting changed to reference field + if handler.column_exist('analytic_accounts'): + entry = AccountEntry.__table__() + table = cls.__table__() + cursor.execute(*table.select(table.id, table.analytic_accounts, + where=table.analytic_accounts != None)) + for line_id, selection_id in cursor.fetchall(): + cursor.execute(*entry.update( + columns=[entry.origin], + values=['%s,%s' % (cls.__name__, line_id)], + where=entry.selection == selection_id)) + handler.drop_column('analytic_accounts') + + @staticmethod + def default_analytic_accounts(): + pool = Pool() + AnalyticAccount = pool.get('analytic_account.account') + + accounts = [] + root_accounts = AnalyticAccount.search([ + ('parent', '=', None), + ]) + for account in root_accounts: + accounts.append({ + 'required': account.mandatory, + 'root_name': account.name, + 'root': account.id, + }) + return accounts + + @staticmethod + def default_analytic_accounts_size(): + pool = Pool() + AnalyticAccount = pool.get('analytic_account.account') + return len(AnalyticAccount.search([('type', '=', 'root')])) @classmethod - def check_root(cls, selections): - "Check Root" - Account = Pool().get('analytic_account.account') + def get_analytic_accounts_size(cls, records, name): + roots = cls.default_analytic_accounts_size() + return {r.id: roots for r in records} - root_accounts = Account.search([ - ('parent', '=', None), - ]) + @classmethod + def validate(cls, analytics): + super(AnalyticMixin, cls).validate(analytics) + cls.check_roots(analytics) - for selection in selections: - roots = [] - for account in selection.accounts: - if account.root.id in roots: - cls.raise_user_error('root_account', (account.rec_name,)) - roots.append(account.root.id) - if Transaction().user: # Root can by pass - for account in root_accounts: - if account.mandatory: - if account.id not in roots: - cls.raise_user_error('root_account', - (account.rec_name,)) - - -class AccountAccountSelection(ModelSQL): - 'Analytic Account - Analytic Account Selection' - __name__ = 'analytic_account.account-analytic_account.account.selection' - _table = 'analytic_account_account_selection_rel' - selection = fields.Many2One('analytic_account.account.selection', - 'Selection', ondelete='CASCADE', required=True, select=True) - account = fields.Many2One('analytic_account.account', 'Account', - ondelete='RESTRICT', required=True, select=True) + @classmethod + def check_roots(cls, analytics): + "Check that all mandatory root entries are defined in entries" + pool = Pool() + Account = pool.get('analytic_account.account') + mandatory_roots = {a for a in Account.search([ + ('type', '=', 'root'), + ('mandatory', '=', True), + ])} + for analytic in analytics: + analytic_roots = {e.root for e in analytic.analytic_accounts} + if not mandatory_roots <= analytic_roots: + cls.raise_user_error('root_account', { + 'name': analytic.rec_name, + }) diff --git a/account.xml b/account.xml index 0264b3baee..c29f128dcf 100644 --- a/account.xml +++ b/account.xml @@ -109,5 +109,16 @@ this repository contains the full copyright notices and license terms. --> form open_chart_start_form + + + analytic.account.entry + form + analytic_account_entry_form + + + analytic.account.entry + tree + analytic_account_entry_tree + diff --git a/view/analytic_account_entry_form.xml b/view/analytic_account_entry_form.xml new file mode 100644 index 0000000000..63bdaa9a03 --- /dev/null +++ b/view/analytic_account_entry_form.xml @@ -0,0 +1,11 @@ + + +
+