kategorie: constraint gegen gleiche Namen auf toplevel,

importer: list/erstellt kategorie, list transaktionen
This commit is contained in:
Frederik Jaeckel 2022-08-28 12:24:25 +02:00
parent 532d9cc7c8
commit 937124bcaf
8 changed files with 207 additions and 77 deletions

View file

@ -3,12 +3,13 @@
# The COPYRIGHT file at the top level of this repository contains the # The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms. # 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.transaction import Transaction
from trytond.pool import Pool from trytond.pool import Pool
from trytond.pyson import Eval, If, Bool from trytond.pyson import Eval, If, Bool
from trytond.exceptions import UserError from trytond.exceptions import UserError
from trytond.i18n import gettext from trytond.i18n import gettext
from sql.operators import Equal
sel_categorytype = [ sel_categorytype = [
@ -51,7 +52,15 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
cls._order.insert(0, ('name', 'ASC')) cls._order.insert(0, ('name', 'ASC'))
t = cls.__table__() t = cls.__table__()
cls._sql_constraints.extend([ 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 @classmethod

View file

@ -55,12 +55,6 @@ full copyright notices and license terms. -->
<field name="domain" eval="[('cattype', '=', 'out')]" pyson="1"/> <field name="domain" eval="[('cattype', '=', 'out')]" pyson="1"/>
<field name="act_window" ref="act_category_list"/> <field name="act_window" ref="act_category_list"/>
</record> </record>
<record model="ir.action.act_window.domain" id="act_category_list_domain_all">
<field name="name">All</field>
<field name="sequence" eval="999"/>
<field name="domain"/>
<field name="act_window" ref="act_category_list"/>
</record>
<!-- action view - tree --> <!-- action view - tree -->
<record model="ir.action.act_window" id="act_category_tree"> <record model="ir.action.act_window" id="act_category_tree">
@ -92,12 +86,6 @@ full copyright notices and license terms. -->
<field name="domain" eval="[('cattype', '=', 'out')]" pyson="1"/> <field name="domain" eval="[('cattype', '=', 'out')]" pyson="1"/>
<field name="act_window" ref="act_category_tree"/> <field name="act_window" ref="act_category_tree"/>
</record> </record>
<record model="ir.action.act_window.domain" id="act_category_tree_domain_all">
<field name="name">All</field>
<field name="sequence" eval="999"/>
<field name="domain"/>
<field name="act_window" ref="act_category_tree"/>
</record>
<!-- permission --> <!-- permission -->

View file

@ -4,14 +4,19 @@
# full copyright notices and license terms. # full copyright notices and license terms.
file_name = 'bargeld.qif' file_name = 'bargeld.qif'
file_category_income = 'category_income.qif'
file_category_expense = 'category_expense.qif'
company_name = 'm-ds' company_name = 'm-ds'
replace_catnames = [
('Musik/Kino', 'Musik, Kino'),
]
from quiffen import Qif from quiffen import Qif
from proteus import config, Model from proteus import config, Model
from datetime import date from datetime import date
import json import json
from datetime import datetime
from decimal import Decimal
cfg1 = config.set_trytond( cfg1 = config.set_trytond(
'postgresql://postgres:test1@localhost:5432/tr44/', '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!') raise ValueError('einige module sind nicht aktiviert!')
def add_category(category, parent, company, cattype): def qif_split_by_type(file_name):
""" check or add category """ 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 = [ blocks = {}
('name', '=', category.name), current_type = None
('cattype', '=', cattype), for line in lines:
] if line.startswith('!Type:'):
current_type = line[len('!Type:'):].strip()
if parent is None:
query.append(('parent', '=', None))
else : else :
query.append(('parent.id', '=', parent.id)) if not current_type in blocks.keys():
blocks[current_type] = []
blocks[current_type].append(line.strip())
cat1 = Category.find(query) for block in blocks.keys():
if len(cat1) == 1: blocks[block] = '\n'.join(blocks[block])
cashbook_category = cat1[0] print ('-- found type: %s (%d bytes)' % (block, len(blocks[block])))
print('- found:', cashbook_category.rec_name)
elif len(cat1) == 0: return blocks
print('- new-go:', getattr(parent, 'rec_name', '-'), category.name, category.income, category.expense)
cashbook_category, = Category.create([{ # end qif_split_by_type
'name': category.name,
'cattype': cattype,
'parent': getattr(parent, 'id', None), def qif_read_categories(catdata):
}], context={'company': company.id}) """ read categories from text
cashbook_category = Category(cashbook_category) """
print('- new-ok:', cashbook_category.rec_name) 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 : else :
raise ValueError('invalid num of category') raise ValueError('invalid line: %s (%s)' % (line, cattxt))
categories[cattype] = add_category(categories[cattype], catname, cattype)
for subcat in category.children: return categories
add_category(subcat, cashbook_category, company, cattype)
# end add_category # end qif_read_categories
def get_category_tree(categories): def qif_read_bank(bankdata):
""" convert categories in dict-tree """ read content of bookings
""" """
result = [] 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 = { cat1 = {
'name': category.name, 'name': cat_name2,
'type': 'in' if category.income == True else 'out' if category.expense == True else 'ups', '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) result.append(cat1)
return result return result
@ -86,19 +199,21 @@ def get_category_tree(categories):
Company = Model.get('company.company') Company = Model.get('company.company')
company1, = Company.find([('rec_name', '=', company_name)]) 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 categories = qif_read_categories(qif_type_data['Cat'])
qif = Qif.parse(file_category_income, day_first=True) to_create = get_category_create(categories['in'], 'in')
cat_tree = [] to_create.extend(get_category_create(categories['out'], 'out'))
for catname in qif.categories.keys(): if len(to_create) > 0:
category = qif.categories[catname] try :
#cat_tree.extend(get_category_tree([category])) catlst = Category.create(to_create, context={'company': company1.id})
add_category(category, None, company1, 'in') print('-- created %d categories' % len(catlst))
except:
print('-- categories alredy exist')
# expense-category if 'Bank' in qif_type_data.keys():
qif = Qif.parse(file_category_expense, day_first=True) bookings = qif_read_bank(qif_type_data['Bank'])
cat_tree = []
for catname in qif.categories.keys(): print('-- bookings:', bookings)
category = qif.categories[catname]
#cat_tree.extend(get_category_tree([category]))
add_category(category, None, company1, 'out')

View file

@ -326,10 +326,6 @@ msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_out"
msgid "Expense" msgid "Expense"
msgstr "Ausgaben" 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" msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_in"
msgid "Revenue" msgid "Revenue"
msgstr "Einnahmen" msgstr "Einnahmen"
@ -338,10 +334,6 @@ msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out"
msgid "Expense" msgid "Expense"
msgstr "Ausgaben" msgstr "Ausgaben"
msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_all"
msgid "All"
msgstr "Alle"
################### ###################
# ir.model.button # # ir.model.button #

View file

@ -294,6 +294,22 @@ msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all"
msgid "All" msgid "All"
msgstr "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" msgctxt "model:ir.model.button,string:line_wfedit_button"
msgid "Edit" msgid "Edit"
msgstr "Edit" msgstr "Edit"

View file

@ -90,6 +90,7 @@ class CategoryTestCase(ModuleTestCase):
cat1, = Category.create([{ cat1, = Category.create([{
'name': 'Test 1', 'name': 'Test 1',
'description': 'Info', 'description': 'Info',
'cattype': 'in',
}]) }])
self.assertEqual(cat1.name, 'Test 1') self.assertEqual(cat1.name, 'Test 1')
self.assertEqual(cat1.rec_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.company.rec_name, 'm-ds')
self.assertEqual(cat1.parent, None) self.assertEqual(cat1.parent, None)
# duplicate, allowed # duplicate of different type, allowed
cat2, = Category.create([{ cat2, = Category.create([{
'name': 'Test 1', 'name': 'Test 1',
'description': 'Info', 'description': 'Info',
'cattype': 'out',
}]) }])
self.assertEqual(cat2.name, 'Test 1') self.assertEqual(cat2.name, 'Test 1')
self.assertEqual(cat2.rec_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.company.rec_name, 'm-ds')
self.assertEqual(cat2.parent, None) 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() @with_transaction()
def test_category_create_nodupl_diff_level(self): def test_category_create_nodupl_diff_level(self):
""" create category """ create category

View file

@ -4,6 +4,5 @@ The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. --> full copyright notices and license terms. -->
<tree sequence="sequence"> <tree sequence="sequence">
<field name="rec_name"/> <field name="rec_name"/>
<field name="cattype"/>
<field name="sequence" tree_invisible="1"/> <field name="sequence" tree_invisible="1"/>
</tree> </tree>

View file

@ -4,7 +4,6 @@ The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. --> full copyright notices and license terms. -->
<tree sequence="sequence"> <tree sequence="sequence">
<field name="name"/> <field name="name"/>
<field name="cattype"/>
<field name="sequence" tree_invisible="1"/> <field name="sequence" tree_invisible="1"/>
<field name="parent" tree_invisible="1"/> <field name="parent" tree_invisible="1"/>
<field name="childs" tree_invisible="1"/> <field name="childs" tree_invisible="1"/>