kategorie: constraint gegen gleiche Namen auf toplevel,
importer: list/erstellt kategorie, list transaktionen
This commit is contained in:
parent
532d9cc7c8
commit
937124bcaf
8 changed files with 207 additions and 77 deletions
13
category.py
13
category.py
|
@ -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
|
||||||
|
|
12
category.xml
12
category.xml
|
@ -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 -->
|
||||||
|
|
|
@ -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')
|
|
||||||
|
|
|
@ -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 #
|
||||||
|
|
16
locale/en.po
16
locale/en.po
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
Loading…
Reference in a new issue