From 64e9bab592e18516f4b7fac5e4465954a58c30c5 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 30 Aug 2022 11:56:27 +0200 Subject: [PATCH] kategorie: hierarchische sortierung, sequence-spalte entfernt --- category.py | 85 ++++++++++++---- importer/import_qif.py | 219 ----------------------------------------- tests/test_category.py | 20 ++++ view/category_form.xml | 3 +- view/category_list.xml | 3 +- view/category_tree.xml | 3 +- 6 files changed, 88 insertions(+), 245 deletions(-) delete mode 100644 importer/import_qif.py diff --git a/category.py b/category.py index db33c78..477016c 100644 --- a/category.py +++ b/category.py @@ -3,14 +3,46 @@ # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. -from trytond.model import ModelView, ModelSQL, fields, Unique, Exclude, tree, sequence_ordered +from trytond.model import ModelView, ModelSQL, fields, Unique, Exclude, tree from trytond.transaction import Transaction from trytond.pool import Pool from trytond.pyson import Eval, If, Bool from trytond.exceptions import UserError from trytond.i18n import gettext from sql.operators import Equal -from sql import With +from sql.functions import Function +from sql import With, Literal + + +class ArrayApppend(Function): + """ sql: array_append + """ + __slots__ = () + _function = 'ARRAY_APPEND' + +# end ArrayApppend + + +class ArrayToString(Function): + """ sql: array_to_string + """ + __slots__ = () + _function = 'ARRAY_TO_STRING' + +# end ArrayToString + + +class Array(Function): + """ sql: array-type + """ + __slots__ = () + _function = 'ARRAY' + + def __str__(self): + return self._function + '[' + ', '.join( + map(self._format, self.args)) + ']' + +# end Array sel_categorytype = [ @@ -19,7 +51,7 @@ sel_categorytype = [ ] -class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): +class Category(tree(separator='/'), ModelSQL, ModelView): 'Category' __name__ = 'cashbook.category' @@ -38,7 +70,6 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): company = fields.Many2One(string='Company', model_name='company.company', required=True, ondelete="RESTRICT") - sequence = fields.Integer(string='Sequence', select=True) parent = fields.Many2One(string="Parent", model_name='cashbook.category', ondelete='RESTRICT', left='left', right='right') @@ -47,10 +78,15 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): left = fields.Integer(string='Left', required=True, select=True) right = fields.Integer(string='Right', required=True, select=True) + @classmethod + def __register__(cls, module_name): + super(Category, cls).__register__(module_name) + cls.migrate_sequence(module_name) + @classmethod def __setup__(cls): super(Category, cls).__setup__() - cls._order.insert(0, ('name', 'ASC')) + cls._order.insert(0, ('rec_name', 'ASC')) t = cls.__table__() cls._sql_constraints.extend([ ('name_uniq', @@ -64,6 +100,13 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): 'cashbook.msg_category_name_unique'), ]) + @classmethod + def migrate_sequence(cls, module_name): + """ remove colum 'sequence' + """ + table = cls.__table_handler__(module_name) + table.drop_column('sequence') + @classmethod def default_cattype(cls): return 'out' @@ -83,28 +126,30 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): @staticmethod def order_rec_name(tables): """ order by pos + a recursive sorting """ Category2 = Pool().get('cashbook.category') tab_cat = Category2.__table__() + tab_cat2 = Category2.__table__() table, _ = tables[None] categories = With('id', 'name', 'name_path', recursive=True) - categories.query = select(tab_cat.id, tab_cat.name, array[]) + categories.query = tab_cat.select( + tab_cat.id, tab_cat.name, Array(tab_cat.name), + where = tab_cat.parent==None, + ) + categories.query |= tab_cat2.join(categories, + condition=categories.id==tab_cat2.parent, + ).select( + tab_cat2.id, tab_cat2.name, ArrayApppend(categories.name_path, tab_cat2.name), + ) + categories.query.all_ = True -# ~ with recursive categories (id, level, name, name_path) as ( - # ~ select "a"."id", 0, "a"."name", array["a"."name"] - # ~ from cashbook_category as "a" - # ~ where "a"."parent" is null - - # ~ union all - - # ~ select "b"."id", "c"."level" + 1, "b"."name", array_append("c"."name_path", "b"."name") - # ~ from cashbook_category as "b" - # ~ inner join categories as "c" on "c"."id" = "b"."parent" -# ~ ) -# ~ select "d"."id", array_to_string("d"."name_path", '/') as "rec_name" -# ~ from categories as "d" -# ~ order by "rec_name" + query = categories.select( + ArrayToString(categories.name_path, '/').as_('rec_name'), + where = table.id==categories.id, + with_ = [categories]) + return [query] @fields.depends('parent', '_parent_parent.cattype') def on_change_with_parent_cattype(self, name=None): diff --git a/importer/import_qif.py b/importer/import_qif.py deleted file mode 100644 index 8947899..0000000 --- a/importer/import_qif.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of the cashbook-module from m-ds for Tryton. -# The COPYRIGHT file at the top level of this repository contains the -# full copyright notices and license terms. - -file_name = 'bargeld.qif' -company_name = 'm-ds' - -replace_catnames = [ - ('Musik/Kino', 'Musik, Kino'), - ] - -from quiffen import Qif -from proteus import config, Model -from datetime import date -import json -from datetime import datetime -from decimal import Decimal - - -cfg1 = config.set_trytond( - 'postgresql://postgres:test1@localhost:5432/tr44/', - user='admin', - config_file='/home/trytproj/projekt/py3tr60/etc/trytond.conf') - -# check active modules -need_modules = ['cashbook', 'party', 'company'] - -IrModule = Model.get('ir.module') -mod_lst = IrModule.find([ - ('state', '=', 'activated'), - ('name', 'in', need_modules), - ]) -if not len(mod_lst) == len(need_modules): - raise ValueError('einige module sind nicht aktiviert!') - - -def qif_split_by_type(file_name): - """ split file by type - """ - lines = [] - with open(file_name, 'r') as fhdl: - print('-- read file "%s"' % file_name) - lines = fhdl.readlines() - - blocks = {} - current_type = None - for line in lines: - if line.startswith('!Type:'): - current_type = line[len('!Type:'):].strip() - else : - if not current_type in blocks.keys(): - blocks[current_type] = [] - blocks[current_type].append(line.strip()) - - for block in blocks.keys(): - blocks[block] = '\n'.join(blocks[block]) - print ('-- found type: %s (%d bytes)' % (block, len(blocks[block]))) - - return blocks - -# end qif_split_by_type - - -def qif_read_categories(catdata): - """ read categories from text - """ - def add_category(catdict, namelst, ctype): - """ add category to dict - """ - if not namelst[0] in catdict.keys(): - catdict[namelst[0]] = {'type': ctype, 'childs': {}} - - if len(namelst) > 1: - catdict[namelst[0]]['childs'] = add_category( - catdict[namelst[0]]['childs'], - [namelst[1]], - ctype) - return catdict - - categories = {'in': {}, 'out': {}} - for cattxt in catdata.split('^'): - if len(cattxt.strip()) == 0: - continue - catname = None - cattype = None - for line in cattxt.strip().split('\n'): - if line.startswith('N'): - catname = line[1:].strip().split(':') - elif line.startswith('E'): - cattype = 'out' - elif line.startswith('I'): - cattype = 'in' - else : - raise ValueError('invalid line: %s (%s)' % (line, cattxt)) - categories[cattype] = add_category(categories[cattype], catname, cattype) - - return categories - -# end qif_read_categories - - -def qif_read_bank(bankdata): - """ read content of bookings - """ - result = [] - - def get_amount_from_txt(amount_txt): - """ convert text to Decimal - """ - if (',' in amount_txt) and (amount_txt[-3] == '.'): - # '.' = decimal, ',' = tousand - amount_txt = amount_txt.replace(',', '.') - elif ('.' in amount_txt) and (amount_txt[-3] == ','): - # ',' = decimal, '.' = tousand - amount_txt = amount_txt.replace('.', '') - amount_txt = amount_txt.replace(',', '.') - elif ',' in amount_txt: - amount_txt = amount_txt.replace(',', '.') - return Decimal(amount_txt) - - for booktxt in bankdata.split('^'): - if len(booktxt.strip()) == 0: - continue - - booking = {'split': []} - for line in booktxt.strip().split('\n'): - line_txt = line[1:].strip() - if line.startswith('D'): # date - booking['date'] = datetime.strptime(line_txt, '%d.%m.%Y').date() - elif line.startswith('T'): # total - booking['amount'] = get_amount_from_txt(line_txt) - elif line.startswith('U'): # total - booking['amount'] = get_amount_from_txt(line_txt) - elif line.startswith('P'): # party - booking['party'] = line_txt - elif line.startswith('A'): # address - booking['address'] = line_txt - elif line.startswith('N'): # address - booking['checknumber'] = line_txt - elif line.startswith('M'): # memo - booking['description'] = line_txt - elif line.startswith('C'): # state - booking['state'] = { - 'X': 'check', - '*': 'edit', - }.get(line_txt, 'edit') - elif line.startswith('L'): # category, account - if line_txt.startswith('[') and line_txt.endswith(']'): - booking['account'] = line_txt[1:-1] - else : - booking['category'] = line_txt - elif line.startswith('S'): # split: category - booking['split'].append({ - 'category': line_txt, - }) - elif line.startswith('E'): # split: memo - booking['split'][-1]['description'] = line_txt - elif line.startswith('$'): # split: amount - booking['split'][-1]['amount'] = get_amount_from_txt(line_txt) - elif line.startswith('£'): # split: amount - booking['split'][-1]['amount'] = get_amount_from_txt(line_txt) - else : - raise ValueError('unknown line-code: %s' % (line)) - - result.append(booking) - return result - -# end qif_read_bank - - -def get_category_create(categories, cattype): - """ convert categories in create-list - """ - result = [] - for cat_name in categories.keys(): - cat_name2 = cat_name - for repl in replace_catnames: - cat_name2 = cat_name.replace(repl[0], repl[1]) - - if cattype != categories[cat_name]['type']: - raise ValueError('cattype dont match') - - cat1 = { - 'name': cat_name2, - 'cattype': categories[cat_name]['type'], - } - - if len(categories[cat_name]['childs']) > 0: - childs = get_category_create(categories[cat_name]['childs'], cattype) - if len(childs) > 0: - cat1['childs'] = [('create', childs)] - result.append(cat1) - return result - -# end get_category_tree - - -Company = Model.get('company.company') -company1, = Company.find([('rec_name', '=', company_name)]) - -qif_type_data = qif_split_by_type(file_name) -if 'Cat' in qif_type_data.keys(): - Category = Model.get('cashbook.category') - - categories = qif_read_categories(qif_type_data['Cat']) - to_create = get_category_create(categories['in'], 'in') - to_create.extend(get_category_create(categories['out'], 'out')) - if len(to_create) > 0: - try : - catlst = Category.create(to_create, context={'company': company1.id}) - print('-- created %d categories' % len(catlst)) - except: - print('-- categories alredy exist') - -if 'Bank' in qif_type_data.keys(): - bookings = qif_read_bank(qif_type_data['Bank']) - - print('-- bookings:', bookings) diff --git a/tests/test_category.py b/tests/test_category.py index d7910d6..898e699 100644 --- a/tests/test_category.py +++ b/tests/test_category.py @@ -74,6 +74,26 @@ class CategoryTestCase(ModuleTestCase): ('rec_name', '=', 'Level 1b/Level 1b.2b'), ]), 1) + # ordering #1 + categories = Category.search([], order=[('rec_name', 'ASC')]) + self.assertEqual(len(categories), 6) + self.assertEqual(categories[0].rec_name, 'Level 1') + self.assertEqual(categories[1].rec_name, 'Level 1b') + self.assertEqual(categories[2].rec_name, 'Level 1b/Level 1b.2a') + self.assertEqual(categories[3].rec_name, 'Level 1b/Level 1b.2b') + self.assertEqual(categories[4].rec_name, 'Level 1/Level 2a') + self.assertEqual(categories[5].rec_name, 'Level 1/Level 2b') + + # ordering #2 + categories = Category.search([], order=[('rec_name', 'DESC')]) + self.assertEqual(len(categories), 6) + self.assertEqual(categories[0].rec_name, 'Level 1/Level 2b') + self.assertEqual(categories[1].rec_name, 'Level 1/Level 2a') + self.assertEqual(categories[2].rec_name, 'Level 1b/Level 1b.2b') + self.assertEqual(categories[3].rec_name, 'Level 1b/Level 1b.2a') + self.assertEqual(categories[4].rec_name, 'Level 1b') + self.assertEqual(categories[5].rec_name, 'Level 1') + @with_transaction() def test_category_create_check_category_type(self): """ create category, update type of category diff --git a/view/category_form.xml b/view/category_form.xml index 093c4d6..9ea7a6e 100644 --- a/view/category_form.xml +++ b/view/category_form.xml @@ -18,8 +18,7 @@ full copyright notices and license terms. -->