From 937124bcaf9160f67fb88ae9015703039a07fc62 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Sun, 28 Aug 2022 12:24:25 +0200 Subject: [PATCH] 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. --> -