book: start-saldo + sperre bei lines>0, saldo, rec_name + tests

This commit is contained in:
Frederik Jaeckel 2022-08-11 13:01:53 +02:00
parent 8fd6e0d339
commit ae5303658e
9 changed files with 218 additions and 40 deletions

75
book.py
View file

@ -9,6 +9,10 @@ from trytond.exceptions import UserError
from trytond.i18n import gettext from trytond.i18n import gettext
from trytond.transaction import Transaction from trytond.transaction import Transaction
from trytond.pool import Pool from trytond.pool import Pool
from trytond.report import Report
from decimal import Decimal
from sql.aggregate import Sum
from sql.conditionals import Case
STATES = { STATES = {
@ -48,6 +52,15 @@ class Book(Workflow, ModelSQL, ModelView):
account = fields.Many2One(string='Account', select=True, account = fields.Many2One(string='Account', select=True,
model_name='account.account', ondelete='RESTRICT', model_name='account.account', ondelete='RESTRICT',
states=STATES, depends=DEPENDS) states=STATES, depends=DEPENDS)
start_balance = fields.Numeric(string='Initial Amount', required=True,
states={
'readonly': Or(
STATES['readonly'],
Bool(Eval('lines')),
),
}, depends=DEPENDS+['lines'])
balance = fields.Function(fields.Numeric(string='Balance', readonly=True),
'on_change_with_balance')
currency = fields.Many2One(string='Currency', required=True, currency = fields.Many2One(string='Currency', required=True,
model_name='currency.currency', model_name='currency.currency',
states={ states={
@ -64,6 +77,7 @@ class Book(Workflow, ModelSQL, ModelView):
def __setup__(cls): def __setup__(cls):
super(Book, cls).__setup__() super(Book, cls).__setup__()
cls._order.insert(0, ('name', 'ASC')) cls._order.insert(0, ('name', 'ASC'))
cls._order.insert(0, ('state', 'ASC'))
t = cls.__table__() t = cls.__table__()
cls._sql_constraints.extend([ cls._sql_constraints.extend([
('state_val', ('state_val',
@ -90,6 +104,12 @@ class Book(Workflow, ModelSQL, ModelView):
}, },
}) })
@classmethod
def default_start_balance(cls):
""" zero
"""
return Decimal('0.0')
@classmethod @classmethod
def default_currency(cls): def default_currency(cls):
""" currency of company """ currency of company
@ -116,6 +136,55 @@ class Book(Workflow, ModelSQL, ModelView):
""" """
return Transaction().user return Transaction().user
@staticmethod
def order_state(tables):
""" edit = 0, check/done = 1
"""
Book2 = Pool().get('cashbook.book')
tab_book = Book2.__table__()
table, _ = tables[None]
query = tab_book.select(
Case(
(tab_book.state == 'open', 0),
else_ = 1),
where=tab_book.id==table.id
)
return [query]
def get_rec_name(self, name):
""" name, balance, state
"""
return '%(name)s | %(balance)s %(symbol)s | %(state)s' % {
'name': self.name or '-',
'balance': Report.format_number(self.balance or 0.0, None),
'symbol': getattr(self.currency, 'symbol', '-'),
'state': self.state_string,
}
@fields.depends('id', 'start_balance')
def on_change_with_balance(self, name=None):
""" compute balance
"""
Line = Pool().get('cashbook.line')
tab_line = Line.__table__()
cursor = Transaction().connection.cursor()
query = tab_line.select(
Sum(tab_line.credit - tab_line.debit).as_('balance'),
group_by=[tab_line.cashbook],
where=tab_line.cashbook == self.id
)
if self.id:
if self.start_balance is not None:
balance = self.start_balance
cursor.execute(*query)
result = cursor.fetchone()
if result:
balance += result[0]
return balance
@classmethod @classmethod
@ModelView.button @ModelView.button
@Workflow.transition('open') @Workflow.transition('open')
@ -147,6 +216,12 @@ class Book(Workflow, ModelSQL, ModelView):
actions = iter(args) actions = iter(args)
for books, values in zip(actions, actions): for books, values in zip(actions, actions):
for book in books: for book in books:
if 'start_balance' in values.keys():
if len(book.lines) > 0:
raise UserError(gettext(
'cashbook.msg_book_err_startamount_with_lines',
bookname = book.rec_name,
))
if book.state != 'open': if book.state != 'open':
# allow state-update, if its the only action # allow state-update, if its the only action
if not (('state' in values.keys()) and (len(values.keys()) == 1)): if not (('state' in values.keys()) and (len(values.keys()) == 1)):

73
line.py
View file

@ -77,6 +77,13 @@ class Line(Workflow, ModelSQL, ModelView):
required=True, readonly=True, depends=['currency_digits']) required=True, readonly=True, depends=['currency_digits'])
credit = fields.Numeric(string='Credit', digits=(16, Eval('currency_digits', 2)), credit = fields.Numeric(string='Credit', digits=(16, Eval('currency_digits', 2)),
required=True, readonly=True, depends=['currency_digits']) required=True, readonly=True, depends=['currency_digits'])
balance = fields.Function(fields.Numeric(string='Balance',
digits=(16, Eval('currency_digits', 2)),
help='Balance of the cash book up to the current line, if the default sorting applies.',
readonly=True, depends=['currency_digits']),
'on_change_with_balance')
currency = fields.Function(fields.Many2One(model_name='currency.currency', currency = fields.Function(fields.Many2One(model_name='currency.currency',
string="Currency"), 'on_change_with_currency') string="Currency"), 'on_change_with_currency')
currency_digits = fields.Function(fields.Integer(string='Currency Digits'), currency_digits = fields.Function(fields.Integer(string='Currency Digits'),
@ -94,8 +101,8 @@ class Line(Workflow, ModelSQL, ModelView):
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
super(Line, cls).__setup__() super(Line, cls).__setup__()
cls._order.insert(0, ('state', 'ASC'))
cls._order.insert(0, ('date', 'ASC')) cls._order.insert(0, ('date', 'ASC'))
cls._order.insert(0, ('state', 'ASC'))
t = cls.__table__() t = cls.__table__()
cls._sql_constraints.extend([ cls._sql_constraints.extend([
('state_val', ('state_val',
@ -146,20 +153,6 @@ class Line(Workflow, ModelSQL, ModelView):
""" """
pass pass
@fields.depends('bookingtype', 'category')
def on_change_bookingtype(self):
""" clear category if not valid type
"""
types = {
'in': ['in', 'mvin'],
'out': ['out', 'mvout'],
}
if self.bookingtype:
if self.category:
if not self.bookingtype in types.get(self.category.cattype, ''):
self.category = None
@classmethod @classmethod
def default_state(cls): def default_state(cls):
""" default: edit """ default: edit
@ -180,6 +173,12 @@ class Line(Workflow, ModelSQL, ModelView):
context = Transaction().context context = Transaction().context
return context.get('cashbook', None) return context.get('cashbook', None)
@classmethod
def search_rec_name(cls, name, clause):
""" search in description +...
"""
return [('description',) + tuple(clause[1:])]
def get_rec_name(self, name): def get_rec_name(self, name):
""" short + name """ short + name
""" """
@ -219,12 +218,6 @@ class Line(Workflow, ModelSQL, ModelView):
return [tab2] return [tab2]
@classmethod
def search_category_view(cls, name, clause):
""" search in category
"""
return [('category.rec_name',) + tuple(clause[1:])]
@fields.depends('category') @fields.depends('category')
def on_change_with_category_view(self, name=None): def on_change_with_category_view(self, name=None):
""" show optimizef form of category for list-view """ show optimizef form of category for list-view
@ -239,10 +232,10 @@ class Line(Workflow, ModelSQL, ModelView):
return self.category.get_long_recname(self.category.name) return self.category.get_long_recname(self.category.name)
@classmethod @classmethod
def search_rec_name(cls, name, clause): def search_category_view(cls, name, clause):
""" search in description +... """ search in category
""" """
return [('description',) + tuple(clause[1:])] return [('category.rec_name',) + tuple(clause[1:])]
@fields.depends('date') @fields.depends('date')
def on_change_with_month(self, name=None): def on_change_with_month(self, name=None):
@ -286,6 +279,20 @@ class Line(Workflow, ModelSQL, ModelView):
""" """
return [('cashbook.state',) + tuple(clause[1:])] return [('cashbook.state',) + tuple(clause[1:])]
@fields.depends('bookingtype', 'category')
def on_change_bookingtype(self):
""" clear category if not valid type
"""
types = {
'in': ['in', 'mvin'],
'out': ['out', 'mvout'],
}
if self.bookingtype:
if self.category:
if not self.bookingtype in types.get(self.category.cattype, ''):
self.category = None
@fields.depends('cashbook', '_parent_cashbook.currency') @fields.depends('cashbook', '_parent_cashbook.currency')
def on_change_with_currency(self, name=None): def on_change_with_currency(self, name=None):
""" currency of cashbook """ currency of cashbook
@ -302,6 +309,23 @@ class Line(Workflow, ModelSQL, ModelView):
else: else:
return 2 return 2
@fields.depends('id', 'cashbook', '_parent_cashbook.start_balance', '_parent_cashbook.id')
def on_change_with_balance(self, name=None):
""" compute balance until current line, with current sort order
"""
Line = Pool().get('cashbook.line')
if self.cashbook:
balance = self.cashbook.start_balance
lines = Line.search([
('cashbook.id', '=', self.cashbook.id),
])
for line in lines:
balance += line.credit - line.debit
if line.id == self.id:
break
return balance
@classmethod @classmethod
def get_debit_credit(cls, values): def get_debit_credit(cls, values):
""" compute debit/credit from amount """ compute debit/credit from amount
@ -470,6 +494,7 @@ class LineContext(ModelView):
""" get number of accessible cashbooks, """ get number of accessible cashbooks,
depends on user-permissions depends on user-permissions
""" """
print('-- on_change_with_num_cashbook:', Transaction().context)
LineContext = Pool().get('cashbook.line.context') LineContext = Pool().get('cashbook.line.context')
return LineContext.default_num_cashbook() return LineContext.default_num_cashbook()

View file

@ -54,6 +54,10 @@ msgctxt "model:ir.message,text:msg_category_type_not_like_parent"
msgid "The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'." msgid "The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'."
msgstr "Der Typ der aktuellen Kategorie '%(catname)s' muß gleich dem Typ der übergeordneten Kategorie '%(parentname)s' sein." msgstr "Der Typ der aktuellen Kategorie '%(catname)s' muß gleich dem Typ der übergeordneten Kategorie '%(parentname)s' sein."
msgctxt "model:ir.message,text:msg_book_err_startamount_with_lines"
msgid "The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings."
msgstr "Der Anfangsbetrag des Kassenbuchs '%(bookname)s' kann nicht geändert werden, da es bereits Buchungen enthält."
############# #############
# res.group # # res.group #
@ -274,6 +278,10 @@ msgctxt "field:cashbook.book,currency:"
msgid "Currency" msgid "Currency"
msgstr "Währung" msgstr "Währung"
msgctxt "field:cashbook.book,start_balance:"
msgid "Initial Amount"
msgstr "Anfangsbetrag"
################# #################
# cashbook.line # # cashbook.line #
@ -390,6 +398,14 @@ msgctxt "field:cashbook.line,currency_digits:"
msgid "Currency Digits" msgid "Currency Digits"
msgstr "Nachkommastellen Währung" msgstr "Nachkommastellen Währung"
msgctxt "field:cashbook.line,balance:"
msgid "Balance"
msgstr "Saldo"
msgctxt "help:cashbook.line,balance:"
msgid "Balance of the cash book up to the current line, if the default sorting applies."
msgstr "Saldo des Kassenbuchs bis zur aktuellen Zeile, sofern die Standardsortierung gilt."
################# #################
# cashbook.type # # cashbook.type #

View file

@ -41,6 +41,9 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_category_type_not_like_parent"> <record model="ir.message" id="msg_category_type_not_like_parent">
<field name="text">The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'.</field> <field name="text">The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'.</field>
</record> </record>
<record model="ir.message" id="msg_book_err_startamount_with_lines">
<field name="text">The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings.</field>
</record>
</data> </data>
</tryton> </tryton>

View file

@ -62,7 +62,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.state, 'open') self.assertEqual(book.state, 'open')
self.assertRaisesRegex(UserError, self.assertRaisesRegex(UserError,
"The cashbook 'Book 1' cannot be deleted because it contains 1 lines and is not in the status 'Archive'.", "The cashbook 'Book 1 | 1.00 usd | Open' cannot be deleted because it contains 1 lines and is not in the status 'Archive'.",
Book.delete, Book.delete,
[book]) [book])
@ -95,7 +95,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.state, 'closed') self.assertEqual(book.state, 'closed')
self.assertRaisesRegex(UserError, self.assertRaisesRegex(UserError,
"The cashbook 'Book 1' cannot be deleted because it contains 1 lines and is not in the status 'Archive'.", "The cashbook 'Book 1 | 1.00 usd | Closed' cannot be deleted because it contains 1 lines and is not in the status 'Archive'.",
Book.delete, Book.delete,
[book]) [book])
@ -160,7 +160,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.state, 'closed') self.assertEqual(book.state, 'closed')
self.assertRaisesRegex(UserError, self.assertRaisesRegex(UserError,
"The cash book 'Book 1a' is 'Closed' and cannot be changed.", "The cash book 'Book 1a | 1.00 usd | Closed' is 'Closed' and cannot be changed.",
Book.write, Book.write,
*[ *[
[book], [book],
@ -183,7 +183,7 @@ class BookTestCase(ModuleTestCase):
Book.wfarchive([book]) Book.wfarchive([book])
self.assertRaisesRegex(UserError, self.assertRaisesRegex(UserError,
"The cash book 'Book 1c' is 'Archive' and cannot be changed.", "The cash book 'Book 1c | 0.00 usd | Archive' is 'Archive' and cannot be changed.",
Book.write, Book.write,
*[ *[
[book], [book],
@ -192,6 +192,59 @@ class BookTestCase(ModuleTestCase):
}, },
]) ])
@with_transaction()
def test_book_deny_update_start_amount(self):
""" create cashbook, add lines, update start-amount
"""
pool = Pool()
Book = pool.get('cashbook.book')
types = self.prep_type()
company = self.prep_company()
category = self.prep_category(cattype='in')
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.start_balance, Decimal('0.0'))
self.assertEqual(book.rec_name, 'Book 1 | 0.00 usd | Open')
Book.write(*[
[book],
{
'start_balance': Decimal('1.0'),
}])
self.assertEqual(book.start_balance, Decimal('1.0'))
self.assertEqual(book.balance, Decimal('1.0'))
Book.write(*[
[book],
{
'lines': [('create', [{
'amount': Decimal('2.0'),
'description': 'Test',
'category': category.id,
'bookingtype': 'in',
}])],
}])
self.assertEqual(book.start_balance, Decimal('1.0'))
self.assertEqual(book.balance, Decimal('3.0'))
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.lines[0].balance, Decimal('3.0'))
self.assertRaisesRegex(UserError,
"The initial amount of the cash book 'Fridas book | 3.00 usd | Open' cannot be changed because it already contains bookings.",
Book.write,
*[
[book],
{
'start_balance': Decimal('1.5'),
},
])
@with_transaction() @with_transaction()
def test_book_permission_owner(self): def test_book_permission_owner(self):
""" create book + 2x users, add users to group, check access """ create book + 2x users, add users to group, check access
@ -228,7 +281,7 @@ class BookTestCase(ModuleTestCase):
'company': company.id, 'company': company.id,
'currency': company.currency.id, 'currency': company.currency.id,
}]) }])
self.assertEqual(book.rec_name, 'Fridas book'), self.assertEqual(book.rec_name, 'Fridas book | 0.00 usd | Open'),
self.assertEqual(book.owner.rec_name, 'Frida'), self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({ with Transaction().set_context({
@ -243,7 +296,7 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id): with Transaction().set_user(usr_lst[0].id):
books = Book.search([]) books = Book.search([])
self.assertEqual(len(books), 1) self.assertEqual(len(books), 1)
self.assertEqual(books[0].rec_name, 'Fridas book') self.assertEqual(books[0].rec_name, 'Fridas book | 0.00 usd | Open')
self.assertRaisesRegex(UserError, self.assertRaisesRegex(UserError,
'You are not allowed to access "Cashbook".', 'You are not allowed to access "Cashbook".',
@ -298,7 +351,7 @@ class BookTestCase(ModuleTestCase):
'currency': company.currency.id, 'currency': company.currency.id,
'btype': types.id, 'btype': types.id,
}]) }])
self.assertEqual(book.rec_name, 'Fridas book'), self.assertEqual(book.rec_name, 'Fridas book | 0.00 usd | Open'),
self.assertEqual(book.owner.rec_name, 'Frida'), self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({ with Transaction().set_context({
@ -315,7 +368,7 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id): with Transaction().set_user(usr_lst[0].id):
books = Book.search([]) books = Book.search([])
self.assertEqual(len(books), 1) self.assertEqual(len(books), 1)
self.assertEqual(books[0].rec_name, 'Fridas book') self.assertEqual(books[0].rec_name, 'Fridas book | 0.00 usd | Open')
@with_transaction() @with_transaction()
def test_book_permission_observer(self): def test_book_permission_observer(self):
@ -360,7 +413,7 @@ class BookTestCase(ModuleTestCase):
'currency': company.currency.id, 'currency': company.currency.id,
'btype': types.id, 'btype': types.id,
}]) }])
self.assertEqual(book.rec_name, 'Fridas book'), self.assertEqual(book.rec_name, 'Fridas book | 0.00 usd | Open'),
self.assertEqual(book.owner.rec_name, 'Frida'), self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({ with Transaction().set_context({
@ -377,6 +430,6 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id): with Transaction().set_user(usr_lst[0].id):
books = Book.search([]) books = Book.search([])
self.assertEqual(len(books), 1) self.assertEqual(len(books), 1)
self.assertEqual(books[0].rec_name, 'Fridas book') self.assertEqual(books[0].rec_name, 'Fridas book | 0.00 usd | Open')
# end BookTestCase # end BookTestCase

View file

@ -131,7 +131,7 @@ class LineTestCase(ModuleTestCase):
self.assertEqual(book.state, 'closed') self.assertEqual(book.state, 'closed')
self.assertRaisesRegex(UserError, self.assertRaisesRegex(UserError,
"The cash book 'Book 1' is 'Closed' and cannot be changed.", "The cash book 'Book | 2.00 usd | Closed' is 'Closed' and cannot be changed.",
Line.write, Line.write,
*[ *[
[book.lines[0]], [book.lines[0]],
@ -478,7 +478,7 @@ class LineTestCase(ModuleTestCase):
self.assertEqual(book.state, 'closed') self.assertEqual(book.state, 'closed')
self.assertRaisesRegex(UserError, self.assertRaisesRegex(UserError,
"The cashbook line '05/01/2022 Text 1' cannot be deleted because the Cashbook 'Book 1' is in state 'Closed'.", "The cashbook line '05/01/2022 Text 1' cannot be deleted because the Cashbook 'Book | 2.00 usd | Closed' is in state 'Closed'.",
Lines.delete, Lines.delete,
[book.lines[0]]) [book.lines[0]])
@ -570,7 +570,7 @@ class LineTestCase(ModuleTestCase):
'amount': Decimal('1.0'), 'amount': Decimal('1.0'),
}])], }])],
}]) }])
self.assertEqual(book.rec_name, 'Fridas book'), self.assertEqual(book.rec_name, 'Fridas book | 1.00 usd | Open'),
self.assertEqual(book.owner.rec_name, 'Frida'), self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({ with Transaction().set_context({
@ -585,7 +585,7 @@ class LineTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id): with Transaction().set_user(usr_lst[0].id):
lines = Line.search([]) lines = Line.search([])
self.assertEqual(len(lines), 1) self.assertEqual(len(lines), 1)
self.assertEqual(lines[0].cashbook.rec_name, 'Fridas book') self.assertEqual(lines[0].cashbook.rec_name, 'Fridas book | 1.00 usd | Open')
self.assertEqual(lines[0].rec_name, '05/01/2022 Test 1') self.assertEqual(lines[0].rec_name, '05/01/2022 Test 1')
Line.write(*[ Line.write(*[
@ -647,7 +647,7 @@ class LineTestCase(ModuleTestCase):
'amount': Decimal('1.0'), 'amount': Decimal('1.0'),
}])], }])],
}]) }])
self.assertEqual(book.rec_name, 'Fridas book'), self.assertEqual(book.rec_name, 'Fridas book | 1.00 usd | Open'),
self.assertEqual(book.owner.rec_name, 'Frida'), self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({ with Transaction().set_context({
@ -731,7 +731,7 @@ class LineTestCase(ModuleTestCase):
'amount': Decimal('1.0'), 'amount': Decimal('1.0'),
}])], }])],
}]) }])
self.assertEqual(book.rec_name, 'Fridas book'), self.assertEqual(book.rec_name, 'Fridas book | 1.00 usd | Open'),
self.assertEqual(book.owner.rec_name, 'Frida'), self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({ with Transaction().set_context({

View file

@ -13,6 +13,10 @@ full copyright notices and license terms. -->
<label name="account"/> <label name="account"/>
<field name="account"/> <field name="account"/>
<label name="start_balance"/>
<field name="start_balance"/>
<newline/>
<label name="state"/> <label name="state"/>
<field name="state"/> <field name="state"/>
<group id="grpstate" col="3" colspan="2"> <group id="grpstate" col="3" colspan="2">

View file

@ -5,6 +5,7 @@ full copyright notices and license terms. -->
<tree> <tree>
<field name="name"/> <field name="name"/>
<field name="btype"/> <field name="btype"/>
<field name="start_balance"/>
<field name="currency"/> <field name="currency"/>
<field name="account"/> <field name="account"/>
<field name="owner"/> <field name="owner"/>

View file

@ -9,6 +9,7 @@ full copyright notices and license terms. -->
<field name="description" expand="1"/> <field name="description" expand="1"/>
<field name="credit" sum="Credit"/> <field name="credit" sum="Credit"/>
<field name="debit" sum="Debit"/> <field name="debit" sum="Debit"/>
<field name="balance"/>
<field name="currency"/> <field name="currency"/>
<field name="state"/> <field name="state"/>
<button name="wfedit"/> <button name="wfedit"/>