diff --git a/.hgignore b/.gitignore similarity index 87% rename from .hgignore rename to .gitignore index 56406cb..f5d0da5 100644 --- a/.hgignore +++ b/.gitignore @@ -1,7 +1,8 @@ -syntax: glob +*.pyc build/* dist/* mds_cashbook.egg-info/* locale/convert_de2en.py __pycache__/* .pytest_cache/* +.env diff --git a/README.rst b/README.rst index 65a79f1..89efe3c 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/__init__.py b/__init__.py index 80f7161..7be31cb 100644 --- a/__init__.py +++ b/__init__.py @@ -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, diff --git a/book.py b/book.py index 872f54a..2b4eb39 100644 --- a/book.py +++ b/book.py @@ -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 = { @@ -73,9 +62,9 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView): model_name='cashbook.type', ondelete='RESTRICT', states={ 'readonly': Or( - STATES['readonly'], - Len(Eval('lines')) > 0), - }, depends=DEPENDS+['lines']) + STATES['readonly'], + 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 = tab_line.select( - tab_line.cashbook, - where=Operator( - getattr(tab_line, name), clause[2])) - return [('id', 'in', query)] + 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): diff --git a/configuration.py b/configuration.py index 2f2f5db..5f3fdf3 100644 --- a/configuration.py +++ b/configuration.py @@ -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 diff --git a/cron.py b/cron.py new file mode 100644 index 0000000..36020ae --- /dev/null +++ b/cron.py @@ -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 diff --git a/cron.xml b/cron.xml new file mode 100644 index 0000000..f1bd33e --- /dev/null +++ b/cron.xml @@ -0,0 +1,15 @@ + + + + + + + cashbook.values|maintenance_values + + hours + + + + diff --git a/currency.py b/currency.py index 817812f..9b4bf3e 100644 --- a/currency.py +++ b/currency.py @@ -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 diff --git a/ir.py b/ir.py index 7aa9aca..0f81aec 100644 --- a/ir.py +++ b/ir.py @@ -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 diff --git a/line.py b/line.py index 45dae05..34e4971 100644 --- a/line.py +++ b/line.py @@ -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 diff --git a/locale/de.po b/locale/de.po index 5d951db..c028703 100644 --- a/locale/de.po +++ b/locale/de.po @@ -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" diff --git a/locale/en.po b/locale/en.po index f3e6784..af4bc7a 100644 --- a/locale/en.po +++ b/locale/en.po @@ -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" + diff --git a/message.xml b/message.xml index ed21703..57a9823 100644 --- a/message.xml +++ b/message.xml @@ -119,6 +119,12 @@ full copyright notices and license terms. --> General + + The value already exists for the record. + + + Search with 'date' no allowed for field '%(fname)s' on model '%(model)s'. + diff --git a/mixin.py b/mixin.py index 363bed5..e261af2 100644 --- a/mixin.py +++ b/mixin.py @@ -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 diff --git a/model.py b/model.py index e7b4ebc..e50adf7 100644 --- a/model.py +++ b/model.py @@ -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 """ diff --git a/splitline.py b/splitline.py index 79e78df..6546992 100644 --- a/splitline.py +++ b/splitline.py @@ -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' diff --git a/tests/book.py b/tests/book.py index 8678173..fa7d028 100644 --- a/tests/book.py +++ b/tests/book.py @@ -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, diff --git a/tests/bookingwiz.py b/tests/bookingwiz.py index 690fcd6..97b99ee 100644 --- a/tests/bookingwiz.py +++ b/tests/bookingwiz.py @@ -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]) diff --git a/tests/config.py b/tests/config.py index a1eb701..f623e7b 100644 --- a/tests/config.py +++ b/tests/config.py @@ -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 diff --git a/tests/currency.py b/tests/currency.py deleted file mode 100644 index 316ddb3..0000000 --- a/tests/currency.py +++ /dev/null @@ -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 diff --git a/tests/line.py b/tests/line.py index 63a6682..2175a11 100644 --- a/tests/line.py +++ b/tests/line.py @@ -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, diff --git a/tests/test_module.py b/tests/test_module.py index 87f9d68..f2f5687 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -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, diff --git a/tests/valuestore.py b/tests/valuestore.py new file mode 100644 index 0000000..ea1a031 --- /dev/null +++ b/tests/valuestore.py @@ -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 diff --git a/tryton.cfg b/tryton.cfg index b522486..4c72909 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -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 diff --git a/valuestore.py b/valuestore.py new file mode 100644 index 0000000..9fc2e88 --- /dev/null +++ b/valuestore.py @@ -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 diff --git a/versiondep.txt b/versiondep.txt index 8b13789..d501ebb 100644 --- a/versiondep.txt +++ b/versiondep.txt @@ -1 +1 @@ - +irrulecontext;7.0.1;7.0.999;mds diff --git a/view/book_list.xml b/view/book_list.xml index 31479c1..5644b9b 100644 --- a/view/book_list.xml +++ b/view/book_list.xml @@ -5,5 +5,7 @@ full copyright notices and license terms. --> - + + + diff --git a/view/book_tree.xml b/view/book_tree.xml index f3cfcb1..da7adef 100644 --- a/view/book_tree.xml +++ b/view/book_tree.xml @@ -5,7 +5,9 @@ full copyright notices and license terms. --> - + + + diff --git a/view/configuration_form.xml b/view/configuration_form.xml index aa870e4..dd55d2c 100644 --- a/view/configuration_form.xml +++ b/view/configuration_form.xml @@ -28,6 +28,10 @@ this repository contains the full copyright notices and license terms. -->