Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a84e3ed8ba | ||
![]() |
4876b06421 | ||
![]() |
619a4e9ed6 | ||
![]() |
559a5d0656 | ||
![]() |
2fdee39611 | ||
![]() |
4df6284257 | ||
![]() |
0aa9df2f1d | ||
![]() |
dadcfb618f | ||
![]() |
603a9d7477 |
11 changed files with 262 additions and 18 deletions
12
README.rst
12
README.rst
|
@ -14,6 +14,18 @@ Requires
|
|||
Changes
|
||||
=======
|
||||
|
||||
*6.0.3 - 31.08.2022*
|
||||
|
||||
- updt: checks, sorting
|
||||
|
||||
*6.0.2 - 25.08.2022*
|
||||
|
||||
- add: split-booking
|
||||
|
||||
*6.0.1 - 23.08.2022*
|
||||
|
||||
- works
|
||||
|
||||
*6.0.0 - 05.08.2022*
|
||||
|
||||
- init
|
||||
|
|
19
book.py
19
book.py
|
@ -76,14 +76,16 @@ class Book(Workflow, ModelSQL, ModelView):
|
|||
),
|
||||
}, depends=DEPENDS+['lines'])
|
||||
start_balance = fields.Numeric(string='Initial Amount', required=True,
|
||||
digits=(16, Eval('currency_digits', 2)),
|
||||
states={
|
||||
'readonly': Or(
|
||||
STATES['readonly'],
|
||||
Bool(Eval('lines')),
|
||||
),
|
||||
}, depends=DEPENDS+['lines'])
|
||||
balance = fields.Function(fields.Numeric(string='Balance', readonly=True),
|
||||
'on_change_with_balance')
|
||||
}, depends=DEPENDS+['lines', 'currency_digits'])
|
||||
balance = fields.Function(fields.Numeric(string='Balance', readonly=True,
|
||||
digits=(16, Eval('currency_digits', 2)),
|
||||
depends=['currency_digits']), 'on_change_with_balance')
|
||||
currency = fields.Many2One(string='Currency', required=True,
|
||||
model_name='currency.currency',
|
||||
states={
|
||||
|
@ -92,6 +94,8 @@ class Book(Workflow, ModelSQL, ModelView):
|
|||
Bool(Eval('lines', [])),
|
||||
),
|
||||
}, depends=DEPENDS+['lines'])
|
||||
currency_digits = fields.Function(fields.Integer(string='Currency Digits',
|
||||
readonly=True), 'on_change_with_currency_digits')
|
||||
state = fields.Selection(string='State', required=True,
|
||||
readonly=True, selection=sel_state_book)
|
||||
state_string = state.translated('state')
|
||||
|
@ -196,6 +200,15 @@ class Book(Workflow, ModelSQL, ModelView):
|
|||
'state': self.state_string,
|
||||
}
|
||||
|
||||
@fields.depends('currency')
|
||||
def on_change_with_currency_digits(self, name=None):
|
||||
""" currency of cashbook
|
||||
"""
|
||||
if self.currency:
|
||||
return self.currency.digits
|
||||
else:
|
||||
return 2
|
||||
|
||||
@fields.depends('id', 'start_balance')
|
||||
def on_change_with_balance(self, name=None):
|
||||
""" compute balance
|
||||
|
|
91
category.py
91
category.py
|
@ -3,12 +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, 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.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 = [
|
||||
|
@ -17,7 +51,7 @@ sel_categorytype = [
|
|||
]
|
||||
|
||||
|
||||
class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
|
||||
class Category(tree(separator='/'), ModelSQL, ModelView):
|
||||
'Category'
|
||||
__name__ = 'cashbook.category'
|
||||
|
||||
|
@ -36,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')
|
||||
|
@ -45,15 +78,35 @@ 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', 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
|
||||
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'
|
||||
|
@ -70,6 +123,34 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
|
|||
def default_right():
|
||||
return 0
|
||||
|
||||
@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 = 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
|
||||
|
||||
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):
|
||||
""" get type of parent category or None
|
||||
|
|
29
category.xml
29
category.xml
|
@ -42,6 +42,20 @@ full copyright notices and license terms. -->
|
|||
<field name="act_window" ref="act_category_list"/>
|
||||
</record>
|
||||
|
||||
<!-- domain view - list -->
|
||||
<record model="ir.action.act_window.domain" id="act_category_list_domain_in">
|
||||
<field name="name">Revenue</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="domain" eval="[('cattype', '=', 'in')]" pyson="1"/>
|
||||
<field name="act_window" ref="act_category_list"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_category_list_domain_out">
|
||||
<field name="name">Expense</field>
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="domain" eval="[('cattype', '=', 'out')]" pyson="1"/>
|
||||
<field name="act_window" ref="act_category_list"/>
|
||||
</record>
|
||||
|
||||
<!-- action view - tree -->
|
||||
<record model="ir.action.act_window" id="act_category_tree">
|
||||
<field name="name">Category</field>
|
||||
|
@ -59,6 +73,21 @@ full copyright notices and license terms. -->
|
|||
<field name="act_window" ref="act_category_tree"/>
|
||||
</record>
|
||||
|
||||
<!-- domain view - tree -->
|
||||
<record model="ir.action.act_window.domain" id="act_category_tree_domain_in">
|
||||
<field name="name">Revenue</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="domain" eval="[('cattype', '=', 'in')]" pyson="1"/>
|
||||
<field name="act_window" ref="act_category_tree"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_category_tree_domain_out">
|
||||
<field name="name">Expense</field>
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="domain" eval="[('cattype', '=', 'out')]" pyson="1"/>
|
||||
<field name="act_window" ref="act_category_tree"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- permission -->
|
||||
<!-- anon: deny all -->
|
||||
<record model="ir.model.access" id="access_category-anon">
|
||||
|
|
20
locale/de.po
20
locale/de.po
|
@ -318,6 +318,22 @@ 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_list_domain_in"
|
||||
msgid "Revenue"
|
||||
msgstr "Einnahmen"
|
||||
|
||||
msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out"
|
||||
msgid "Expense"
|
||||
msgstr "Ausgaben"
|
||||
|
||||
|
||||
###################
|
||||
# ir.model.button #
|
||||
|
@ -446,6 +462,10 @@ msgctxt "field:cashbook.book,currency:"
|
|||
msgid "Currency"
|
||||
msgstr "Währung"
|
||||
|
||||
msgctxt "field:cashbook.book,currency_digits:"
|
||||
msgid "Currency Digits"
|
||||
msgstr "Nachkommastellen Währung"
|
||||
|
||||
msgctxt "field:cashbook.book,start_balance:"
|
||||
msgid "Initial Amount"
|
||||
msgstr "Anfangsbetrag"
|
||||
|
|
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"
|
||||
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"
|
||||
|
|
|
@ -28,6 +28,72 @@ 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)
|
||||
|
||||
# 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
|
||||
|
@ -90,6 +156,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 +164,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 +176,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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[tryton]
|
||||
version=6.0.0
|
||||
version=6.0.3
|
||||
depends:
|
||||
res
|
||||
currency
|
||||
|
|
|
@ -18,8 +18,7 @@ full copyright notices and license terms. -->
|
|||
<field name="company"/>
|
||||
<label name="parent"/>
|
||||
<field name="parent"/>
|
||||
<label name="sequence"/>
|
||||
<field name="sequence"/>
|
||||
<newline/>
|
||||
<field name="childs" colspan="6"/>
|
||||
</page>
|
||||
</notebook>
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
<!-- 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. -->
|
||||
<tree sequence="sequence">
|
||||
<tree>
|
||||
<field name="rec_name"/>
|
||||
<field name="cattype"/>
|
||||
<field name="sequence" tree_invisible="1"/>
|
||||
</tree>
|
||||
|
|
|
@ -2,10 +2,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. -->
|
||||
<tree sequence="sequence">
|
||||
<tree >
|
||||
<field name="name"/>
|
||||
<field name="cattype"/>
|
||||
<field name="sequence" tree_invisible="1"/>
|
||||
<field name="parent" tree_invisible="1"/>
|
||||
<field name="childs" tree_invisible="1"/>
|
||||
</tree>
|
||||
|
|
Loading…
Reference in a new issue