From 603a9d74774659a4b75422879731d9d7c9c0548d Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 25 Aug 2022 15:57:19 +0200 Subject: [PATCH 01/28] Version 6.0.2 --- README.rst | 8 ++++++++ tryton.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 97408f7..a4e2e94 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,14 @@ Requires Changes ======= +*6.0.2 - 25.08.2022* + +- add: split-booking + +*6.0.1 - 23.08.2022* + +- works + *6.0.0 - 05.08.2022* - init diff --git a/tryton.cfg b/tryton.cfg index dab4bf6..38229b2 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=6.0.0 +version=6.0.2 depends: res currency From 0aa9df2f1db2ad1bc424375c8fdbc4f1143b5850 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Fri, 26 Aug 2022 23:47:51 +0200 Subject: [PATCH 03/28] importer begonnen --- importer/import_qif.py | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 importer/import_qif.py diff --git a/importer/import_qif.py b/importer/import_qif.py new file mode 100644 index 0000000..00fca92 --- /dev/null +++ b/importer/import_qif.py @@ -0,0 +1,72 @@ +# -*- 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. + +from quiffen import Qif + +file_name = 'bargeld.qif' +company_name = 'm-ds' + +from proteus import config, Model +from datetime import date + +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 add_category(category, parent, company): + """ check or add category + """ + Category = Model.get('cashbook.category') + + query = [('name', '=', category.name)] + if parent is None: + query.append(('parent', '=', None)) + else : + query.append(('parent.id', '=', parent.id)) + + cat1 = Category.find(query) + if len(cat1) == 1: + cashbook_category = cat1[0] + print('- found:', cashbook_category.rec_name) + elif len(cat1) == 0: + print('- new-go:', getattr(parent, 'rec_name', '-'), category.name, category.income, category.expense) + cashbook_category, = Category.create([{ + 'name': category.name, + 'cattype': 'in' if category.income == True else 'out', + 'parent': getattr(parent, 'id', None), + }], context={'company': company.id}) + cashbook_category = Category(cashbook_category) + print('- new-ok:', cashbook_category.rec_name) + else : + raise ValueError('invalid num of category') + + for subcat in category.children: + add_category(subcat, cashbook_category, company) + +# end add_category + + +qif = Qif.parse(file_name, day_first=True) + +Company = Model.get('company.company') +company1, = Company.find([('rec_name', '=', company_name)]) + +# import categories +for catname in qif.categories.keys(): + category = qif.categories[catname] + add_category(category, None, company1) From 4df6284257275dcf5024ba22bc334fe0b6bd4088 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Sat, 27 Aug 2022 09:32:17 +0200 Subject: [PATCH 04/28] =?UTF-8?q?kategorie:=20domain-views,=20importer=20e?= =?UTF-8?q?rg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- category.xml | 41 ++++++++++++++++++++++++++++++++++ importer/import_qif.py | 50 ++++++++++++++++++++++++++++++++++-------- locale/de.po | 24 ++++++++++++++++++++ 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/category.xml b/category.xml index a5dcc66..896fcd8 100644 --- a/category.xml +++ b/category.xml @@ -42,6 +42,26 @@ full copyright notices and license terms. --> + + + Revenue + + + + + + Expense + + + + + + All + + + + + Category @@ -59,6 +79,27 @@ full copyright notices and license terms. --> + + + Revenue + + + + + + Expense + + + + + + All + + + + + + diff --git a/importer/import_qif.py b/importer/import_qif.py index 00fca92..0b569e5 100644 --- a/importer/import_qif.py +++ b/importer/import_qif.py @@ -3,13 +3,15 @@ # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. -from quiffen import Qif - file_name = 'bargeld.qif' +file_category_income = 'category_income.qif' +file_category_expense = 'category_expense.qif' company_name = 'm-ds' +from quiffen import Qif from proteus import config, Model from datetime import date +import json cfg1 = config.set_trytond( 'postgresql://postgres:test1@localhost:5432/tr44/', @@ -28,12 +30,16 @@ if not len(mod_lst) == len(need_modules): raise ValueError('einige module sind nicht aktiviert!') -def add_category(category, parent, company): +def add_category(category, parent, company, cattype): """ check or add category """ Category = Model.get('cashbook.category') - query = [('name', '=', category.name)] + query = [ + ('name', '=', category.name), + ('cattype', '=', cattype), + ] + if parent is None: query.append(('parent', '=', None)) else : @@ -47,7 +53,7 @@ def add_category(category, parent, company): print('- new-go:', getattr(parent, 'rec_name', '-'), category.name, category.income, category.expense) cashbook_category, = Category.create([{ 'name': category.name, - 'cattype': 'in' if category.income == True else 'out', + 'cattype': cattype, 'parent': getattr(parent, 'id', None), }], context={'company': company.id}) cashbook_category = Category(cashbook_category) @@ -56,17 +62,43 @@ def add_category(category, parent, company): raise ValueError('invalid num of category') for subcat in category.children: - add_category(subcat, cashbook_category, company) + add_category(subcat, cashbook_category, company, cattype) # end add_category -qif = Qif.parse(file_name, day_first=True) +def get_category_tree(categories): + """ convert categories in dict-tree + """ + result = [] + for category in categories: + cat1 = { + 'name': category.name, + 'type': 'in' if category.income == True else 'out' if category.expense == True else 'ups', + } + cat1['childs'] = get_category_tree(category.children) + result.append(cat1) + return result + +# end get_category_tree + Company = Model.get('company.company') company1, = Company.find([('rec_name', '=', company_name)]) -# import categories + +# income-category +qif = Qif.parse(file_category_income, day_first=True) +cat_tree = [] for catname in qif.categories.keys(): category = qif.categories[catname] - add_category(category, None, company1) + #cat_tree.extend(get_category_tree([category])) + add_category(category, None, company1, 'in') + +# expense-category +qif = Qif.parse(file_category_expense, day_first=True) +cat_tree = [] +for catname in qif.categories.keys(): + category = qif.categories[catname] + #cat_tree.extend(get_category_tree([category])) + add_category(category, None, company1, 'out') diff --git a/locale/de.po b/locale/de.po index 073d916..1148f46 100644 --- a/locale/de.po +++ b/locale/de.po @@ -318,6 +318,30 @@ msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all" msgid "All" msgstr "Alle" +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_in" +msgid "Revenue" +msgstr "Einnahmen" + +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_out" +msgid "Expense" +msgstr "Ausgaben" + +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_all" +msgid "All" +msgstr "Alle" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_in" +msgid "Revenue" +msgstr "Einnahmen" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out" +msgid "Expense" +msgstr "Ausgaben" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_all" +msgid "All" +msgstr "Alle" + ################### # ir.model.button # From 2fdee39611b4249a5f8d8e1f911819b2b0b51325 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Sun, 28 Aug 2022 12:24:25 +0200 Subject: [PATCH 05/28] kategorie: constraint gegen gleiche Namen auf toplevel, importer: list/erstellt kategorie, list transaktionen --- category.py | 13 ++- category.xml | 12 --- importer/import_qif.py | 219 +++++++++++++++++++++++++++++++---------- locale/de.po | 8 -- locale/en.po | 16 +++ tests/test_category.py | 14 ++- view/category_list.xml | 1 - view/category_tree.xml | 1 - 8 files changed, 207 insertions(+), 77 deletions(-) diff --git a/category.py b/category.py index 0d23eb8..ffaf75c 100644 --- a/category.py +++ b/category.py @@ -3,12 +3,13 @@ # 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, tree, sequence_ordered +from trytond.model import ModelView, ModelSQL, fields, Unique, Exclude, tree, sequence_ordered 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 sel_categorytype = [ @@ -51,7 +52,15 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): cls._order.insert(0, ('name', 'ASC')) t = cls.__table__() cls._sql_constraints.extend([ - ('name_uniq', Unique(t, t.name, t.company, t.parent), 'cashbook.msg_category_name_unique'), + ('name_uniq', + Unique(t, t.name, t.company, t.parent), + 'cashbook.msg_category_name_unique'), + ('name2_uniq', + Exclude(t, + (t.name, Equal), + (t.cattype, Equal), + where=(t.parent == None)), + 'cashbook.msg_category_name_unique'), ]) @classmethod diff --git a/category.xml b/category.xml index 896fcd8..516e4c7 100644 --- a/category.xml +++ b/category.xml @@ -55,12 +55,6 @@ full copyright notices and license terms. --> - - All - - - - @@ -92,12 +86,6 @@ full copyright notices and license terms. --> - - All - - - - diff --git a/importer/import_qif.py b/importer/import_qif.py index 0b569e5..8947899 100644 --- a/importer/import_qif.py +++ b/importer/import_qif.py @@ -4,14 +4,19 @@ # full copyright notices and license terms. file_name = 'bargeld.qif' -file_category_income = 'category_income.qif' -file_category_expense = 'category_expense.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/', @@ -30,53 +35,161 @@ if not len(mod_lst) == len(need_modules): raise ValueError('einige module sind nicht aktiviert!') -def add_category(category, parent, company, cattype): - """ check or add category +def qif_split_by_type(file_name): + """ split file by type """ - Category = Model.get('cashbook.category') + lines = [] + with open(file_name, 'r') as fhdl: + print('-- read file "%s"' % file_name) + lines = fhdl.readlines() - query = [ - ('name', '=', category.name), - ('cattype', '=', cattype), - ] + 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()) - if parent is None: - query.append(('parent', '=', None)) - else : - query.append(('parent.id', '=', parent.id)) + for block in blocks.keys(): + blocks[block] = '\n'.join(blocks[block]) + print ('-- found type: %s (%d bytes)' % (block, len(blocks[block]))) - cat1 = Category.find(query) - if len(cat1) == 1: - cashbook_category = cat1[0] - print('- found:', cashbook_category.rec_name) - elif len(cat1) == 0: - print('- new-go:', getattr(parent, 'rec_name', '-'), category.name, category.income, category.expense) - cashbook_category, = Category.create([{ - 'name': category.name, - 'cattype': cattype, - 'parent': getattr(parent, 'id', None), - }], context={'company': company.id}) - cashbook_category = Category(cashbook_category) - print('- new-ok:', cashbook_category.rec_name) - else : - raise ValueError('invalid num of category') + return blocks - for subcat in category.children: - add_category(subcat, cashbook_category, company, cattype) - -# end add_category +# end qif_split_by_type -def get_category_tree(categories): - """ convert categories in dict-tree +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 = [] - for category in categories: + + 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': category.name, - 'type': 'in' if category.income == True else 'out' if category.expense == True else 'ups', + 'name': cat_name2, + 'cattype': categories[cat_name]['type'], } - cat1['childs'] = get_category_tree(category.children) + + 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 @@ -86,19 +199,21 @@ def get_category_tree(categories): 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') -# income-category -qif = Qif.parse(file_category_income, day_first=True) -cat_tree = [] -for catname in qif.categories.keys(): - category = qif.categories[catname] - #cat_tree.extend(get_category_tree([category])) - add_category(category, None, company1, 'in') + 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') -# expense-category -qif = Qif.parse(file_category_expense, day_first=True) -cat_tree = [] -for catname in qif.categories.keys(): - category = qif.categories[catname] - #cat_tree.extend(get_category_tree([category])) - add_category(category, None, company1, 'out') +if 'Bank' in qif_type_data.keys(): + bookings = qif_read_bank(qif_type_data['Bank']) + + print('-- bookings:', bookings) diff --git a/locale/de.po b/locale/de.po index 1148f46..37bc9fb 100644 --- a/locale/de.po +++ b/locale/de.po @@ -326,10 +326,6 @@ msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_out" msgid "Expense" msgstr "Ausgaben" -msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_all" -msgid "All" -msgstr "Alle" - msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_in" msgid "Revenue" msgstr "Einnahmen" @@ -338,10 +334,6 @@ msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out" msgid "Expense" msgstr "Ausgaben" -msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_all" -msgid "All" -msgstr "Alle" - ################### # ir.model.button # diff --git a/locale/en.po b/locale/en.po index c6f7965..8606e32 100644 --- a/locale/en.po +++ b/locale/en.po @@ -294,6 +294,22 @@ msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all" msgid "All" msgstr "All" +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_in" +msgid "Revenue" +msgstr "Revenue" + +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_out" +msgid "Expense" +msgstr "Expense" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_in" +msgid "Revenue" +msgstr "Revenue" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out" +msgid "Expense" +msgstr "Expense" + msgctxt "model:ir.model.button,string:line_wfedit_button" msgid "Edit" msgstr "Edit" diff --git a/tests/test_category.py b/tests/test_category.py index bfea57e..f2e0b6e 100644 --- a/tests/test_category.py +++ b/tests/test_category.py @@ -90,6 +90,7 @@ class CategoryTestCase(ModuleTestCase): cat1, = Category.create([{ 'name': 'Test 1', 'description': 'Info', + 'cattype': 'in', }]) self.assertEqual(cat1.name, 'Test 1') self.assertEqual(cat1.rec_name, 'Test 1') @@ -97,10 +98,11 @@ class CategoryTestCase(ModuleTestCase): self.assertEqual(cat1.company.rec_name, 'm-ds') self.assertEqual(cat1.parent, None) - # duplicate, allowed + # duplicate of different type, allowed cat2, = Category.create([{ 'name': 'Test 1', 'description': 'Info', + 'cattype': 'out', }]) self.assertEqual(cat2.name, 'Test 1') self.assertEqual(cat2.rec_name, 'Test 1') @@ -108,6 +110,16 @@ class CategoryTestCase(ModuleTestCase): self.assertEqual(cat2.company.rec_name, 'm-ds') self.assertEqual(cat2.parent, None) + # deny duplicate of same type + self.assertRaisesRegex(UserError, + 'The category name already exists at this level.', + Category.create, + [{ + 'name': 'Test 1', + 'description': 'Info', + 'cattype': 'in', + }]) + @with_transaction() def test_category_create_nodupl_diff_level(self): """ create category diff --git a/view/category_list.xml b/view/category_list.xml index f3ca367..3e88e24 100644 --- a/view/category_list.xml +++ b/view/category_list.xml @@ -4,6 +4,5 @@ The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. --> - diff --git a/view/category_tree.xml b/view/category_tree.xml index ccc59bd..98449ad 100644 --- a/view/category_tree.xml +++ b/view/category_tree.xml @@ -4,7 +4,6 @@ The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. --> - From 559a5d06567c161900ed8c500136ad7b82b59ebf Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Mon, 29 Aug 2022 23:34:36 +0200 Subject: [PATCH 06/28] kategory: sortierung begnnen --- category.py | 27 +++++++++++++++++++++++++ tests/test_category.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/category.py b/category.py index ffaf75c..db33c78 100644 --- a/category.py +++ b/category.py @@ -10,6 +10,7 @@ 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 sel_categorytype = [ @@ -79,6 +80,32 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): def default_right(): return 0 + @staticmethod + def order_rec_name(tables): + """ order by pos + """ + Category2 = Pool().get('cashbook.category') + tab_cat = Category2.__table__() + table, _ = tables[None] + + categories = With('id', 'name', 'name_path', recursive=True) + categories.query = select(tab_cat.id, tab_cat.name, array[]) + +# ~ 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" + @fields.depends('parent', '_parent_parent.cattype') def on_change_with_parent_cattype(self, name=None): """ get type of parent category or None diff --git a/tests/test_category.py b/tests/test_category.py index f2e0b6e..d7910d6 100644 --- a/tests/test_category.py +++ b/tests/test_category.py @@ -28,6 +28,52 @@ class CategoryTestCase(ModuleTestCase): }]) return category + @with_transaction() + def test_category_check_rec_name(self): + """ create category, test rec_name, search, order + """ + pool = Pool() + Category = pool.get('cashbook.category') + company = self.prep_company() + + Category.create([{ + 'company': company.id, + 'name': 'Level 1', + 'cattype': 'in', + 'childs': [('create', [{ + 'company': company.id, + 'name': 'Level 2a', + 'cattype': 'in', + }, { + 'company': company.id, + 'name': 'Level 2b', + 'cattype': 'in', + }])], + }, { + 'company': company.id, + 'name': 'Level 1b', + 'cattype': 'in', + 'childs': [('create', [{ + 'company': company.id, + 'name': 'Level 1b.2a', + 'cattype': 'in', + }, { + 'company': company.id, + 'name': 'Level 1b.2b', + 'cattype': 'in', + }])], + }]) + + self.assertEqual(Category.search_count([ + ('rec_name', 'ilike', '%1b.2b%'), + ]), 1) + self.assertEqual(Category.search_count([ + ('rec_name', 'ilike', '%1b.2%'), + ]), 2) + self.assertEqual(Category.search_count([ + ('rec_name', '=', 'Level 1b/Level 1b.2b'), + ]), 1) + @with_transaction() def test_category_create_check_category_type(self): """ create category, update type of category From 619a4e9ed61a2a969058a888525ae63223d229ec Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 30 Aug 2022 11:56:27 +0200 Subject: [PATCH 07/28] 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. -->