diff --git a/book.py b/book.py
index 3bdbfd6..a360f7c 100644
--- a/book.py
+++ b/book.py
@@ -9,6 +9,10 @@ from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.transaction import Transaction
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 = {
@@ -48,6 +52,15 @@ class Book(Workflow, ModelSQL, ModelView):
account = fields.Many2One(string='Account', select=True,
model_name='account.account', ondelete='RESTRICT',
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,
model_name='currency.currency',
states={
@@ -64,6 +77,7 @@ class Book(Workflow, ModelSQL, ModelView):
def __setup__(cls):
super(Book, cls).__setup__()
cls._order.insert(0, ('name', 'ASC'))
+ cls._order.insert(0, ('state', 'ASC'))
t = cls.__table__()
cls._sql_constraints.extend([
('state_val',
@@ -90,6 +104,12 @@ class Book(Workflow, ModelSQL, ModelView):
},
})
+ @classmethod
+ def default_start_balance(cls):
+ """ zero
+ """
+ return Decimal('0.0')
+
@classmethod
def default_currency(cls):
""" currency of company
@@ -116,6 +136,55 @@ class Book(Workflow, ModelSQL, ModelView):
"""
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
@ModelView.button
@Workflow.transition('open')
@@ -147,6 +216,12 @@ class Book(Workflow, ModelSQL, ModelView):
actions = iter(args)
for books, values in zip(actions, actions):
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':
# allow state-update, if its the only action
if not (('state' in values.keys()) and (len(values.keys()) == 1)):
diff --git a/line.py b/line.py
index ee559c7..0484ab7 100644
--- a/line.py
+++ b/line.py
@@ -77,6 +77,13 @@ class Line(Workflow, ModelSQL, ModelView):
required=True, readonly=True, depends=['currency_digits'])
credit = fields.Numeric(string='Credit', digits=(16, Eval('currency_digits', 2)),
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',
string="Currency"), 'on_change_with_currency')
currency_digits = fields.Function(fields.Integer(string='Currency Digits'),
@@ -94,8 +101,8 @@ class Line(Workflow, ModelSQL, ModelView):
@classmethod
def __setup__(cls):
super(Line, cls).__setup__()
- cls._order.insert(0, ('state', 'ASC'))
cls._order.insert(0, ('date', 'ASC'))
+ cls._order.insert(0, ('state', 'ASC'))
t = cls.__table__()
cls._sql_constraints.extend([
('state_val',
@@ -146,20 +153,6 @@ class Line(Workflow, ModelSQL, ModelView):
"""
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
def default_state(cls):
""" default: edit
@@ -180,6 +173,12 @@ class Line(Workflow, ModelSQL, ModelView):
context = Transaction().context
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):
""" short + name
"""
@@ -219,12 +218,6 @@ class Line(Workflow, ModelSQL, ModelView):
return [tab2]
- @classmethod
- def search_category_view(cls, name, clause):
- """ search in category
- """
- return [('category.rec_name',) + tuple(clause[1:])]
-
@fields.depends('category')
def on_change_with_category_view(self, name=None):
""" 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)
@classmethod
- def search_rec_name(cls, name, clause):
- """ search in description +...
+ def search_category_view(cls, name, clause):
+ """ search in category
"""
- return [('description',) + tuple(clause[1:])]
+ return [('category.rec_name',) + tuple(clause[1:])]
@fields.depends('date')
def on_change_with_month(self, name=None):
@@ -286,6 +279,20 @@ class Line(Workflow, ModelSQL, ModelView):
"""
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')
def on_change_with_currency(self, name=None):
""" currency of cashbook
@@ -302,6 +309,23 @@ class Line(Workflow, ModelSQL, ModelView):
else:
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
def get_debit_credit(cls, values):
""" compute debit/credit from amount
@@ -470,6 +494,7 @@ class LineContext(ModelView):
""" get number of accessible cashbooks,
depends on user-permissions
"""
+ print('-- on_change_with_num_cashbook:', Transaction().context)
LineContext = Pool().get('cashbook.line.context')
return LineContext.default_num_cashbook()
diff --git a/locale/de.po b/locale/de.po
index 1114c50..6ab03d3 100644
--- a/locale/de.po
+++ b/locale/de.po
@@ -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'."
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 #
@@ -274,6 +278,10 @@ msgctxt "field:cashbook.book,currency:"
msgid "Currency"
msgstr "Währung"
+msgctxt "field:cashbook.book,start_balance:"
+msgid "Initial Amount"
+msgstr "Anfangsbetrag"
+
#################
# cashbook.line #
@@ -390,6 +398,14 @@ msgctxt "field:cashbook.line,currency_digits:"
msgid "Currency Digits"
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 #
diff --git a/message.xml b/message.xml
index 9daa5b0..439317f 100644
--- a/message.xml
+++ b/message.xml
@@ -41,6 +41,9 @@ full copyright notices and license terms. -->
The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'.
+
+ The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings.
+
diff --git a/tests/test_book.py b/tests/test_book.py
index 29a90f3..ed25879 100644
--- a/tests/test_book.py
+++ b/tests/test_book.py
@@ -62,7 +62,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.state, 'open')
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])
@@ -95,7 +95,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.state, 'closed')
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])
@@ -160,7 +160,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.state, 'closed')
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],
@@ -183,7 +183,7 @@ class BookTestCase(ModuleTestCase):
Book.wfarchive([book])
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],
@@ -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()
def test_book_permission_owner(self):
""" create book + 2x users, add users to group, check access
@@ -228,7 +281,7 @@ class BookTestCase(ModuleTestCase):
'company': company.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'),
with Transaction().set_context({
@@ -243,7 +296,7 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id):
books = Book.search([])
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,
'You are not allowed to access "Cashbook".',
@@ -298,7 +351,7 @@ class BookTestCase(ModuleTestCase):
'currency': company.currency.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'),
with Transaction().set_context({
@@ -315,7 +368,7 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id):
books = Book.search([])
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()
def test_book_permission_observer(self):
@@ -360,7 +413,7 @@ class BookTestCase(ModuleTestCase):
'currency': company.currency.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'),
with Transaction().set_context({
@@ -377,6 +430,6 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id):
books = Book.search([])
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
diff --git a/tests/test_line.py b/tests/test_line.py
index 63c644f..3bcd986 100644
--- a/tests/test_line.py
+++ b/tests/test_line.py
@@ -131,7 +131,7 @@ class LineTestCase(ModuleTestCase):
self.assertEqual(book.state, 'closed')
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,
*[
[book.lines[0]],
@@ -478,7 +478,7 @@ class LineTestCase(ModuleTestCase):
self.assertEqual(book.state, 'closed')
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,
[book.lines[0]])
@@ -570,7 +570,7 @@ class LineTestCase(ModuleTestCase):
'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'),
with Transaction().set_context({
@@ -585,7 +585,7 @@ class LineTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id):
lines = Line.search([])
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')
Line.write(*[
@@ -647,7 +647,7 @@ class LineTestCase(ModuleTestCase):
'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'),
with Transaction().set_context({
@@ -731,7 +731,7 @@ class LineTestCase(ModuleTestCase):
'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'),
with Transaction().set_context({
diff --git a/view/book_form.xml b/view/book_form.xml
index ec543c1..863bade 100644
--- a/view/book_form.xml
+++ b/view/book_form.xml
@@ -13,6 +13,10 @@ full copyright notices and license terms. -->
+
+
+
+
diff --git a/view/book_list.xml b/view/book_list.xml
index 892ed2e..74acad6 100644
--- a/view/book_list.xml
+++ b/view/book_list.xml
@@ -5,6 +5,7 @@ full copyright notices and license terms. -->
+
diff --git a/view/line_list.xml b/view/line_list.xml
index 4638907..9d959d4 100644
--- a/view/line_list.xml
+++ b/view/line_list.xml
@@ -9,6 +9,7 @@ full copyright notices and license terms. -->
+