Compare commits

...

28 commits
main ... 7.0

Author SHA1 Message Date
Frederik Jaeckel
93af84690d Version 7.0.36 2024-07-19 15:43:30 +02:00
Frederik Jaeckel
531c6b1537 Use 'irrulecontext' to add 'user_id' to context of ir.rule 2024-07-19 15:42:05 +02:00
Frederik Jaeckel
9cb4b15c45 update gitignore 2024-07-19 15:41:58 +02:00
Frederik Jaeckel
fef3138313 Etikett ver 7.0.35 zum Änderungssatz 5c39bc8d615c hinzugefügt 2024-06-01 11:19:21 +02:00
Frederik Jaeckel
fc6c2aa7de Version 7.0.35 2024-06-01 11:18:58 +02:00
Frederik Jaeckel
d83e8851c8 Config: setting for booking-wizard - 'fixate' 2024-06-01 11:13:47 +02:00
Frederik Jaeckel
8881d1e3af tests valuestore: allow add of data to cashbook-line 2023-12-31 13:17:47 +01:00
Frederik Jaeckel
8826711b0f Etikett ver 7.0.34 zum Änderungssatz ca3bc8bb4652 hinzugefügt 2024-05-30 12:57:26 +02:00
Frederik Jaeckel
5333c47eab Version 7.0.34 2024-05-30 12:57:13 +02:00
Frederik Jaeckel
c4d48292fc hgignore 2024-05-30 12:44:27 +02:00
Frederik Jaeckel
7d8241d5e8 Allow to fixate a booking from Booking-Wizard 2024-05-30 12:44:11 +02:00
Frederik Jaeckel
27628a5c2a re-calculate values @ module install 2024-01-20 18:09:03 +01:00
Frederik Jaeckel
326c3decd6 Etikett ver 7.0.33 zum Änderungssatz 6b2661e04bff hinzugefügt 2023-12-31 10:20:18 +01:00
Frederik Jaeckel
410c1a977c Version 7.0.33 2023-12-31 10:20:05 +01:00
Frederik Jaeckel
96f730ccfc cashbook: optimize for speed for checking rows 2023-12-29 22:55:41 +01:00
Frederik Jaeckel
592dc03959 valuestore: fix test for number of fields 2023-12-29 22:53:27 +01:00
Frederik Jaeckel
4455ab7d2b cashbook: add (optional) balance_all + balance_ref to list-view 2023-12-29 14:52:50 +01:00
Frederik Jaeckel
a7f014274a optimize searcher/sort, add search/sort to balance_ref 2023-12-29 14:51:39 +01:00
Frederik Jaeckel
3548f29277 add worker-based precalculation of cashbook-values 2023-12-27 15:49:02 +01:00
Frederik Jaeckel
3744a96a99 remove caching 2023-12-23 10:36:58 +01:00
Frederik Jaeckel
89ec926ce9 add user_id to context of ir.rule for cashbook.split 2023-12-13 21:52:27 +01:00
Frederik Jaeckel
7a7d6c0fbf Etikett ver 7.0.32 zum Änderungssatz c9ed06fe4724 hinzugefügt 2023-12-06 20:44:51 +01:00
Frederik Jaeckel
826cd9df91 Version 7.0.32 2023-12-06 20:44:41 +01:00
Frederik Jaeckel
d5d09d6c5b columns optional 2023-12-06 20:20:13 +01:00
Frederik Jaeckel
9d8515ac10 readme 2023-11-30 13:53:13 +01:00
Frederik Jaeckel
dac3d679e9 allow extension of context-models 2023-12-04 20:06:02 +01:00
Frederik Jaeckel
7399c26a29 Etikett ver 7.0.31 zum Änderungssatz e40433ff9fdc hinzugefügt 2023-11-30 13:29:56 +01:00
Frederik Jaeckel
370ca4d72b Version 7.0.31 2023-11-30 13:29:42 +01:00
35 changed files with 1579 additions and 425 deletions

View file

