speedup: indexes, caching
This commit is contained in:
parent
86e6c33cc1
commit
624a5bff55
11 changed files with 352 additions and 32 deletions
|
@ -15,11 +15,15 @@ from .configuration import Configuration, UserConfiguration
|
||||||
from .category import Category
|
from .category import Category
|
||||||
from .reconciliation import Reconciliation
|
from .reconciliation import Reconciliation
|
||||||
from .cbreport import ReconciliationReport
|
from .cbreport import ReconciliationReport
|
||||||
|
from .currency import CurrencyRate
|
||||||
|
from .model import MemCache
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
Pool.register(
|
Pool.register(
|
||||||
|
MemCache,
|
||||||
Configuration,
|
Configuration,
|
||||||
UserConfiguration,
|
UserConfiguration,
|
||||||
|
CurrencyRate,
|
||||||
Type,
|
Type,
|
||||||
Category,
|
Category,
|
||||||
Book,
|
Book,
|
||||||
|
|
59
book.py
59
book.py
|
@ -13,9 +13,9 @@ from trytond.report import Report
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from sql.aggregate import Sum
|
from sql.aggregate import Sum
|
||||||
from sql.conditionals import Case, Coalesce
|
from sql.conditionals import Case
|
||||||
from sql.functions import CurrentDate
|
from .model import order_name_hierarchical, sub_ids_hierarchical, \
|
||||||
from .model import order_name_hierarchical, sub_ids_hierarchical, AnyInArray
|
AnyInArray, CACHEKEY_CURRENCY
|
||||||
|
|
||||||
|
|
||||||
STATES = {
|
STATES = {
|
||||||
|
@ -48,12 +48,12 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
|
||||||
__name__ = 'cashbook.book'
|
__name__ = 'cashbook.book'
|
||||||
|
|
||||||
company = fields.Many2One(string='Company', model_name='company.company',
|
company = fields.Many2One(string='Company', model_name='company.company',
|
||||||
required=True, ondelete="RESTRICT")
|
required=True, select=True, ondelete="RESTRICT")
|
||||||
name = fields.Char(string='Name', required=True,
|
name = fields.Char(string='Name', required=True,
|
||||||
states=STATES, depends=DEPENDS)
|
states=STATES, depends=DEPENDS)
|
||||||
description = fields.Text(string='Description',
|
description = fields.Text(string='Description',
|
||||||
states=STATES, depends=DEPENDS)
|
states=STATES, depends=DEPENDS)
|
||||||
btype = fields.Many2One(string='Type',
|
btype = fields.Many2One(string='Type', select=True,
|
||||||
help='A cash book with type can contain postings. Without type is a view.',
|
help='A cash book with type can contain postings. Without type is a view.',
|
||||||
model_name='cashbook.type', ondelete='RESTRICT',
|
model_name='cashbook.type', ondelete='RESTRICT',
|
||||||
states={
|
states={
|
||||||
|
@ -346,12 +346,15 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
|
||||||
Currency = pool.get('currency.currency')
|
Currency = pool.get('currency.currency')
|
||||||
Company = pool.get('company.company')
|
Company = pool.get('company.company')
|
||||||
IrDate = pool.get('ir.date')
|
IrDate = pool.get('ir.date')
|
||||||
|
MemCache = pool.get('cashbook.memcache')
|
||||||
tab_book = Book2.__table__()
|
tab_book = Book2.__table__()
|
||||||
tab_comp = Company.__table__()
|
tab_comp = Company.__table__()
|
||||||
cursor = Transaction().connection.cursor()
|
cursor = Transaction().connection.cursor()
|
||||||
context = Transaction().context
|
context = Transaction().context
|
||||||
|
|
||||||
result = {x:{y.id: Decimal('0.0') for y in cashbooks} for x in names}
|
result = {
|
||||||
|
x:{y.id: Decimal('0.0') for y in cashbooks}
|
||||||
|
for x in ['balance', 'balance_all', 'balance_ref']}
|
||||||
|
|
||||||
# deny invalid date in context
|
# deny invalid date in context
|
||||||
query_date = context.get('date', IrDate.today())
|
query_date = context.get('date', IrDate.today())
|
||||||
|
@ -361,6 +364,28 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
|
||||||
except :
|
except :
|
||||||
query_date = IrDate.today()
|
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': 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
|
# query balances of cashbooks and sub-cashbooks
|
||||||
with Transaction().set_context({
|
with Transaction().set_context({
|
||||||
'date': query_date,
|
'date': query_date,
|
||||||
|
@ -381,26 +406,20 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
|
||||||
Sum(tab_line.balance).as_('balance'),
|
Sum(tab_line.balance).as_('balance'),
|
||||||
Sum(tab_line.balance_all).as_('balance_all'),
|
Sum(tab_line.balance_all).as_('balance_all'),
|
||||||
group_by=[tab_book.id, tab_line.currency, tab_comp.currency],
|
group_by=[tab_book.id, tab_line.currency, tab_comp.currency],
|
||||||
where=tab_book.id.in_([x.id for x in cashbooks]),
|
where=tab_book.id.in_([x.id for x in todo_cashbook]),
|
||||||
)
|
)
|
||||||
cursor.execute(*query)
|
cursor.execute(*query)
|
||||||
records = cursor.fetchall()
|
records = cursor.fetchall()
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
values = {
|
result['balance'][record[0]] += Currency.compute(
|
||||||
'balance': Currency.compute(
|
record[2], record[4], record[1])
|
||||||
record[2], record[4], record[1],
|
result['balance_all'][record[0]] += Currency.compute(
|
||||||
),
|
record[2], record[5], record[1])
|
||||||
'balance_all': Currency.compute(
|
result['balance_ref'][record[0]] += Currency.compute(
|
||||||
record[2], record[5], record[1],
|
record[2], record[5], record[3])
|
||||||
),
|
|
||||||
'balance_ref': Currency.compute(
|
|
||||||
record[2], record[5], record[3],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
for name in names:
|
MemCache.store_result(cashbooks, cache_keys, result)
|
||||||
result[name][record[0]] += values[name]
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@fields.depends('btype')
|
@fields.depends('btype')
|
||||||
|
|
47
currency.py
Normal file
47
currency.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# -*- 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 Pool, PoolMeta
|
||||||
|
from .model import CACHEKEY_CURRENCY
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyRate(metaclass=PoolMeta):
|
||||||
|
__name__ = 'currency.currency.rate'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, vlist):
|
||||||
|
""" update cache-value
|
||||||
|
"""
|
||||||
|
MemCache = Pool().get('cashbook.memcache')
|
||||||
|
|
||||||
|
records = super(CurrencyRate, cls).create(vlist)
|
||||||
|
for rate in records:
|
||||||
|
MemCache.record_update(CACHEKEY_CURRENCY % rate.currency.id, rate)
|
||||||
|
return records
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write(cls, *args):
|
||||||
|
""" update cache-value
|
||||||
|
"""
|
||||||
|
MemCache = Pool().get('cashbook.memcache')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete(cls, records):
|
||||||
|
""" set cache to None
|
||||||
|
"""
|
||||||
|
MemCache = Pool().get('cashbook.memcache')
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
MemCache.record_update(CACHEKEY_CURRENCY % record.currency.id, None)
|
||||||
|
super(CurrencyRate, cls).delete(records)
|
||||||
|
|
||||||
|
# end
|
8
line.py
8
line.py
|
@ -15,7 +15,7 @@ from sql import Literal
|
||||||
from sql.functions import DatePart
|
from sql.functions import DatePart
|
||||||
from sql.conditionals import Case
|
from sql.conditionals import Case
|
||||||
from .book import sel_state_book
|
from .book import sel_state_book
|
||||||
from .mixin import SecondCurrencyMixin
|
from .mixin import SecondCurrencyMixin, MemCacheIndexMx
|
||||||
|
|
||||||
|
|
||||||
sel_payee = [
|
sel_payee = [
|
||||||
|
@ -48,7 +48,7 @@ STATES = {
|
||||||
DEPENDS=['state', 'state_cashbook']
|
DEPENDS=['state', 'state_cashbook']
|
||||||
|
|
||||||
|
|
||||||
class Line(SecondCurrencyMixin, Workflow, ModelSQL, ModelView):
|
class Line(SecondCurrencyMixin, MemCacheIndexMx, Workflow, ModelSQL, ModelView):
|
||||||
'Cashbook Line'
|
'Cashbook Line'
|
||||||
__name__ = 'cashbook.line'
|
__name__ = 'cashbook.line'
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class Line(SecondCurrencyMixin, Workflow, ModelSQL, ModelView):
|
||||||
states=STATES, depends=DEPENDS)
|
states=STATES, depends=DEPENDS)
|
||||||
descr_short = fields.Function(fields.Char(string='Description', readonly=True),
|
descr_short = fields.Function(fields.Char(string='Description', readonly=True),
|
||||||
'on_change_with_descr_short', searcher='search_descr_short')
|
'on_change_with_descr_short', searcher='search_descr_short')
|
||||||
category = fields.Many2One(string='Category',
|
category = fields.Many2One(string='Category', select=True,
|
||||||
model_name='cashbook.category', ondelete='RESTRICT',
|
model_name='cashbook.category', ondelete='RESTRICT',
|
||||||
states={
|
states={
|
||||||
'readonly': Or(
|
'readonly': Or(
|
||||||
|
@ -88,7 +88,7 @@ class Line(SecondCurrencyMixin, Workflow, ModelSQL, ModelView):
|
||||||
states={'invisible': True}), 'on_change_with_booktransf_feature')
|
states={'invisible': True}), 'on_change_with_booktransf_feature')
|
||||||
|
|
||||||
bookingtype = fields.Selection(string='Type', required=True,
|
bookingtype = fields.Selection(string='Type', required=True,
|
||||||
help='Type of Booking', selection=sel_bookingtype,
|
help='Type of Booking', selection=sel_bookingtype, select=True,
|
||||||
states=STATES, depends=DEPENDS)
|
states=STATES, depends=DEPENDS)
|
||||||
bookingtype_string = bookingtype.translated('bookingtype')
|
bookingtype_string = bookingtype.translated('bookingtype')
|
||||||
amount = fields.Numeric(string='Amount', digits=(16, Eval('currency_digits', 2)),
|
amount = fields.Numeric(string='Amount', digits=(16, Eval('currency_digits', 2)),
|
||||||
|
|
16
mixin.py
16
mixin.py
|
@ -10,6 +10,7 @@ from trytond.modules.currency.ir import rate_decimal
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
STATES = {
|
STATES = {
|
||||||
'readonly': Or(
|
'readonly': Or(
|
||||||
Eval('state', '') != 'edit',
|
Eval('state', '') != 'edit',
|
||||||
|
@ -187,3 +188,18 @@ class SecondCurrencyMixin:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
# end SecondCurrencyMixin
|
# end SecondCurrencyMixin
|
||||||
|
|
||||||
|
|
||||||
|
class MemCacheIndexMx:
|
||||||
|
""" add index to 'create_date' + 'write_date'
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def __setup__(cls):
|
||||||
|
super(MemCacheIndexMx, cls).__setup__()
|
||||||
|
# add index
|
||||||
|
cls.write_date.select = True
|
||||||
|
cls.create_date.select = True
|
||||||
|
|
||||||
|
# end MemCacheIndexMx
|
||||||
|
|
||||||
|
|
||||||
|
|
152
model.py
152
model.py
|
@ -3,11 +3,18 @@
|
||||||
# The COPYRIGHT file at the top level of this repository contains the
|
# The COPYRIGHT file at the top level of this repository contains the
|
||||||
# full copyright notices and license terms.
|
# full copyright notices and license terms.
|
||||||
|
|
||||||
from trytond.model import MultiValueMixin, ValueMixin, fields, Unique
|
from trytond.model import MultiValueMixin, ValueMixin, fields, Unique, Model
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from trytond.pool import Pool
|
from trytond.pool import Pool
|
||||||
|
from trytond.cache import MemoryCache
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
from decimal import Decimal
|
||||||
from sql import With, Literal
|
from sql import With, Literal
|
||||||
from sql.functions import Function
|
from sql.functions import Function
|
||||||
|
from sql.conditionals import Coalesce
|
||||||
|
|
||||||
|
ENABLE_CACHE = True
|
||||||
|
CACHEKEY_CURRENCY = 'currency-%s'
|
||||||
|
|
||||||
|
|
||||||
class ArrayAgg(Function):
|
class ArrayAgg(Function):
|
||||||
|
@ -62,6 +69,149 @@ class Array(Function):
|
||||||
# end Array
|
# 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 == False:
|
||||||
|
return None
|
||||||
|
return cls._cashbook_value_cache.get(cache_key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def store_result(cls, records, cache_keys, values):
|
||||||
|
""" store result to cache
|
||||||
|
"""
|
||||||
|
if ENABLE_CACHE == False:
|
||||||
|
return
|
||||||
|
for record in records:
|
||||||
|
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], data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def store_value(cls, cache_key, values):
|
||||||
|
""" store values to cache
|
||||||
|
"""
|
||||||
|
if ENABLE_CACHE == False:
|
||||||
|
return
|
||||||
|
cls._cashbook_value_cache.set(cache_key, values)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_from_cache(cls, records, cache_keys, names, result):
|
||||||
|
""" get stored values from memcache
|
||||||
|
"""
|
||||||
|
if ENABLE_CACHE == False:
|
||||||
|
return (records, result)
|
||||||
|
|
||||||
|
todo_records = []
|
||||||
|
for record in records:
|
||||||
|
values = 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 == False:
|
||||||
|
return '-'
|
||||||
|
|
||||||
|
dt1 = datetime.now()
|
||||||
|
print('-- get_key_by_record:', name, record.id)
|
||||||
|
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])
|
||||||
|
|
||||||
|
print('-- get_key_by_record-END:', datetime.now() - dt1, '-'.join(fname))
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
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):
|
def sub_ids_hierarchical(model_name):
|
||||||
""" get table with id and sub-ids
|
""" get table with id and sub-ids
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -52,7 +52,7 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
|
||||||
],
|
],
|
||||||
states=STATES, depends=DEPENDS+['date_to'])
|
states=STATES, depends=DEPENDS+['date_to'])
|
||||||
date_to = fields.Date(string='End Date',
|
date_to = fields.Date(string='End Date',
|
||||||
required=True,
|
required=True, select=True,
|
||||||
domain=[
|
domain=[
|
||||||
If(Eval('date_to') & Eval('date_from'),
|
If(Eval('date_to') & Eval('date_from'),
|
||||||
('date_from', '<=', Eval('date_to')),
|
('date_from', '<=', Eval('date_to')),
|
||||||
|
|
10
splitline.py
10
splitline.py
|
@ -12,7 +12,7 @@ from trytond.i18n import gettext
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from .line import sel_linetype, sel_bookingtype, STATES, DEPENDS
|
from .line import sel_linetype, sel_bookingtype, STATES, DEPENDS
|
||||||
from .book import sel_state_book
|
from .book import sel_state_book
|
||||||
from .mixin import SecondCurrencyMixin
|
from .mixin import SecondCurrencyMixin, MemCacheIndexMx
|
||||||
|
|
||||||
|
|
||||||
sel_linetype = [
|
sel_linetype = [
|
||||||
|
@ -26,7 +26,7 @@ sel_target = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SplitLine(SecondCurrencyMixin, ModelSQL, ModelView):
|
class SplitLine(SecondCurrencyMixin, MemCacheIndexMx, ModelSQL, ModelView):
|
||||||
'Split booking line'
|
'Split booking line'
|
||||||
__name__ = 'cashbook.split'
|
__name__ = 'cashbook.split'
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ class SplitLine(SecondCurrencyMixin, ModelSQL, ModelView):
|
||||||
states=STATES, depends=DEPENDS)
|
states=STATES, depends=DEPENDS)
|
||||||
splittype = fields.Selection(string='Type', required=True,
|
splittype = fields.Selection(string='Type', required=True,
|
||||||
help='Type of split booking line', selection=sel_linetype,
|
help='Type of split booking line', selection=sel_linetype,
|
||||||
states=STATES, depends=DEPENDS)
|
states=STATES, depends=DEPENDS, select=True)
|
||||||
category = fields.Many2One(string='Category',
|
category = fields.Many2One(string='Category', select=True,
|
||||||
model_name='cashbook.category', ondelete='RESTRICT',
|
model_name='cashbook.category', ondelete='RESTRICT',
|
||||||
states={
|
states={
|
||||||
'readonly': STATES['readonly'],
|
'readonly': STATES['readonly'],
|
||||||
|
@ -59,7 +59,7 @@ class SplitLine(SecondCurrencyMixin, ModelSQL, ModelView):
|
||||||
('owner.id', '=', Eval('owner_cashbook', -1)),
|
('owner.id', '=', Eval('owner_cashbook', -1)),
|
||||||
('id', '!=', Eval('cashbook', -1)),
|
('id', '!=', Eval('cashbook', -1)),
|
||||||
('btype', '!=', None),
|
('btype', '!=', None),
|
||||||
],
|
], select=True,
|
||||||
states={
|
states={
|
||||||
'readonly': STATES['readonly'],
|
'readonly': STATES['readonly'],
|
||||||
'invisible': Eval('splittype', '') != 'tr',
|
'invisible': Eval('splittype', '') != 'tr',
|
||||||
|
|
|
@ -12,12 +12,14 @@ from trytond.modules.cashbook.tests.test_config import ConfigTestCase
|
||||||
from trytond.modules.cashbook.tests.test_category import CategoryTestCase
|
from trytond.modules.cashbook.tests.test_category import CategoryTestCase
|
||||||
from trytond.modules.cashbook.tests.test_reconciliation import ReconTestCase
|
from trytond.modules.cashbook.tests.test_reconciliation import ReconTestCase
|
||||||
from trytond.modules.cashbook.tests.test_bookingwiz import BookingWizardTestCase
|
from trytond.modules.cashbook.tests.test_bookingwiz import BookingWizardTestCase
|
||||||
|
from trytond.modules.cashbook.tests.test_currency import CurrencyTestCase
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['suite']
|
__all__ = ['suite']
|
||||||
|
|
||||||
|
|
||||||
class CashbookTestCase(\
|
class CashbookTestCase(\
|
||||||
|
CurrencyTestCase, \
|
||||||
BookingWizardTestCase,\
|
BookingWizardTestCase,\
|
||||||
ReconTestCase,\
|
ReconTestCase,\
|
||||||
CategoryTestCase,\
|
CategoryTestCase,\
|
||||||
|
|
82
tests/test_currency.py
Normal file
82
tests/test_currency.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# -*- 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 ModuleTestCase, with_transaction
|
||||||
|
from trytond.pool import Pool
|
||||||
|
from trytond.transaction import Transaction
|
||||||
|
from trytond.modules.cashbook.model import CACHEKEY_CURRENCY
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyTestCase(ModuleTestCase):
|
||||||
|
'Test cache for currency'
|
||||||
|
module = 'cashbook'
|
||||||
|
|
||||||
|
@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()))
|
||||||
|
self.assertEqual(MemCache.read_value(cache_key), value)
|
||||||
|
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()))
|
||||||
|
self.assertEqual(MemCache.read_value(cache_key), value)
|
||||||
|
|
||||||
|
Currency.write(*[
|
||||||
|
[currency],
|
||||||
|
{
|
||||||
|
'rates': [('delete', [currency.rates[0].id])],
|
||||||
|
}])
|
||||||
|
self.assertEqual(MemCache.read_value(cache_key), None)
|
||||||
|
|
||||||
|
# end CurrencyTestCase
|
2
types.py
2
types.py
|
@ -17,7 +17,7 @@ class Type(ModelSQL, ModelView):
|
||||||
company = fields.Many2One(string='Company', model_name='company.company',
|
company = fields.Many2One(string='Company', model_name='company.company',
|
||||||
required=True, ondelete="RESTRICT")
|
required=True, ondelete="RESTRICT")
|
||||||
feature = fields.Selection(string='Feature', required=True,
|
feature = fields.Selection(string='Feature', required=True,
|
||||||
selection='get_sel_feature',
|
selection='get_sel_feature', select=True,
|
||||||
help='Select feature set of the Cashbook.')
|
help='Select feature set of the Cashbook.')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
Loading…
Reference in a new issue