kategorie: hierarchische sortierung, sequence-spalte entfernt
This commit is contained in:
parent
7fd42c0b42
commit
64e9bab592
6 changed files with 88 additions and 245 deletions
85
category.py
85
category.py
|
@ -3,14 +3,46 @@
|
||||||
# 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, Exclude, tree, sequence_ordered
|
from trytond.model import ModelView, ModelSQL, fields, Unique, Exclude, tree
|
||||||
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
|
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 = [
|
sel_categorytype = [
|
||||||
|
@ -19,7 +51,7 @@ sel_categorytype = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
|
class Category(tree(separator='/'), ModelSQL, ModelView):
|
||||||
'Category'
|
'Category'
|
||||||
__name__ = 'cashbook.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',
|
company = fields.Many2One(string='Company', model_name='company.company',
|
||||||
required=True, ondelete="RESTRICT")
|
required=True, ondelete="RESTRICT")
|
||||||
sequence = fields.Integer(string='Sequence', select=True)
|
|
||||||
parent = fields.Many2One(string="Parent",
|
parent = fields.Many2One(string="Parent",
|
||||||
model_name='cashbook.category', ondelete='RESTRICT',
|
model_name='cashbook.category', ondelete='RESTRICT',
|
||||||
left='left', right='right')
|
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)
|
left = fields.Integer(string='Left', required=True, select=True)
|
||||||
right = fields.Integer(string='Right', 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
|
@classmethod
|
||||||
def __setup__(cls):
|
def __setup__(cls):
|
||||||
super(Category, cls).__setup__()
|
super(Category, cls).__setup__()
|
||||||
cls._order.insert(0, ('name', 'ASC'))
|
cls._order.insert(0, ('rec_name', 'ASC'))
|
||||||
t = cls.__table__()
|
t = cls.__table__()
|
||||||
cls._sql_constraints.extend([
|
cls._sql_constraints.extend([
|
||||||
('name_uniq',
|
('name_uniq',
|
||||||
|
@ -64,6 +100,13 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
|
||||||
'cashbook.msg_category_name_unique'),
|
'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
|
@classmethod
|
||||||
def default_cattype(cls):
|
def default_cattype(cls):
|
||||||
return 'out'
|
return 'out'
|
||||||
|
@ -83,28 +126,30 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def order_rec_name(tables):
|
def order_rec_name(tables):
|
||||||
""" order by pos
|
""" order by pos
|
||||||
|
a recursive sorting
|
||||||
"""
|
"""
|
||||||
Category2 = Pool().get('cashbook.category')
|
Category2 = Pool().get('cashbook.category')
|
||||||
tab_cat = Category2.__table__()
|
tab_cat = Category2.__table__()
|
||||||
|
tab_cat2 = Category2.__table__()
|
||||||
table, _ = tables[None]
|
table, _ = tables[None]
|
||||||
|
|
||||||
categories = With('id', 'name', 'name_path', recursive=True)
|
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 (
|
query = categories.select(
|
||||||
# ~ select "a"."id", 0, "a"."name", array["a"."name"]
|
ArrayToString(categories.name_path, '/').as_('rec_name'),
|
||||||
# ~ from cashbook_category as "a"
|
where = table.id==categories.id,
|
||||||
# ~ where "a"."parent" is null
|
with_ = [categories])
|
||||||
|
return [query]
|
||||||
# ~ 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')
|
@fields.depends('parent', '_parent_parent.cattype')
|
||||||
def on_change_with_parent_cattype(self, name=None):
|
def on_change_with_parent_cattype(self, name=None):
|
||||||
|
|
|
@ -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)
|
|
|
@ -74,6 +74,26 @@ class CategoryTestCase(ModuleTestCase):
|
||||||
('rec_name', '=', 'Level 1b/Level 1b.2b'),
|
('rec_name', '=', 'Level 1b/Level 1b.2b'),
|
||||||
]), 1)
|
]), 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()
|
@with_transaction()
|
||||||
def test_category_create_check_category_type(self):
|
def test_category_create_check_category_type(self):
|
||||||
""" create category, update type of category
|
""" create category, update type of category
|
||||||
|
|
|
@ -18,8 +18,7 @@ full copyright notices and license terms. -->
|
||||||
<field name="company"/>
|
<field name="company"/>
|
||||||
<label name="parent"/>
|
<label name="parent"/>
|
||||||
<field name="parent"/>
|
<field name="parent"/>
|
||||||
<label name="sequence"/>
|
<newline/>
|
||||||
<field name="sequence"/>
|
|
||||||
<field name="childs" colspan="6"/>
|
<field name="childs" colspan="6"/>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<!-- This file is part of the cashbook-module from m-ds for Tryton.
|
<!-- 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
|
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>
|
||||||
<field name="rec_name"/>
|
<field name="rec_name"/>
|
||||||
<field name="sequence" tree_invisible="1"/>
|
|
||||||
</tree>
|
</tree>
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
<!-- This file is part of the cashbook-module from m-ds for Tryton.
|
<!-- 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
|
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 >
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<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"/>
|
||||||
</tree>
|
</tree>
|
||||||
|
|
Loading…
Reference in a new issue