@ -1,7 +1,8 @@
syntax: glob
*.pyc
build/*
dist/*
mds_cashbook.egg-info/*
locale/convert_de2en.py
__pycache__/*
.pytest_cache/*
.env

View file

@ -153,6 +153,27 @@ currency are converted into the display currency of the parent cash book.
Changes
=======
*7.0.0 - 30.11.2023*
*7.0.36 - 19.07.2024*
- compatibility tu Tryton 7.0
- updt: optimize check of permissions
*7.0.35 - 01.06.2024*
- add: config setting for fixate in booking-wizard
*7.0.34 - 30.05.2024*
- add: fixate of booking from booking-wizard
*7.0.33 - 31.12.2023*
- remove caching
- add worker-based precalculation of cashbook-values
*7.0.32 - 06.12.2023*
- columns optional
*7.0.31 - 30.11.2023*
- compatibility to Tryton 7.0

View file

@ -16,13 +16,13 @@ from .category import Category
from .reconciliation import Reconciliation
from .cbreport import ReconciliationReport
from .currency import CurrencyRate
from .model import MemCache
from .valuestore import ValueStore
from .ir import Rule
from .cron import Cron
def register():
Pool.register(
MemCache,
Configuration,
UserConfiguration,
CurrencyRate,
@ -36,7 +36,9 @@ def register():
OpenCashBookStart,
RunCbReportStart,
EnterBookingStart,
ValueStore,
Rule,
Cron,
module='cashbook', type_='model')
Pool.register(
ReconciliationReport,

212
book.py
View file

@ -4,30 +4,19 @@
# full copyright notices and license terms.
from trytond.model import (
Workflow, ModelView, ModelSQL, fields, Check,
tree, Index)
from trytond.pyson import Eval, Or, Bool, Id, Len
Workflow, ModelView, ModelSQL, fields, Check, tree, Index)
from trytond.pyson import Eval, Or, Bool, Id
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 trytond.config import config
from decimal import Decimal
from datetime import date
from sql.aggregate import Sum
from sql.conditionals import Case
from .model import order_name_hierarchical, sub_ids_hierarchical, \
AnyInArray, CACHEKEY_CURRENCY
# enable/disable caching of cachekey for 'currency.rate'
if config.get(
'cashbook', 'cache_currency', default='yes'
).lower() in ['yes', '1', 'true']:
ENA_CURRKEY = True
else:
ENA_CURRKEY = False
from .model import (
order_name_hierarchical, sub_ids_hierarchical, AnyInArray)
STATES = {
@ -74,8 +63,8 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
states={
'readonly': Or(
STATES['readonly'],
Len(Eval('lines')) > 0),
}, depends=DEPENDS+['lines'])
Eval('has_lines', False))},
depends=DEPENDS+['has_lines'])
feature = fields.Function(fields.Char(
string='Feature', readonly=True,
states={'invisible': True}), 'on_change_with_feature')
@ -97,6 +86,9 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
string='Lines', field='cashbook',
model_name='cashbook.line',
states=STATES, depends=DEPENDS)
has_lines = fields.Function(fields.Boolean(
string='Has Lines', readonly=True, states={'invisible': True}),
'on_change_with_has_lines')
reconciliations = fields.One2Many(
string='Reconciliations',
field='cashbook', model_name='cashbook.recon',
@ -123,10 +115,14 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
states={
'readonly': Or(
STATES2['readonly'],
Len(Eval('lines')) > 0),
Eval('has_lines', False)),
'invisible': STATES2['invisible'],
'required': ~STATES2['invisible'],
}, depends=DEPENDS2+['lines'])
}, depends=DEPENDS2+['has_lines'])
value_store = fields.One2Many(
string='Values', model_name='cashbook.values', field='cashbook',
readonly=True)
balance = fields.Function(fields.Numeric(
string='Balance',
readonly=True, depends=['currency_digits'],
@ -139,7 +135,6 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
help='Balance of all bookings',
digits=(16, Eval('currency_digits', 2))),
'get_balance_cashbook', searcher='search_balance')
balance_ref = fields.Function(fields.Numeric(
string='Balance (Ref.)',
help='Balance in company currency',
@ -147,7 +142,7 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
states={
'invisible': ~Bool(Eval('company_currency')),
}, depends=['company_currency_digits', 'company_currency']),
'get_balance_cashbook')
'get_balance_cashbook', searcher='search_balance')
company_currency = fields.Function(fields.Many2One(
readonly=True,
string='Company Currency', states={'invisible': True},
@ -163,8 +158,8 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
states={
'readonly': Or(
STATES2['readonly'],
Len(Eval('lines', [])) > 0),
}, depends=DEPENDS2+['lines'])
Eval('has_lines', False))},
depends=DEPENDS2+['has_lines'])
currency_digits = fields.Function(fields.Integer(
string='Currency Digits',
readonly=True), 'on_change_with_currency_digits')
@ -354,47 +349,141 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
)
return (query, tab_line)
@classmethod
def work_order_balance(cls, tables, field_name):
""" get order-query
"""
pool = Pool()
Book2 = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
context = Transaction().context
query_date = context.get('date', None)
table, _ = tables[None]
if query_date is not None:
if field_name == 'balance_ref':
raise UserError(gettext(
'cashbook.msg_nosearch_with_date',
fname=field_name, model=Book2.__name__))
(tab_book, tab2) = Book2.get_balance_of_cashbook_sql()
query = tab_book.select(
getattr(tab_book, field_name),
where=tab_book.cashbook == table.id)
return [query]
else:
tab_val = ValueStore.__table__()
tab_book = Book2.__table__()
query = tab_book.join(
tab_val,
condition=(
tab_book.id == tab_val.cashbook) & (
tab_val.field_name == field_name),
).select(
tab_val.numvalue,
where=tab_book.id == table.id)
return [query]
@staticmethod
def order_balance(tables):
""" order by balance
"""
Book2 = Pool().get('cashbook.book')
(tab_book, tab2) = Book2.get_balance_of_cashbook_sql()
table, _ = tables[None]
query = tab_book.select(
tab_book.balance,
where=tab_book.cashbook == table.id)
return [query]
return Book2.work_order_balance(tables, 'balance')
@staticmethod
def order_balance_all(tables):
""" order by balance-all
"""
Book2 = Pool().get('cashbook.book')
(tab_book, tab2) = Book2.get_balance_of_cashbook_sql()
table, _ = tables[None]
return Book2.work_order_balance(tables, 'balance_all')
query = tab_book.select(
tab_book.balance_all,
where=tab_book.cashbook == table.id)
return [query]
@staticmethod
def order_balance_ref(tables):
""" order by balance-all
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_balance(tables, 'balance_ref')
@classmethod
def search_balance(cls, name, clause):
""" search in 'balance'
"""
(tab_line, tab2) = cls.get_balance_of_cashbook_sql()
ValueStore = Pool().get('cashbook.values')
Operator = fields.SQL_OPERATORS[clause[1]]
context = Transaction().context
query_date = context.get('date', None)
if query_date is not None:
if name == 'balance_ref':
raise UserError(gettext(
'cashbook.msg_nosearch_with_date',
fname=name, model=cls.__name__))
(tab_line, tab2) = cls.get_balance_of_cashbook_sql()
query = tab_line.select(
tab_line.cashbook,
where=Operator(
getattr(tab_line, name), clause[2]))
return [('id', 'in', query)]
else:
value_query = ValueStore.search([
('field_name', '=', clause[0]),
('numvalue',) + tuple(clause[1:]),
],
query=True)
return [('value_store', 'in', value_query)]
@classmethod
def valuestore_delete_records(cls, records):
""" delete value-records
"""
ValStore = Pool().get('cashbook.values')
if records:
ValStore.delete_values(records)
@classmethod
def valuestore_fields(cls):
""" field to update
"""
return ['balance', 'balance_all', 'balance_ref']
@classmethod
def valuestore_update_records(cls, records):
""" compute current values of records,
store to global storage
"""
ValStore = Pool().get('cashbook.values')
if records:
ValStore.update_values(
cls.get_balance_values(
records,
['balance', 'balance_all', 'balance_ref']))
@classmethod
def get_balance_cashbook(cls, cashbooks, names):
""" get balance of cashbooks
"""
context = Transaction().context
result = {x: {y.id: Decimal('0.0') for y in cashbooks} for x in names}
# return computed values if 'date' is in context
query_date = context.get('date', None)
if query_date is not None:
return cls.get_balance_values(cashbooks, names)
for cashbook in cashbooks:
for value in cashbook.value_store:
if value.field_name in names:
result[value.field_name][cashbook.id] = value.numvalue
return result
@classmethod
def get_balance_values(cls, cashbooks, names):
""" get balance of cashbook
"""
pool = Pool()
@ -402,7 +491,6 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
Currency = pool.get('currency.currency')
Company = pool.get('company.company')
IrDate = pool.get('ir.date')
MemCache = pool.get('cashbook.memcache')
tab_book = Book2.__table__()
tab_comp = Company.__table__()
cursor = Transaction().connection.cursor()
@ -420,28 +508,6 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
except Exception:
query_date = IrDate.today()
cache_keys = {
x.id: MemCache.get_key_by_record(
name='get_balance_cashbook',
record=x,
query=[{
'model': 'cashbook.line',
'query': [('cashbook.parent', 'child_of', [x.id])],
}, {
'model': 'currency.currency.rate',
'query': [('currency.id', '=', x.currency.id)],
'cachekey' if ENA_CURRKEY
else 'disabled': CACHEKEY_CURRENCY % x.currency.id,
}, ],
addkeys=[query_date.isoformat()])
for x in cashbooks}
# read from cache
(todo_cashbook, result) = MemCache.read_from_cache(
cashbooks, cache_keys, names, result)
if len(todo_cashbook) == 0:
return result
# query balances of cashbooks and sub-cashbooks
with Transaction().set_context({
'date': query_date}):
@ -466,7 +532,7 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
Sum(tab_line.balance_all).as_('balance_all'),
group_by=[
tab_book.id, tab_line.currency, tab_comp.currency],
where=tab_book.id.in_([x.id for x in todo_cashbook]),
where=tab_book.id.in_([x.id for x in cashbooks]),
)
cursor.execute(*query)
records = cursor.fetchall()
@ -478,10 +544,19 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
record[2], record[5], record[1])
result['balance_ref'][record[0]] += Currency.compute(
record[2], record[5], record[3])
MemCache.store_result(cashbooks, cache_keys, result, todo_cashbook)
return result
@fields.depends('id')
def on_change_with_has_lines(self, name=None):
""" return True if cashbook has lines
(we dont use 'if self.lines:' this would slow down the client)
"""
Line = Pool().get('cashbook.line')
if Line.search_count([('cashbook', '=', self.id)]):
return True
return False
@fields.depends('btype')
def on_change_with_feature(self, name=None):
""" get feature-set
@ -533,6 +608,14 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
"""
pass
@classmethod
def create(cls, vlist):
""" update values
"""
records = super(Book, cls).create(vlist)
cls.valuestore_update_records(records)
return records
@classmethod
def write(cls, *args):
""" deny update if book is not 'open'
@ -541,7 +624,9 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
actions = iter(args)
to_write_config = []
to_update = []
for books, values in zip(actions, actions):
to_update.extend(books)
for book in books:
# deny btype-->None if lines not empty
if 'btype' in values.keys():
@ -575,6 +660,7 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
if len(to_write_config) > 0:
ConfigUser.write(*to_write_config)
cls.valuestore_update_records(to_update)
@classmethod
def delete(cls, books):

View file

@ -71,6 +71,8 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin):
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[('btype', '!=', None), ('state', '=', 'open')]))
fixate = fields.MultiValue(fields.Boolean(
string='Fixate', help='Fixating of the booking is activated.'))
@classmethod
def multivalue_model(cls, field):
@ -81,7 +83,7 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin):
if field in [
'date_from', 'date_to', 'checked', 'done',
'catnamelong', 'defbook', 'book1', 'book2',
'book3', 'book4', 'book5']:
'book3', 'book4', 'book5', 'fixate']:
return pool.get('cashbook.configuration_user')
return super(Configuration, cls).multivalue_model(field)
@ -97,6 +99,10 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin):
def default_catnamelong(cls, **pattern):
return cls.multivalue_model('catnamelong').default_catnamelong()
@classmethod
def default_fixate(cls, **pattern):
return cls.multivalue_model('fixate').default_fixate()
# end Configuration
@ -175,6 +181,9 @@ class UserConfiguration(ModelSQL, UserValueMixin):
('owner.id', '=', Eval('iduser', -1))
], depends=['iduser'])
fixate = fields.Boolean(
string='Fixate', help='Fixating of the booking is activated.')
@classmethod
def default_checked(cls):
return True
@ -187,4 +196,8 @@ class UserConfiguration(ModelSQL, UserValueMixin):
def default_done(cls):
return False
@classmethod
def default_fixate(cls):
return False
# end UserConfiguration

18
cron.py Normal file
View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.pool import PoolMeta
class Cron(metaclass=PoolMeta):
__name__ = 'ir.cron'
@classmethod
def __setup__(cls):
super(Cron, cls).__setup__()
cls.method.selection.append(
('cashbook.values|maintenance_values', "Update Cashbooks"))
# end Cron

15
cron.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds.de for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.cron" id="asset_cron">
<field name="method">cashbook.values|maintenance_values</field>
<field name="interval_number" eval="1"/>
<field name="interval_type">hours</field>
</record>
</data>
</tryton>

View file

@ -3,8 +3,7 @@
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.pool import Pool, PoolMeta
from .model import CACHEKEY_CURRENCY
from trytond.pool import PoolMeta, Pool
class CurrencyRate(metaclass=PoolMeta):
@ -14,36 +13,51 @@ class CurrencyRate(metaclass=PoolMeta):
def create(cls, vlist):
""" update cache-value
"""
MemCache = Pool().get('cashbook.memcache')
pool = Pool()
Cashbook = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
records = super(CurrencyRate, cls).create(vlist)
for rate in records:
MemCache.record_update(CACHEKEY_CURRENCY % rate.currency.id, rate)
ValueStore.update_books(
ValueStore.get_book_by_books(
Cashbook.search([
('currency', 'in', [
x.currency.id for x in records])])))
return records
@classmethod
def write(cls, *args):
""" update cache-value
"""
MemCache = Pool().get('cashbook.memcache')
pool = Pool()
Cashbook = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
actions = iter(args)
all_rates = []
for rates, values in zip(actions, actions):
all_rates.extend(rates)
super(CurrencyRate, cls).write(*args)
actions = iter(args)
for rates, values in zip(actions, actions):
for rate in rates:
MemCache.record_update(
CACHEKEY_CURRENCY % rate.currency.id, rate)
ValueStore.update_books(
ValueStore.get_book_by_books(
Cashbook.search([
('currency', 'in', [
x.currency.id for x in all_rates])])))
@classmethod
def delete(cls, records):
""" set cache to None
"""
MemCache = Pool().get('cashbook.memcache')
pool = Pool()
Cashbook = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
for record in records:
MemCache.record_update(
CACHEKEY_CURRENCY % record.currency.id, None)
books = ValueStore.get_book_by_books(Cashbook.search([
('currency', 'in', [x.currency.id for x in records])]))
super(CurrencyRate, cls).delete(records)
ValueStore.update_books(books)
# end

23
ir.py
View file

@ -3,7 +3,6 @@
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.transaction import Transaction
from trytond.pool import PoolMeta
@ -11,17 +10,15 @@ class Rule(metaclass=PoolMeta):
__name__ = 'ir.rule'
@classmethod
def _get_context(cls, model_name):
context = super()._get_context(model_name)
if model_name in {'cashbook.book', 'cashbook.line', 'cashbook.recon'}:
context['user_id'] = Transaction().user
return context
@classmethod
def _get_cache_key(cls, model_name):
key = super()._get_cache_key(model_name)
if model_name in {'cashbook.book', 'cashbook.line', 'cashbook.recon'}:
key = (*key, Transaction().user)
return key
def _context_modelnames(cls):
""" list of models to add 'user_id' to context
"""
result = super(Rule, cls)._context_modelnames()
return result | {
'cashbook.book',
'cashbook.line',
'cashbook.recon',
'cashbook.split'
}
# end Rule

25
line.py
View file

@ -15,7 +15,7 @@ from sql import Literal
from sql.functions import DatePart
from sql.conditionals import Case
from .book import sel_state_book
from .mixin import SecondCurrencyMixin, MemCacheIndexMx
from .mixin import SecondCurrencyMixin
from .const import DEF_NONE
@ -49,9 +49,7 @@ STATES = {
DEPENDS = ['state', 'state_cashbook']
class Line(
SecondCurrencyMixin, MemCacheIndexMx, Workflow, ModelSQL,
ModelView):
class Line(SecondCurrencyMixin, Workflow, ModelSQL, ModelView):
'Cashbook Line'
__name__ = 'cashbook.line'
@ -1037,6 +1035,8 @@ class Line(
def create(cls, vlist):
""" add debit/credit
"""
ValueStore = Pool().get('cashbook.values')
vlist = [x.copy() for x in vlist]
for values in vlist:
values.update(cls.add_values_from_splitlines(values))
@ -1056,18 +1056,26 @@ class Line(
recname='%(date)s|%(descr)s' % {
'date': date_txt,
'descr': values.get('description', '-')}))
return super(Line, cls).create(vlist)
records = super(Line, cls).create(vlist)
if records:
ValueStore.update_books(ValueStore.get_book_by_line(records))
return records
@classmethod
def write(cls, *args):
""" deny update if cashbook.line!='open',
add or update debit/credit
"""
ValueStore = Pool().get('cashbook.values')
actions = iter(args)
to_write = []
to_update = []
for lines, values in zip(actions, actions):
cls.check_permission_write(lines, values)
to_update.extend(lines)
for line in lines:
if line.reconciliation:
# deny state-change to 'edit' if line is
@ -1111,12 +1119,19 @@ class Line(
super(Line, cls).write(*to_write)
if to_update:
ValueStore.update_books(ValueStore.get_book_by_line(to_update))
@classmethod
def delete(cls, lines):
""" deny delete if book is not 'open' or wf is not 'edit'
"""
ValueStore = Pool().get('cashbook.values')
cls.check_permission_delete(lines)
to_update = ValueStore.get_book_by_line(lines)
super(Line, cls).delete(lines)
ValueStore.update_books(to_update)
# end Line

View file

@ -3,6 +3,14 @@ msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
###########
# ir.cron #
###########
msgctxt "selection:ir.cron,method:"
msgid "Update Cashbooks"
msgstr "Kassenbücher Aktualisieren"
##############
# ir.message #
##############
@ -158,6 +166,14 @@ msgctxt "model:ir.message,text:msg_btype_general"
msgid "General"
msgstr "Allgemein"
msgctxt "model:ir.message,text:msg_value_exists_in_store"
msgid "The value already exists for the record."
msgstr "Der Wert existiert für den Datensatz bereits."
msgctxt "model:ir.message,text:msg_nosearch_with_date"
msgid "Search with 'date' no allowed for field '%(fname)s' on model '%(model)s'."
msgstr "Suche mit 'date' nicht erlaubt für Feld '%(fname)s' auf Modell '%(model)s'."
#############
# res.group #
@ -598,6 +614,10 @@ msgctxt "field:cashbook.book,booktransf_feature:"
msgid "Feature"
msgstr "Merkmal"
msgctxt "field:cashbook.book,value_store:"
msgid "Values"
msgstr "Werte"
##################
# cashbook.split #
@ -1238,6 +1258,14 @@ msgctxt "help:cashbook.configuration,book5:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration,fixate:"
msgid "Fixate"
msgstr "Festschreiben"
msgctxt "help:cashbook.configuration,fixate:"
msgid "Fixating of the booking is activated."
msgstr "Die Festschreiben der Buchung ist aktiviert."
msgctxt "field:cashbook.configuration,date_from:"
msgid "Start Date"
msgstr "Beginndatum"
@ -1358,6 +1386,14 @@ msgctxt "help:cashbook.configuration_user,book5:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration_user,fixate:"
msgid "Fixate"
msgstr "Festschreiben"
msgctxt "help:cashbook.configuration_user,fixate:"
msgid "Fixating of the booking is activated."
msgstr "Die Festschreiben der Buchung ist aktiviert."
##################
# cashbook.recon #
@ -1626,6 +1662,18 @@ msgctxt "field:cashbook.enterbooking.start,party:"
msgid "Party"
msgstr "Partei"
msgctxt "field:cashbook.enterbooking.start,description:"
msgid "Description"
msgstr "Beschreibung"
msgctxt "field:cashbook.enterbooking.start,fixate:"
msgid "Fixate"
msgstr "Festschreiben"
msgctxt "help:cashbook.enterbooking.start,fixate:"
msgid "The booking is fixed immediately."
msgstr "Die Buchung wird sofort festgeschrieben."
#########################
# cashbook.enterbooking #
@ -1645,3 +1693,27 @@ msgstr "Speichern"
msgctxt "wizard_button:cashbook.enterbooking,start,savenext_:"
msgid "Save & Next"
msgstr "Speichern & Weiter"
###################
# cashbook.values #
###################
msgctxt "model:cashbook.values,name:"
msgid "Value Store"
msgstr "Wertespeicher"
msgctxt "field:cashbook.values,resource:"
msgid "Resource"
msgstr "Ressource"
msgctxt "field:cashbook.values,field_name:"
msgid "Field Name"
msgstr "Feldname"
msgctxt "field:cashbook.values,numvalue:"
msgid "Value"
msgstr "Wert"
msgctxt "field:cashbook.values,valuedigits:"
msgid "Digits"
msgstr "Dezimalstellen"

View file

@ -2,6 +2,10 @@
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "selection:ir.cron,method:"
msgid "Update Cashbooks"
msgstr "Update Cashbooks"
msgctxt "model:ir.message,text:msg_type_short_unique"
msgid "The Abbreviation must be unique."
msgstr "The Abbreviation must be unique."
@ -154,6 +158,14 @@ msgctxt "model:ir.message,text:msg_btype_general"
msgid "General"
msgstr "General"
msgctxt "model:ir.message,text:msg_value_exists_in_store"
msgid "The value already exists for the record."
msgstr "The value already exists for the record."
msgctxt "model:ir.message,text:msg_nosearch_with_date"
msgid "Search with 'date' no allowed for field '%(fname)s' on model '%(model)s'."
msgstr "Search with 'date' no allowed for field '%(fname)s' on model '%(model)s'."
msgctxt "model:res.group,name:group_cashbook"
msgid "Cashbook"
msgstr "Cashbook"
@ -562,6 +574,10 @@ msgctxt "field:cashbook.book,booktransf_feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "field:cashbook.book,value_store:"
msgid "Values"
msgstr "Values"
msgctxt "model:cashbook.split,name:"
msgid "Split booking line"
msgstr "Split booking line"
@ -1170,6 +1186,14 @@ msgctxt "help:cashbook.configuration,book5:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration,fixate:"
msgid "Fixate"
msgstr "Fixate"
msgctxt "help:cashbook.configuration,fixate:"
msgid "Fixating of the booking is activated."
msgstr "Fixating of the booking is activated."
msgctxt "field:cashbook.configuration,date_from:"
msgid "Start Date"
msgstr "Start Date"
@ -1286,6 +1310,14 @@ msgctxt "help:cashbook.configuration_user,book5:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration_user,fixate:"
msgid "Fixate"
msgstr "Fixate"
msgctxt "help:cashbook.configuration_user,fixate:"
msgid "Fixating of the booking is activated."
msgstr "Fixating of the booking is activated."
msgctxt "model:cashbook.recon,name:"
msgid "Cashbook Reconciliation"
msgstr "Cashbook Reconciliation"
@ -1538,6 +1570,18 @@ msgctxt "field:cashbook.enterbooking.start,party:"
msgid "Party"
msgstr "Party"
msgctxt "field:cashbook.enterbooking.start,description:"
msgid "Description"
msgstr "Description"
msgctxt "field:cashbook.enterbooking.start,fixate:"
msgid "Fixate"
msgstr "Fixate"
msgctxt "help:cashbook.enterbooking.start,fixate:"
msgid "The booking is fixed immediately."
msgstr "The booking is fixed immediately."
msgctxt "model:cashbook.enterbooking,name:"
msgid "Enter Booking"
msgstr "Enter Booking"
@ -1550,3 +1594,23 @@ msgctxt "wizard_button:cashbook.enterbooking,start,save_:"
msgid "Save"
msgstr "Save"
msgctxt "wizard_button:cashbook.enterbooking,start,savenext_:"
msgid "Save & Next"
msgstr "Save & Next"
msgctxt "model:cashbook.values,name:"
msgid "Value Store"
msgstr "Value Store"
msgctxt "field:cashbook.values,resource:"
msgid "Resource"
msgstr "Resource"
msgctxt "field:cashbook.values,field_name:"
msgid "Field Name"
msgstr "Field Name"
msgctxt "field:cashbook.values,numvalue:"
msgid "Value"
msgstr "Value"

View file

@ -119,6 +119,12 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_btype_general">
<field name="text">General</field>
</record>
<record model="ir.message" id="msg_value_exists_in_store">
<field name="text">The value already exists for the record.</field>
</record>
<record model="ir.message" id="msg_nosearch_with_date">
<field name="text">Search with 'date' no allowed for field '%(fname)s' on model '%(model)s'.</field>
</record>
</data>
</tryton>

View file

@ -3,7 +3,7 @@
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import fields, Index
from trytond.model import fields
from trytond.pyson import Eval, Bool, Or
from trytond.pool import Pool
from trytond.modules.currency.ir import rate_decimal
@ -196,25 +196,3 @@ class SecondCurrencyMixin:
return 2
# end SecondCurrencyMixin
class MemCacheIndexMx:
""" add index to 'create_date' + 'write_date'
"""
__slots__ = ()
@classmethod
def __setup__(cls):
super(MemCacheIndexMx, cls).__setup__()
t = cls.__table__()
# add index
cls._sql_indexes.update({
Index(
t,
(t.write_date, Index.Range())),
Index(
t,
(t.create_date, Index.Range())),
})
# end MemCacheIndexMx

172
model.py
View file

@ -4,35 +4,14 @@
# full copyright notices and license terms.
from trytond.model import (
MultiValueMixin, ValueMixin, fields, Unique, Model, Index)
MultiValueMixin, ValueMixin, fields, Unique, Index)
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.cache import MemoryCache
from trytond.config import config
from datetime import timedelta
from decimal import Decimal
from sql import With
from sql.functions import Function
from sql.conditionals import Coalesce
import copy
from .const import DEF_NONE
if config.get('cashbook', 'memcache', default='yes').lower() \
in ['yes', '1', 'true']:
ENABLE_CACHE = True
else:
ENABLE_CACHE = False
if config.get('cashbook', 'sync', default='yes').lower() \
in ['yes', '1', 'true']:
ENABLE_CACHESYNC = True
else:
ENABLE_CACHESYNC = False
CACHEKEY_CURRENCY = 'currency-%s'
class ArrayAgg(Function):
"""input values, including nulls, concatenated into an array.
"""
@ -86,155 +65,6 @@ class Array(Function):
# end Array
class MemCache(Model):
""" store values to cache
"""
__name__ = 'cashbook.memcache'
_cashbook_value_cache = MemoryCache(
'cashbook.book.valuecache',
context=False,
duration=timedelta(seconds=60*60*4))
@classmethod
def read_value(cls, cache_key):
""" read values from cache
"""
if ENABLE_CACHE is False:
return None
return copy.deepcopy(cls._cashbook_value_cache.get(cache_key))
@classmethod
def store_result(cls, records, cache_keys, values, skip_records=[]):
""" store result to cache
"""
if ENABLE_CACHE is False:
return
for record in records:
if record not in skip_records:
continue
data = {
x: values[x][record.id]
for x in values.keys() if record.id in values[x].keys()}
cls._cashbook_value_cache.set(
cache_keys[record.id], copy.deepcopy(data))
if ENABLE_CACHESYNC is True:
cls._cashbook_value_cache.sync(Transaction())
@classmethod
def store_value(cls, cache_key, values):
""" store values to cache
"""
if ENABLE_CACHE is False:
return
cls._cashbook_value_cache.set(cache_key, copy.deepcopy(values))
@classmethod
def read_from_cache(cls, records, cache_keys, names, result):
""" get stored values from memcache
"""
if ENABLE_CACHE is False:
return (records, result)
todo_records = []
for record in records:
values = copy.deepcopy(cls.read_value(cache_keys[record.id]))
if values:
for name in names:
if name not in values.keys():
continue
if values[name] is None:
continue
if result[name][record.id] is None:
result[name][record.id] = Decimal('0.0')
result[name][record.id] += values[name]
else:
todo_records.append(record)
return (todo_records, result)
@classmethod
def get_key_by_record(cls, name, record, query, addkeys=[]):
""" read records to build a cache-key
"""
pool = Pool()
cursor = Transaction().connection.cursor()
if ENABLE_CACHE is False:
return '-'
fname = [name, str(record.id)]
fname.extend(addkeys)
# query the last edited record for each item in 'query'
for line in query:
if len(line.keys()) == 0:
continue
if 'cachekey' in line.keys():
key = cls.read_value(line['cachekey'])
if key:
fname.append(key)
continue
Model = pool.get(line['model'])
tab_model = Model.__table__()
tab_query = Model.search(line['query'], query=True)
qu1 = tab_model.join(
tab_query,
condition=tab_query.id == tab_model.id,
).select(
tab_model.id,
tab_model.write_date,
tab_model.create_date,
limit=1,
order_by=[
Coalesce(
tab_model.write_date, tab_model.create_date).desc,
tab_model.id.desc,
],
)
cursor.execute(*qu1)
records = cursor.fetchall()
if len(records) > 0:
fname.append(cls.genkey(
records[0][0],
records[0][1],
records[0][2],
))
else:
fname.append('0')
if 'cachekey' in line.keys():
key = cls.store_value(line['cachekey'], fname[-1])
return '-'.join(fname)
@classmethod
def genkey(cls, id_record, write_date, create_date):
""" get key as text
"""
date_val = write_date if write_date is not None else create_date
return '-'.join([
str(id_record),
'%s%s' % (
'w' if write_date is not None else 'c',
date_val.timestamp() if date_val is not None else '-'),
])
@classmethod
def record_update(cls, cache_key, record):
""" update cache-value
"""
if ENABLE_CACHE is False:
return
cls.store_value(
cache_key,
cls.genkey(record.id, record.write_date, record.create_date)
if record is not None else None)
# end mem_cache
def sub_ids_hierarchical(model_name):
""" get table with id and sub-ids
"""

View file

@ -11,7 +11,7 @@ from trytond.report import Report
from trytond.i18n import gettext
from .line import sel_bookingtype, sel_linestate, STATES, DEPENDS
from .book import sel_state_book
from .mixin import SecondCurrencyMixin, MemCacheIndexMx
from .mixin import SecondCurrencyMixin
sel_linetype = [
@ -25,7 +25,7 @@ sel_target = [
]
class SplitLine(SecondCurrencyMixin, MemCacheIndexMx, ModelSQL, ModelView):
class SplitLine(SecondCurrencyMixin, ModelSQL, ModelView):
'Split booking line'
__name__ = 'cashbook.split'

View file

@ -307,6 +307,10 @@ class BookTestCase(object):
Book.search_count([('balance_all', '<', Decimal('5.0'))]),
0)
self.assertEqual(
Book.search_count([('balance_ref', '<', Decimal('5.0'))]),
0)
@with_transaction()
def test_book_deny_btype_set_none(self):
""" create cashbook, add lines,

View file

@ -24,6 +24,7 @@ class BookingWizardTestCase(object):
Category = pool.get('cashbook.category')
Party = pool.get('party.party')
IrDate = pool.get('ir.date')
Config = pool.get('cashbook.configuration')
company = self.prep_company()
with Transaction().set_context({
@ -51,6 +52,10 @@ class BookingWizardTestCase(object):
'cattype': 'out',
}])
cfg1 = Config()
cfg1.fixate = True
cfg1.save()
(sess_id, start_state, end_state) = BookingWiz.create()
w_obj = BookingWiz(sess_id)
self.assertEqual(start_state, 'start')
@ -65,6 +70,7 @@ class BookingWizardTestCase(object):
self.assertEqual(result['view']['defaults']['booktransf'], None)
self.assertEqual(result['view']['defaults']['description'], None)
self.assertEqual(result['view']['defaults']['category'], None)
self.assertEqual(result['view']['defaults']['fixate'], True)
self.assertEqual(len(book.lines), 0)
@ -75,7 +81,7 @@ class BookingWizardTestCase(object):
'description': 'Test 1',
'category': categories[1].id,
'bookingtype': 'out',
}
'fixate': True}
for x in r1.keys():
setattr(w_obj.start, x, r1[x])
@ -88,6 +94,7 @@ class BookingWizardTestCase(object):
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Exp|-10.00 usd|Test 1 [Food]')
self.assertEqual(book.lines[0].state, 'check')
@with_transaction()
def test_bookwiz_transfer(self):
@ -147,6 +154,7 @@ class BookingWizardTestCase(object):
self.assertEqual(result['view']['defaults']['booktransf'], None)
self.assertEqual(result['view']['defaults']['description'], None)
self.assertEqual(result['view']['defaults']['category'], None)
self.assertEqual(result['view']['defaults']['fixate'], False)
self.assertEqual(len(books[0].lines), 0)
self.assertEqual(len(books[1].lines), 0)
@ -157,7 +165,7 @@ class BookingWizardTestCase(object):
'description': 'Test 1',
'booktransf': books[1].id,
'bookingtype': 'mvout',
}
'fixate': False}
for x in r1.keys():
setattr(w_obj.start, x, r1[x])

View file

@ -178,10 +178,12 @@ class ConfigTestCase(object):
self.assertEqual(cfg2.date_to, None)
self.assertEqual(cfg2.checked, True)
self.assertEqual(cfg2.done, False)
self.assertEqual(cfg2.fixate, False)
cfg2.date_from = date(2022, 4, 1)
cfg2.date_to = date(2022, 5, 30)
cfg2.checked = False
cfg2.fixate = True
cfg2.save()
# change to user 'diego'
@ -194,6 +196,7 @@ class ConfigTestCase(object):
self.assertEqual(cfg2.date_to, None)
self.assertEqual(cfg2.checked, True)
self.assertEqual(cfg2.done, False)
self.assertEqual(cfg2.fixate, False)
cfg2.date_from = date(2022, 4, 15)
cfg2.date_to = date(2022, 5, 15)
@ -209,5 +212,6 @@ class ConfigTestCase(object):
self.assertEqual(cfg2.date_to, date(2022, 5, 30))
self.assertEqual(cfg2.checked, False)
self.assertEqual(cfg2.done, False)
self.assertEqual(cfg2.fixate, True)
# end ConfigTestCase

View file

@ -1,85 +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.
from trytond.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.modules.cashbook.model import CACHEKEY_CURRENCY, ENABLE_CACHE
from datetime import date
from decimal import Decimal
import time
class CurrencyTestCase(object):
""" test currency
"""
@with_transaction()
def test_currency_update_cache(self):
""" add/update/del rate of currency, check cache
"""
pool = Pool()
MemCache = pool.get('cashbook.memcache')
Currency = pool.get('currency.currency')
CurrencyRate = pool.get('currency.currency.rate')
self.prep_config()
self.prep_company()
MemCache._cashbook_value_cache.clear_all()
currency, = Currency.search([('name', '=', 'usd')])
cache_key = CACHEKEY_CURRENCY % currency.id
# cache should be empty
self.assertEqual(MemCache.read_value(cache_key), None)
CurrencyRate.delete(currency.rates)
self.assertEqual(len(currency.rates), 0)
# add rate
Currency.write(*[
[currency],
{
'rates': [('create', [{
'date': date(2022, 5, 1),
'rate': Decimal('1.05'),
}])],
}])
self.assertEqual(len(currency.rates), 1)
# expected key
value = '%d-c%s' % (
currency.rates[0].id,
str(currency.rates[0].create_date.timestamp()))
if ENABLE_CACHE is True:
self.assertEqual(MemCache.read_value(cache_key), value)
else:
self.assertEqual(MemCache.read_value(cache_key), None)
time.sleep(1.0)
Currency.write(*[
[currency],
{
'rates': [
('write', [currency.rates[0].id], {
'rate': Decimal('1.06'),
})],
}])
self.assertEqual(len(currency.rates), 1)
value = '%d-w%s' % (
currency.rates[0].id,
str(currency.rates[0].write_date.timestamp()))
if ENABLE_CACHE is True:
self.assertEqual(MemCache.read_value(cache_key), value)
else:
self.assertEqual(MemCache.read_value(cache_key), None)
Currency.write(*[
[currency],
{
'rates': [('delete', [currency.rates[0].id])],
}])
self.assertEqual(MemCache.read_value(cache_key), None)
# end CurrencyTestCase

View file

@ -291,6 +291,8 @@ class LineTestCase(object):
self.assertEqual(books[0].lines[0].reference, None)
self.assertEqual(len(books[0].lines[0].references), 1)
self.prep_valstore_run_worker()
self.assertEqual(
books[0].lines[0].rec_name,
'05/05/2022|to|-10.00 usd|Transfer USD --> ' +
@ -366,6 +368,8 @@ class LineTestCase(object):
self.assertEqual(books[0].lines[0].reference, None)
self.assertEqual(len(books[0].lines[0].references), 1)
self.prep_valstore_run_worker()
self.assertEqual(
books[0].lines[0].rec_name,
'05/05/2022|from|10.00 usd|Transfer USD <-- ' +
@ -458,6 +462,8 @@ class LineTestCase(object):
self.assertEqual(books[0].lines[0].reference, None)
self.assertEqual(len(books[0].lines[0].references), 1)
self.prep_valstore_run_worker()
# 10 CHF --> USD: USD = CHF * 1.05 / 1.04
# 10 CHF = 10.0961538 USD
# EUR | USD | CHF
@ -1147,6 +1153,8 @@ class LineTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(len(book.lines), 1)
self.assertEqual(
book.lines[0].rec_name,
@ -1216,6 +1224,8 @@ class LineTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(len(book.lines), 1)
self.assertEqual(
book.lines[0].rec_name,

View file

@ -14,11 +14,11 @@ from .config import ConfigTestCase
from .category import CategoryTestCase
from .reconciliation import ReconTestCase
from .bookingwiz import BookingWizardTestCase
from .currency import CurrencyTestCase
from .valuestore import ValuestoreTestCase
class CashbookTestCase(
CurrencyTestCase,
ValuestoreTestCase,
BookingWizardTestCase,
ReconTestCase,
CategoryTestCase,

840
tests/valuestore.py Normal file
View file

@ -0,0 +1,840 @@
# -*- 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.
from datetime import date, timedelta
from decimal import Decimal
from trytond.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.exceptions import UserError
from trytond.transaction import Transaction
class ValuestoreTestCase(object):
""" test storage of values
"""
@with_transaction()
def test_valstore_update_currency_rate(self):
""" create cashbook, check update of cashbook on
update of rates of currency
"""
pool = Pool()
Book = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
Currency = pool.get('currency.currency')
Queue = pool.get('ir.queue')
types = self.prep_type()
company = self.prep_company()
(usd, euro) = self.prep_2nd_currency(company)
self.assertEqual(company.currency.rec_name, 'Euro')
with Transaction().set_context({
'company': company.id,
'date': date(2022, 5, 20)}):
self.assertEqual(Queue.search_count([]), 0)
category = self.prep_category(cattype='in')
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': usd.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': '10 US$',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('10.0'),
'party': party.id,
}, {
'date': date(2022, 5, 10),
'description': '5 US$',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('5.0'),
'party': party.id,
}])],
}])
# run worker
self.assertEqual(
ValueStore.search_count([]),
len(Book.valuestore_fields()))
self.prep_valstore_run_worker()
book, = Book.search([])
self.assertEqual(book.rec_name, 'Book 1 | 15.00 usd | Open')
self.assertEqual(book.balance, Decimal('15.0'))
self.assertEqual(book.balance_all, Decimal('15.0'))
self.assertEqual(book.balance_ref, Decimal('14.29'))
self.assertEqual(
len(book.value_store),
len(Book.valuestore_fields()))
self.assertEqual(
book.value_store[0].rec_name,
'[Book 1 | 15.00 usd | Open]|balance|15.00|2')
self.assertEqual(
book.value_store[1].rec_name,
'[Book 1 | 15.00 usd | Open]|balance_all|15.00|2')
self.assertEqual(
book.value_store[2].rec_name,
'[Book 1 | 15.00 usd | Open]|balance_ref|14.29|2')
# add rate to usd
self.assertEqual(Queue.search_count([]), 0)
Currency.write(*[
[usd],
{
'rates': [('create', [{
'date': date(2022, 5, 6),
'rate': Decimal('1.08'),
}])],
}])
self.assertEqual(Queue.search_count([]), 1)
self.prep_valstore_run_worker()
self.assertEqual(Queue.search_count([]), 0)
# check reference-currency
book, = Book.search([])
self.assertEqual(book.rec_name, 'Book 1 | 15.00 usd | Open')
self.assertEqual(book.balance, Decimal('15.0'))
self.assertEqual(book.balance_all, Decimal('15.0'))
self.assertEqual(book.balance_ref, Decimal('13.89'))
self.assertEqual(
len(book.value_store),
len(Book.valuestore_fields()))
self.assertEqual(
book.value_store[0].rec_name,
'[Book 1 | 15.00 usd | Open]|balance|15.00|2')
self.assertEqual(
book.value_store[1].rec_name,
'[Book 1 | 15.00 usd | Open]|balance_all|15.00|2')
self.assertEqual(
book.value_store[2].rec_name,
'[Book 1 | 15.00 usd | Open]|balance_ref|13.89|2')
# find rate
self.assertEqual(len(usd.rates), 2)
self.assertEqual(usd.rates[0].date, date(2022, 5, 6))
self.assertEqual(usd.rates[0].rate, Decimal('1.08'))
# update rate
self.assertEqual(Queue.search_count([]), 0)
Currency.write(*[
[usd],
{
'rates': [(
'write',
[usd.rates[0]],
{'rate': Decimal('1.12')})],
}])
self.assertEqual(Queue.search_count([]), 1)
self.prep_valstore_run_worker()
self.assertEqual(Queue.search_count([]), 0)
book, = Book.search([])
self.assertEqual(book.rec_name, 'Book 1 | 15.00 usd | Open')
self.assertEqual(book.balance_ref, Decimal('13.39'))
self.assertEqual(
book.value_store[2].rec_name,
'[Book 1 | 15.00 usd | Open]|balance_ref|13.39|2')
# delete rate
self.assertEqual(Queue.search_count([]), 0)
Currency.write(*[
[usd],
{
'rates': [('delete', [usd.rates[0]])],
}])
self.assertEqual(Queue.search_count([]), 1)
self.prep_valstore_run_worker()
self.assertEqual(Queue.search_count([]), 0)
book, = Book.search([])
self.assertEqual(book.rec_name, 'Book 1 | 15.00 usd | Open')
self.assertEqual(book.balance_ref, Decimal('14.29'))
self.assertEqual(
book.value_store[2].rec_name,
'[Book 1 | 15.00 usd | Open]|balance_ref|14.29|2')
def prep_valstore_run_worker(self):
""" run tasks from queue
"""
Queue = Pool().get('ir.queue')
tasks = Queue.search([])
for task in tasks:
task.run()
Queue.delete(tasks)
@with_transaction()
def test_valstore_update_store_values(self):
""" create cashbook, store value
"""
pool = Pool()
Book = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
Queue = pool.get('ir.queue')
types = self.prep_type()
company = self.prep_company()
(usd, euro) = self.prep_2nd_currency(company)
self.assertEqual(company.currency.rec_name, 'Euro')
with Transaction().set_context({'company': company.id}):
self.assertEqual(Queue.search_count([]), 0)
category = self.prep_category(cattype='in')
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': usd.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': '10 US$',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('10.0'),
'party': party.id,
}, {
'date': date(2022, 5, 10),
'description': '5 US$',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('5.0'),
'party': party.id,
}])],
}])
# run worker
self.assertEqual(
ValueStore.search_count([]),
len(Book.valuestore_fields()))
self.prep_valstore_run_worker()
# check values until 2022-05-05
with Transaction().set_context({
'date': date(2022, 5, 5)}):
book, = Book.search([])
self.assertEqual(book.rec_name, 'Book 1 | 10.00 usd | Open')
self.assertEqual(book.balance, Decimal('10.0'))
self.assertEqual(book.balance_all, Decimal('15.0'))
self.assertEqual(book.balance_ref, Decimal('14.29'))
self.assertEqual(
len(book.value_store),
len(Book.valuestore_fields()))
self.assertEqual(
book.value_store[0].rec_name,
'[Book 1 | 10.00 usd | Open]|balance|15.00|2')
self.assertEqual(
book.value_store[1].rec_name,
'[Book 1 | 10.00 usd | Open]|balance_all|15.00|2')
self.assertEqual(
book.value_store[2].rec_name,
'[Book 1 | 10.00 usd | Open]|balance_ref|14.29|2')
# values created by book-create, without context
self.assertEqual(
ValueStore.search_count([]),
len(Book.valuestore_fields()))
values = ValueStore.search([], order=[('field_name', 'ASC')])
self.assertEqual(
len(values),
len(Book.valuestore_fields()))
self.assertEqual(
values[0].rec_name,
'[Book 1 | 10.00 usd | Open]|balance|15.00|2')
self.assertEqual(
values[1].rec_name,
'[Book 1 | 10.00 usd | Open]|balance_all|15.00|2')
self.assertEqual(
values[2].rec_name,
'[Book 1 | 10.00 usd | Open]|balance_ref|14.29|2')
# check write of too much digits
self.assertRaisesRegex(
UserError,
r'The number of digits in the value ' +
r'"' + r"'12.345'" +
r'" for field "Value" in "[Book 1 | 10\.00 usd | ' +
r'Open]|balance|12\.35|2" of "Value Store" exceeds ' +
r'the limit of "2".',
ValueStore.write,
*[
[values[0]],
{
'numvalue': Decimal('12.345'),
}
])
# update with context
Book.valuestore_update_records([book])
values = ValueStore.search([], order=[('field_name', 'ASC')])
self.assertEqual(
len(values),
len(Book.valuestore_fields()))
self.assertEqual(
values[0].rec_name,
'[Book 1 | 10.00 usd | Open]|balance|10.00|2')
self.assertEqual(
values[1].rec_name,
'[Book 1 | 10.00 usd | Open]|balance_all|15.00|2')
self.assertEqual(
values[2].rec_name,
'[Book 1 | 10.00 usd | Open]|balance_ref|14.29|2')
# check values until 2022-05-15
with Transaction().set_context({
'date': date(2022, 5, 15)}):
book, = Book.search([])
self.assertEqual(book.rec_name, 'Book 1 | 15.00 usd | Open')
self.assertEqual(book.balance, Decimal('15.0'))
self.assertEqual(book.balance_all, Decimal('15.0'))
self.assertEqual(book.balance_ref, Decimal('14.29'))
# update values
self.assertEqual(
ValueStore.search_count([]),
len(Book.valuestore_fields()))
Book.valuestore_update_records([book])
values = ValueStore.search(
[('field_name', 'in', [
'balance', 'balance_all', 'balance_ref'])],
order=[('field_name', 'ASC')])
self.assertEqual(len(values), 3)
self.assertEqual(
values[0].rec_name,
'[Book 1 | 15.00 usd | Open]|balance|15.00|2')
self.assertEqual(
values[1].rec_name,
'[Book 1 | 15.00 usd | Open]|balance_all|15.00|2')
self.assertEqual(
values[2].rec_name,
'[Book 1 | 15.00 usd | Open]|balance_ref|14.29|2')
# delete book, should delete values
Book.write(*[
[book],
{'lines': [('delete', [x.id for x in book.lines])]}
])
self.assertEqual(
ValueStore.search_count([]),
len(Book.valuestore_fields()))
Book.delete([book])
self.assertEqual(ValueStore.search_count([]), 0)
@with_transaction()
def test_valstore_update_store_values_line(self):
""" create cashbooks hierarchical, add lines
check update of parent cashbooks
"""
pool = Pool()
Book = pool.get('cashbook.book')
Line = pool.get('cashbook.line')
ValueStore = pool.get('cashbook.values')
types = self.prep_type()
company = self.prep_company()
(usd, euro) = self.prep_2nd_currency(company)
self.assertEqual(company.currency.rec_name, 'Euro')
with Transaction().set_context({'company': company.id}):
category = self.prep_category(cattype='in')
party = self.prep_party()
book, = Book.create([{
'name': 'Lev 0',
'btype': types.id,
'company': company.id,
'currency': usd.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': '10 US$',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('10.0'),
'party': party.id,
}])],
'childs': [('create', [{
'name': 'Lev 1a',
'btype': types.id,
'company': company.id,
'currency': euro.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
}, {
'name': 'Lev 1b',
'btype': types.id,
'company': company.id,
'currency': euro.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
}])],
}])
self.assertEqual(book.rec_name, 'Lev 0 | 10.00 usd | Open')
self.assertEqual(len(book.lines), 1)
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev|10.00 usd|10 US$ [Cat1]')
self.assertEqual(len(book.childs), 2)
self.assertEqual(
book.childs[0].rec_name, 'Lev 0/Lev 1a | 0.00 € | Open')
self.assertEqual(len(book.childs[0].lines), 0)
self.assertEqual(
book.childs[1].rec_name, 'Lev 0/Lev 1b | 0.00 € | Open')
self.assertEqual(len(book.childs[1].lines), 0)
self.assertEqual(
ValueStore.search_count([]),
3 * len(Book.valuestore_fields()))
self.prep_valstore_run_worker()
values = ValueStore.search(
[('field_name', 'in', [
'balance', 'balance_all', 'balance_ref'])],
order=[('cashbook', 'ASC'), ('field_name', 'ASC')])
self.assertEqual(len(values), 3 * 3)
self.assertEqual(
values[0].rec_name,
'[Lev 0 | 10.00 usd | Open]|balance|10.00|2')
self.assertEqual(
values[1].rec_name,
'[Lev 0 | 10.00 usd | Open]|balance_all|10.00|2')
self.assertEqual(
values[2].rec_name,
'[Lev 0 | 10.00 usd | Open]|balance_ref|9.52|2')
self.assertEqual(
values[3].rec_name,
'[Lev 0/Lev 1a | 0.00 € | Open]|balance|0.00|2')
self.assertEqual(
values[4].rec_name,
'[Lev 0/Lev 1a | 0.00 € | Open]|balance_all|0.00|2')
self.assertEqual(
values[5].rec_name,
'[Lev 0/Lev 1a | 0.00 € | Open]|balance_ref|0.00|2')
self.assertEqual(
values[6].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance|0.00|2')
self.assertEqual(
values[7].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance_all|0.00|2')
self.assertEqual(
values[8].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance_ref|0.00|2')
# add bookings
Line.create(self.prep_valstore_line_create_data(
[{
'cashbook': values[0].cashbook.id, # Lev 0
'amount': Decimal('2.0'),
'bookingtype': 'in',
'category': category.id,
'date': date(2022, 5, 10),
}, {
'cashbook': values[3].cashbook.id, # Lev 1a
'amount': Decimal('3.0'),
'bookingtype': 'in',
'category': category.id,
'date': date(2022, 5, 10),
}]))
# add 'date' to context, will return computed
# (not stored) values
with Transaction().set_context({'date': date(2022, 5, 10)}):
values = ValueStore.search(
[('field_name', 'in', [
'balance', 'balance_all', 'balance_ref'])],
order=[('cashbook', 'ASC'), ('field_name', 'ASC')])
self.assertEqual(len(values), 9)
self.assertEqual(
values[0].rec_name,
'[Lev 0 | 15.15 usd | Open]|balance|10.00|2')
self.assertEqual(
values[1].rec_name,
'[Lev 0 | 15.15 usd | Open]|balance_all|10.00|2')
self.assertEqual(
values[2].rec_name,
'[Lev 0 | 15.15 usd | Open]|balance_ref|9.52|2')
self.assertEqual(
values[3].rec_name,
'[Lev 0/Lev 1a | 3.00 € | Open]|balance|0.00|2')
self.assertEqual(
values[4].rec_name,
'[Lev 0/Lev 1a | 3.00 € | Open]|balance_all|0.00|2')
self.assertEqual(
values[5].rec_name,
'[Lev 0/Lev 1a | 3.00 € | Open]|balance_ref|0.00|2')
self.assertEqual(
values[6].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance|0.00|2')
self.assertEqual(
values[7].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance_all|0.00|2')
self.assertEqual(
values[8].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance_ref|0.00|2')
# before run of workers - w/o 'date' in context
values = ValueStore.search(
[('field_name', 'in', [
'balance', 'balance_all', 'balance_ref'])],
order=[('cashbook', 'ASC'), ('field_name', 'ASC')])
self.assertEqual(len(values), 9)
self.assertEqual(
values[0].rec_name,
'[Lev 0 | 10.00 usd | Open]|balance|10.00|2')
self.assertEqual(
values[1].rec_name,
'[Lev 0 | 10.00 usd | Open]|balance_all|10.00|2')
self.assertEqual(
values[2].rec_name,
'[Lev 0 | 10.00 usd | Open]|balance_ref|9.52|2')
self.assertEqual(
values[3].rec_name,
'[Lev 0/Lev 1a | 0.00 € | Open]|balance|0.00|2')
self.assertEqual(
values[4].rec_name,
'[Lev 0/Lev 1a | 0.00 € | Open]|balance_all|0.00|2')
self.assertEqual(
values[5].rec_name,
'[Lev 0/Lev 1a | 0.00 € | Open]|balance_ref|0.00|2')
self.assertEqual(
values[6].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance|0.00|2')
self.assertEqual(
values[7].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance_all|0.00|2')
self.assertEqual(
values[8].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance_ref|0.00|2')
self.prep_valstore_run_worker()
# after run of workers
values = ValueStore.search(
[('field_name', 'in', [
'balance', 'balance_all', 'balance_ref'])],
order=[('cashbook', 'ASC'), ('field_name', 'ASC')])
self.assertEqual(len(values), 9)
self.assertEqual(
values[0].rec_name,
'[Lev 0 | 15.15 usd | Open]|balance|15.15|2')
self.assertEqual(
values[1].rec_name,
'[Lev 0 | 15.15 usd | Open]|balance_all|15.15|2')
self.assertEqual(
values[2].rec_name,
'[Lev 0 | 15.15 usd | Open]|balance_ref|14.43|2')
self.assertEqual(
values[3].rec_name,
'[Lev 0/Lev 1a | 3.00 € | Open]|balance|3.00|2')
self.assertEqual(
values[4].rec_name,
'[Lev 0/Lev 1a | 3.00 € | Open]|balance_all|3.00|2')
self.assertEqual(
values[5].rec_name,
'[Lev 0/Lev 1a | 3.00 € | Open]|balance_ref|3.00|2')
self.assertEqual(
values[6].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance|0.00|2')
self.assertEqual(
values[7].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance_all|0.00|2')
self.assertEqual(
values[8].rec_name,
'[Lev 0/Lev 1b | 0.00 € | Open]|balance_ref|0.00|2')
def prep_valstore_line_create_data(self, query):
""" allow add of data
"""
return query
@with_transaction()
def test_valstore_search_sort_books(self):
""" create cashbooks add lines, search/sort
with and w/o 'date' in context
"""
pool = Pool()
Book = pool.get('cashbook.book')
Line = pool.get('cashbook.line')
types = self.prep_type()
company = self.prep_company()
(usd, euro) = self.prep_2nd_currency(company)
self.assertEqual(company.currency.rec_name, 'Euro')
with Transaction().set_context({'company': company.id}):
category = self.prep_category(cattype='in')
party = self.prep_party()
books = Book.create([{
'name': 'Cashbook 1',
'btype': types.id,
'company': company.id,
'currency': usd.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': '10 US$',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('10.0'),
'party': party.id,
}])]}, {
'name': 'Cashbook 2',
'btype': types.id,
'company': company.id,
'currency': euro.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
}, {
'name': 'Cashbook 3',
'btype': types.id,
'company': company.id,
'currency': euro.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
}])
self.assertEqual(len(books), 3)
self.assertEqual(books[0].rec_name, 'Cashbook 1 | 10.00 usd | Open')
self.assertEqual(books[1].rec_name, 'Cashbook 2 | 0.00 € | Open')
self.assertEqual(books[2].rec_name, 'Cashbook 3 | 0.00 € | Open')
Line.create(self.prep_valstore_line_create_data(
[{
'cashbook': books[1].id,
'bookingtype': 'in',
'amount': Decimal('5.0'),
'date': date(2022, 5, 6),
'description': '5€ in',
'category': category.id,
}, {
'cashbook': books[2].id,
'bookingtype': 'in',
'amount': Decimal('6.0'),
'date': date(2022, 5, 7),
'description': '6€ in',
'category': category.id,
}]))
# no 'date' in context, using stored values
# workers not yet processed
books = Book.search([], order=[('name', 'ASC')])
self.assertEqual(len(books), 3)
self.assertEqual(books[0].rec_name, 'Cashbook 1 | 10.00 usd | Open')
self.assertEqual(books[1].rec_name, 'Cashbook 2 | 0.00 € | Open')
self.assertEqual(books[2].rec_name, 'Cashbook 3 | 0.00 € | Open')
self.assertEqual(
Book.search_count([('balance', '=', Decimal('0.0'))]),
2)
self.assertEqual(
Book.search_count([('balance_all', '=', Decimal('0.0'))]),
2)
self.assertEqual(
Book.search_count([('balance_ref', '=', Decimal('0.0'))]),
2)
# check sorting - using stored values
books = Book.search([], order=[
('balance_all', 'DESC'), ('name', 'ASC')])
self.assertEqual(len(books), 3)
self.assertEqual(books[0].rec_name, 'Cashbook 1 | 10.00 usd | Open')
self.assertEqual(books[1].rec_name, 'Cashbook 2 | 0.00 € | Open')
self.assertEqual(books[2].rec_name, 'Cashbook 3 | 0.00 € | Open')
# search again with 'date' - using computed values
with Transaction().set_context({'date': date(2022, 5, 6)}):
self.assertEqual(
Book.search_count([('balance', '=', Decimal('5.0'))]),
1)
self.assertEqual(
Book.search_count([
('balance', '>=', Decimal('5.0')),
('balance', '<', Decimal('9.0')),
]),
1)
self.assertEqual(
Book.search_count([
('balance_all', '>=', Decimal('5.0'))]),
3)
self.assertRaisesRegex(
UserError,
"Search with 'date' no allowed for field " +
"'balance_ref' on model 'cashbook.book'.",
Book.search_count,
[('balance_ref', '=', Decimal('0.0'))])
self.assertRaisesRegex(
UserError,
"Search with 'date' no allowed for field " +
"'balance_ref' on model 'cashbook.book'.",
Book.search,
[], order=[('balance_ref', 'ASC')])
# check sorting - using computed values
books = Book.search([], order=[
('balance_all', 'DESC'),
('name', 'ASC'),
('balance', 'ASC')])
self.assertEqual(len(books), 3)
self.assertEqual(
books[0].rec_name, 'Cashbook 1 | 10.00 usd | Open')
self.assertEqual(
books[1].rec_name, 'Cashbook 3 | 0.00 € | Open')
self.assertEqual(books[1].balance_all, Decimal('6.0'))
self.assertEqual(
books[2].rec_name, 'Cashbook 2 | 5.00 € | Open')
with Transaction().set_context({'date': date(2022, 5, 7)}):
self.assertEqual(
Book.search_count([('balance', '=', Decimal('5.0'))]),
1)
self.assertEqual(
Book.search_count([
('balance', '>=', Decimal('5.0')),
('balance', '<', Decimal('9.0')),
]),
2)
# run workers
self.prep_valstore_run_worker()
# check stored values - no 'date' in context
books = Book.search([], order=[('name', 'ASC')])
self.assertEqual(len(books), 3)
self.assertEqual(books[0].rec_name, 'Cashbook 1 | 10.00 usd | Open')
self.assertEqual(books[1].rec_name, 'Cashbook 2 | 5.00 € | Open')
self.assertEqual(books[2].rec_name, 'Cashbook 3 | 6.00 € | Open')
# check sorting - using stored values
# run most sorters
books = Book.search([], order=[
('balance_all', 'DESC'),
('name', 'ASC'),
('balance', 'ASC'),
('balance_ref', 'ASC')])
self.assertEqual(len(books), 3)
self.assertEqual(books[0].rec_name, 'Cashbook 1 | 10.00 usd | Open')
self.assertEqual(books[1].rec_name, 'Cashbook 3 | 6.00 € | Open')
self.assertEqual(books[2].rec_name, 'Cashbook 2 | 5.00 € | Open')
self.assertEqual(
Book.search_count([('balance', '=', Decimal('0.0'))]),
0)
self.assertEqual(
Book.search_count([('balance', '=', Decimal('5.0'))]),
1)
self.assertEqual(
Book.search_count([('balance', '=', Decimal('6.0'))]),
1)
self.assertEqual(
Book.search_count([
('balance', '>=', Decimal('5.0')),
('balance', '<', Decimal('9.0')),
]),
2)
self.assertEqual(
Book.search_count([('balance_ref', '=', Decimal('6.0'))]),
1)
@with_transaction()
def test_valstore_maintain_values(self):
""" create cashbook, check maintenance -
update records by cron, delete lost records
"""
pool = Pool()
Book = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
tab_book = Book.__table__()
cursor = Transaction().connection.cursor()
types = self.prep_type()
company = self.prep_company()
(usd, euro) = self.prep_2nd_currency(company)
self.assertEqual(company.currency.rec_name, 'Euro')
category = self.prep_category(cattype='in')
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': usd.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': '10 US$',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('10.0'),
'party': party.id,
}, {
'date': date(2022, 5, 10),
'description': '5 US$',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('5.0'),
'party': party.id,
}])],
}])
# clean 'numvalue'
# maintenance_values() will restore it
val1, = ValueStore.search([('field_name', '=', 'balance')])
ValueStore.write(*[[val1], {'numvalue': None}])
self.assertTrue(val1.write_date is not None)
self.assertTrue(val1.create_date is not None)
self.assertEqual(val1.numvalue, None)
# update outdated records
with Transaction().set_context({
'maintenance_date': val1.write_date.date() +
timedelta(days=2)}):
ValueStore.maintenance_values()
val1, = ValueStore.search([('field_name', '=', 'balance')])
self.assertEqual(val1.numvalue, Decimal('15.0'))
# delete book
self.assertEqual(Book.search_count([]), 1)
self.assertEqual(
ValueStore.search_count([]),
len(Book.valuestore_fields()))
query = tab_book.delete(where=tab_book.id == book.id)
cursor.execute(*query)
self.assertEqual(Book.search_count([]), 0)
self.assertEqual(ValueStore.search_count([]), 0)
# end ValuestoreTestCase

View file

@ -1,10 +1,11 @@
[tryton]
version=7.0.0
version=7.0.36
depends:
res
currency
party
company
irrulecontext
xml:
icon.xml
group.xml
@ -21,3 +22,4 @@ xml:
wizard_runreport.xml
wizard_booking.xml
menu.xml
cron.xml

217
valuestore.py Normal file
View file

@ -0,0 +1,217 @@
# -*- 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.
from sql.functions import CurrentTimestamp, DateTrunc
from sql.aggregate import Count
from sql.conditionals import Coalesce
from trytond.model import ModelSQL, fields, Unique, Index
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.pyson import Eval, PYSON, PYSONEncoder, PYSONDecoder
from trytond.model.modelstorage import EvalEnvironment
from trytond.report import Report
class ValueStore(ModelSQL):
'Value Store'
__name__ = 'cashbook.values'
cashbook = fields.Many2One(
string='Cashbook', required=True, model_name='cashbook.book',
ondelete='CASCADE')
field_name = fields.Char(string='Field Name', required=True)
numvalue = fields.Numeric(
string='Value', digits=(16, Eval('valuedigits', 6)),
depends=['valuedigits'])
valuedigits = fields.Function(fields.Integer(
string='Digits', readonly=True),
'on_change_with_valuedigits')
@classmethod
def __register__(cls, module_name):
super(ValueStore, cls).__register__(module_name)
# clear value-store, re-calc
records = cls.search([])
if records:
cls.delete(records)
cls.maintenance_values()
@classmethod
def __setup__(cls):
super(ValueStore, cls).__setup__()
t = cls.__table__()
cls._sql_constraints.extend([
('uniqu_field',
Unique(t, t.cashbook, t.field_name),
'cashbook.msg_value_exists_in_store'),
])
cls._sql_indexes.update({
Index(
t,
(t.field_name, Index.Equality())),
})
def get_rec_name(self, name):
""" name, balance, state
"""
return '|'.join([
'[' + getattr(self.cashbook, 'rec_name', '-') + ']',
self.field_name or '-',
Report.format_number(
self.numvalue,
None,
digits=self.valuedigits or 2)
if self.numvalue is not None else '-',
str(self.valuedigits) if self.valuedigits is not None else '-'])
@fields.depends('cashbook', 'field_name')
def on_change_with_valuedigits(self, name=None):
""" get digits by field name
"""
Cashbook = Pool().get('cashbook.book')
if self.cashbook and self.field_name:
fieldobj = Cashbook._fields.get(self.field_name, None)
if fieldobj:
digit = getattr(fieldobj, 'digits', (16, 6))[1]
if isinstance(digit, PYSON):
# execute pyson on linked record
digit = PYSONDecoder(
EvalEnvironment(self.cashbook, Cashbook)
).decode(PYSONEncoder().encode(digit))
return digit
return 6
@classmethod
def _maintenance_fields(cls):
""" list of model and fieldnames,
to daily update
"""
return ['balance']
@classmethod
def maintenance_values(cls):
""" update values by cron
"""
pool = Pool()
Cashbook = pool.get('cashbook.book')
tab_val = cls.__table__()
tab_book = Cashbook.__table__()
cursor = Transaction().connection.cursor()
context = Transaction().context
# select records older than 'check_dt'
check_dt = context.get('maintenance_date', None)
if not check_dt:
check_dt = DateTrunc('day', CurrentTimestamp())
# select records to update
query = tab_val.select(
tab_val.id,
where=tab_val.field_name.in_(cls._maintenance_fields()) &
(DateTrunc('day', Coalesce(
tab_val.write_date,
tab_val.create_date,
'1990-01-01 00:00:00')) < check_dt))
cursor.execute(*query)
records = []
for x in cursor.fetchall():
cb = cls(x[0]).cashbook
if cb not in records:
records.append(cb)
# add records with missing fields in value-store
num_fields = len(Cashbook.valuestore_fields())
query = tab_book.join(
tab_val,
condition=tab_book.id == tab_val.cashbook,
type_='LEFT OUTER',
).select(
tab_book.id,
Count(tab_val.id).as_('num'),
group_by=[tab_book.id],
having=Count(tab_val.id) < num_fields)
cursor.execute(*query)
records.extend([Cashbook(x[0]) for x in cursor.fetchall()])
if records:
Cashbook.valuestore_update_records([x for x in records])
@classmethod
def get_book_by_line(cls, records):
""" select books above current record to update
records: cashbook.line
"""
Book = Pool().get('cashbook.book')
to_update = []
if records:
books = Book.search([
('parent', 'parent_of', [x.cashbook.id for x in records])
])
to_update.extend([
x for x in books
if x not in to_update])
return to_update
@classmethod
def get_book_by_books(cls, records):
""" select books above current record to update
records: cashbook.book
"""
Book = Pool().get('cashbook.book')
to_update = []
if records:
books = Book.search([
('parent', 'parent_of', [x.id for x in records])
])
to_update.extend([
x for x in books
if x not in to_update])
return to_update
@classmethod
def update_books(cls, books):
""" get cashbooks to update, queue it
"""
Book = Pool().get('cashbook.book')
if books:
Book.__queue__.valuestore_update_records(books)
@classmethod
def update_values(cls, data):
""" data: {'fieldname': {id1: value, id2: value, ...}, ...}
"""
to_write = []
to_create = []
for name in data.keys():
for record_id in data[name].keys():
# get existing record
records = cls.search([
('cashbook', '=', record_id),
('field_name', '=', name)])
if records:
for record in records:
to_write.extend([
[record],
{'numvalue': data[name][record_id]}])
else:
to_create.append({
'cashbook': record_id,
'field_name': name,
'numvalue': data[name][record_id]})
if to_create:
cls.create(to_create)
if to_write:
cls.write(*to_write)
# end ValueStore

View file

@ -1 +1 @@
irrulecontext;7.0.1;7.0.999;mds

View file

@ -5,5 +5,7 @@ full copyright notices and license terms. -->
<tree keyword_open="1">
<field name="rec_name" expand="1"/>
<field name="balance" sum="1" symbol="currency"/>
<field name="state"/>
<field name="balance_all" symbol="currency" optional="1"/>
<field name="balance_ref" symbol="company_currency" optional="1"/>
<field name="state" optional="0"/>
</tree>

View file

@ -5,7 +5,9 @@ full copyright notices and license terms. -->
<tree keyword_open="1" tree_state="1">
<field name="name" expand="1"/>
<field name="balance" symbol="currency"/>
<field name="state"/>
<field name="balance_all" symbol="currency" optional="1"/>
<field name="balance_ref" symbol="company_currency" optional="1"/>
<field name="state" optional="0"/>
<field name="parent" tree_invisible="1"/>
<field name="childs" tree_invisible="1"/>
</tree>

View file

@ -28,6 +28,10 @@ this repository contains the full copyright notices and license terms. -->
<label name="book5"/>
<field name="book5"/>
<label name="fixate"/>
<field name="fixate"/>
<newline/>
<separator id="sepcb" colspan="4" string="Cashbook"/>
<label name="catnamelong"/>
<field name="catnamelong"/>

View file

@ -22,6 +22,10 @@ full copyright notices and license terms. -->
<page id="descr" string="Description" col="1">
<field name="description"/>
</page>
<page name="fixate" col="2">
<label name="fixate"/>
<field name="fixate"/>
</page>
</notebook>
<field name="cashbooks"/>
@ -29,4 +33,3 @@ full copyright notices and license terms. -->
<field name="currency"/>
<field name="owner_cashbook"/>
</form>

View file

@ -4,14 +4,14 @@ The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tree>
<field name="cashbook" tree_invisible="1"/>
<field name="number"/>
<field name="number" optional="0"/>
<field name="date"/>
<field name="payee"/>
<field name="category_view"/>
<field name="descr_short" expand="1"/>
<field name="credit" sum="1"/>
<field name="debit" sum="1"/>
<field name="balance"/>
<field name="state"/>
<field name="credit" sum="1" optional="0"/>
<field name="debit" sum="1" optional="0"/>
<field name="balance" optional="0"/>
<field name="state" optional="0"/>
<button name="wfcheck"/>
</tree>

View file

@ -4,13 +4,13 @@ The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tree>
<field name="cashbook" tree_invisible="1"/>
<field name="number"/>
<field name="date"/>
<field name="payee"/>
<field name="category_view"/>
<field name="descr_short" expand="1"/>
<field name="credit" sum="1"/>
<field name="debit" sum="1"/>
<field name="state"/>
<field name="number" optional="0"/>
<field name="date" optional="0"/>
<field name="payee" optional="0"/>
<field name="category_view" optional="0"/>
<field name="descr_short" expand="1" optional="0"/>
<field name="credit" sum="1" optional="0"/>
<field name="debit" sum="1" optional="0"/>
<field name="state" optional="0"/>
<button name="wfrecon"/>
</tree>

View file

@ -4,11 +4,11 @@ The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tree>
<field name="cashbook"/>
<field name="date"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="start_amount"/>
<field name="end_amount"/>
<field name="lines"/>
<field name="state"/>
<field name="date" optional="0"/>
<field name="date_from" optional="0"/>
<field name="date_to" optional="0"/>
<field name="start_amount" optional="0"/>
<field name="end_amount" optional="0"/>
<field name="lines" optional="0"/>
<field name="state" optional="0"/>
</tree>

View file

@ -8,7 +8,7 @@ full copyright notices and license terms. -->
<field name="splittype"/>
<field name="category"/>
<field name="booktransf"/>
<field name="description" expand="1"/>
<field name="amount" sum="1" symbol="currency"/>
<field name="amount_2nd_currency" symbol="currency2nd"/>
<field name="description" expand="1" optional="0"/>
<field name="amount" sum="1" symbol="currency" optional="0"/>
<field name="amount_2nd_currency" symbol="currency2nd" optional="0"/>
</tree>

View file

@ -7,7 +7,7 @@ from trytond.model import ModelView, fields
from trytond.wizard import Wizard, StateView, StateTransition, Button
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.pyson import Eval, Bool, If
from trytond.pyson import Eval, Bool, If, And
from decimal import Decimal
from .line import sel_bookingtype
@ -45,7 +45,9 @@ class EnterBookingStart(ModelView):
depends=['currency_digits', 'bookingtype'],
digits=(16, Eval('currency_digits', 2)), required=True,
domain=[('amount', '>=', Decimal('0.0'))])
description = fields.Text(string='Description')
description = fields.Text(
string='Description', states={'required': Bool(Eval('fixate'))},
depends=['fixate'])
category = fields.Many2One(
string='Category',
model_name='cashbook.category', depends=['bookingtype'],
@ -60,6 +62,8 @@ class EnterBookingStart(ModelView):
('cattype', '=', 'in'),
('cattype', '=', 'out'),
)])
fixate = fields.Boolean(
string='Fixate', help='The booking is fixed immediately.')
# party or cashbook as counterpart
booktransf = fields.Many2One(
@ -77,7 +81,10 @@ class EnterBookingStart(ModelView):
string='Party', model_name='party.party',
states={
'invisible': ~Eval('bookingtype', '').in_(['in', 'out']),
}, depends=['bookingtype'])
'required': And(
Bool(Eval('fixate')),
Eval('bookingtype', '').in_(['in', 'out']))},
depends=['bookingtype', 'fixate'])
@fields.depends('bookingtype', 'category')
def on_change_bookingtype(self):
@ -150,6 +157,8 @@ class EnterBookingWizard(Wizard):
book_ids.append(getattr(cfg1, x, None).id)
result = {
'fixate': cfg1.fixate
if cfg1 and cfg1.fixate is not None else False,
'cashbooks': [x.id for x in Cashbook.search([
('state', '=', 'open'),
('btype', '!=', None),
@ -187,7 +196,9 @@ class EnterBookingWizard(Wizard):
elif self.start.bookingtype in ['mvin', 'mvout']:
query['booktransf'] = self.start.booktransf.id
Line.create([query])
lines = Line.create([query])
if self.start.fixate:
Line.wfcheck(lines)
return 'end'
def transition_savenext_(self):