@ -284,6 +284,206 @@ class MisReport(models.Model):
# TODO: kpi name cannot be start with query name
# TODO: kpi name cannot be start with query name
@api.multi
def _prepare_aep ( self , root_account ) :
self . ensure_one ( )
aep = AEP ( self . env )
for kpi in self . kpi_ids :
aep . parse_expr ( kpi . expression )
aep . done_parsing ( root_account )
return aep
@api.multi
def _fetch_queries ( self , date_from , date_to ,
get_additional_query_filter = None ) :
self . ensure_one ( )
res = { }
for query in self . query_ids :
model = self . env [ query . model_id . model ]
eval_context = {
' env ' : self . env ,
' time ' : time ,
' datetime ' : datetime ,
' dateutil ' : dateutil ,
# deprecated
' uid ' : self . env . uid ,
' context ' : self . env . context ,
}
domain = query . domain and \
safe_eval ( query . domain , eval_context ) or [ ]
if get_additional_query_filter :
domain . extend ( get_additional_query_filter ( query ) )
if query . date_field . ttype == ' date ' :
domain . extend ( [ ( query . date_field . name , ' >= ' , date_from ) ,
( query . date_field . name , ' <= ' , date_to ) ] )
else :
datetime_from = _utc_midnight (
date_from , self . _context . get ( ' tz ' , ' UTC ' ) )
datetime_to = _utc_midnight (
date_to , self . _context . get ( ' tz ' , ' UTC ' ) , add_day = 1 )
domain . extend ( [ ( query . date_field . name , ' >= ' , datetime_from ) ,
( query . date_field . name , ' < ' , datetime_to ) ] )
field_names = [ f . name for f in query . field_ids ]
if not query . aggregate :
data = model . search_read ( domain , field_names )
res [ query . name ] = [ AutoStruct ( * * d ) for d in data ]
elif query . aggregate == ' sum ' :
data = model . read_group (
domain , field_names , [ ] )
s = AutoStruct ( count = data [ 0 ] [ ' __count ' ] )
for field_name in field_names :
v = data [ 0 ] [ field_name ]
setattr ( s , field_name , v )
res [ query . name ] = s
else :
data = model . search_read ( domain , field_names )
s = AutoStruct ( count = len ( data ) )
if query . aggregate == ' min ' :
agg = _min
elif query . aggregate == ' max ' :
agg = _max
elif query . aggregate == ' avg ' :
agg = _avg
for field_name in field_names :
setattr ( s , field_name ,
agg ( [ d [ field_name ] for d in data ] ) )
res [ query . name ] = s
return res
@api.multi
def _compute ( self , lang_id , aep ,
date_from , date_to ,
period_from , period_to ,
target_move ,
get_additional_move_line_filter = None ,
get_additional_query_filter = None ,
period_id = None ) :
""" Evaluate a report for a given period.
It returns a dictionary keyed on kpi . name with the following values :
* val : the evaluated kpi , or None if there is no data or an error
* val_r : the rendered kpi as a string , or #ERR, #DIV
* val_c : a comment ( explaining the error , typically )
* style : the css style of the kpi
( may change in the future ! )
* prefix : a prefix to display in front of the rendered value
* suffix : a prefix to display after rendered value
* dp : the decimal precision of the kpi
* is_percentage : true if the kpi is of percentage type
( may change in the future ! )
* expr : the kpi expression
* drilldown : true if the drilldown method of
mis . report . instance . period is going to do something
useful in this kpi
: param lang_id : id of a res . lang object
: param aep : an AccountingExpressionProcessor instance created
using _prepare_aep ( )
: param date_from , date_to : the starting and ending date
: param period_from , period_to : the starting and ending accounting
period ( optional , if present must
match date_from , date_to )
: param target_move : all | posted
: param get_additional_move_line_filter : a bound method that takes
no arguments and returns
a domain compatible with
account . move . line
: param get_additional_query_filter : a bound method that takes a single
query argument and returns a
domain compatible with the query
underlying model
: param period_id : an optional opaque value that is returned as
query_id field in the result ( may change in the
future ! )
"""
self . ensure_one ( )
res = { }
localdict = {
' registry ' : self . pool ,
' sum ' : _sum ,
' min ' : _min ,
' max ' : _max ,
' len ' : len ,
' avg ' : _avg ,
' AccountingNone ' : AccountingNone ,
}
localdict . update ( self . _fetch_queries (
date_from , date_to , get_additional_query_filter ) )
if get_additional_move_line_filter :
additional_move_line_filter = get_additional_move_line_filter ( )
aep . do_queries ( date_from , date_to ,
period_from , period_to ,
target_move ,
additional_move_line_filter )
compute_queue = self . kpi_ids
recompute_queue = [ ]
while True :
for kpi in compute_queue :
try :
kpi_val_comment = kpi . name + " = " + kpi . expression
kpi_eval_expression = aep . replace_expr ( kpi . expression )
kpi_val = safe_eval ( kpi_eval_expression , localdict )
localdict [ kpi . name ] = kpi_val
except ZeroDivisionError :
kpi_val = None
kpi_val_rendered = ' #DIV/0 '
kpi_val_comment + = ' \n \n %s ' % ( traceback . format_exc ( ) , )
except ( NameError , ValueError ) :
recompute_queue . append ( kpi )
kpi_val = None
kpi_val_rendered = ' #ERR '
kpi_val_comment + = ' \n \n %s ' % ( traceback . format_exc ( ) , )
except :
kpi_val = None
kpi_val_rendered = ' #ERR '
kpi_val_comment + = ' \n \n %s ' % ( traceback . format_exc ( ) , )
else :
kpi_val_rendered = kpi . render ( lang_id , kpi_val )
try :
kpi_style = None
if kpi . css_style :
kpi_style = safe_eval ( kpi . css_style , localdict )
except :
_logger . warning ( " error evaluating css stype expression %s " ,
kpi . css_style , exc_info = True )
kpi_style = None
drilldown = ( kpi_val is not None and
AEP . has_account_var ( kpi . expression ) )
res [ kpi . name ] = {
' val ' : None if kpi_val is AccountingNone else kpi_val ,
' val_r ' : kpi_val_rendered ,
' val_c ' : kpi_val_comment ,
' style ' : kpi_style ,
' prefix ' : kpi . prefix ,
' suffix ' : kpi . suffix ,
' dp ' : kpi . dp ,
' is_percentage ' : kpi . type == ' pct ' ,
' period_id ' : period_id ,
' expr ' : kpi . expression ,
' drilldown ' : drilldown ,
}
if len ( recompute_queue ) == 0 :
# nothing to recompute, we are done
break
if len ( recompute_queue ) == len ( compute_queue ) :
# could not compute anything in this iteration
# (ie real Value errors or cyclic dependency)
# so we stop trying
break
# try again
compute_queue = recompute_queue
recompute_queue = [ ]
return res
class MisReportInstancePeriod ( models . Model ) :
class MisReportInstancePeriod ( models . Model ) :
""" A MIS report instance has the logic to compute
""" A MIS report instance has the logic to compute
@ -424,7 +624,7 @@ class MisReportInstancePeriod(models.Model):
@api.multi
@api.multi
def drilldown ( self , expr ) :
def drilldown ( self , expr ) :
assert len ( self ) == 1
self . ensure_one ( )
if AEP . has_account_var ( expr ) :
if AEP . has_account_var ( expr ) :
aep = AEP ( self . env )
aep = AEP ( self . env )
aep . parse_expr ( expr )
aep . parse_expr ( expr )
@ -448,144 +648,18 @@ class MisReportInstancePeriod(models.Model):
else :
else :
return False
return False
def _fetch_queries ( self ) :
assert len ( self ) == 1
res = { }
for query in self . report_instance_id . report_id . query_ids :
model = self . env [ query . model_id . model ]
eval_context = {
' env ' : self . env ,
' time ' : time ,
' datetime ' : datetime ,
' dateutil ' : dateutil ,
# deprecated
' uid ' : self . env . uid ,
' context ' : self . env . context ,
}
domain = query . domain and \
safe_eval ( query . domain , eval_context ) or [ ]
domain . extend ( self . _get_additional_query_filter ( query ) )
if query . date_field . ttype == ' date ' :
domain . extend ( [ ( query . date_field . name , ' >= ' , self . date_from ) ,
( query . date_field . name , ' <= ' , self . date_to ) ] )
else :
datetime_from = _utc_midnight (
self . date_from , self . _context . get ( ' tz ' , ' UTC ' ) )
datetime_to = _utc_midnight (
self . date_to , self . _context . get ( ' tz ' , ' UTC ' ) , add_day = 1 )
domain . extend ( [ ( query . date_field . name , ' >= ' , datetime_from ) ,
( query . date_field . name , ' < ' , datetime_to ) ] )
field_names = [ f . name for f in query . field_ids ]
if not query . aggregate :
data = model . search_read ( domain , field_names )
res [ query . name ] = [ AutoStruct ( * * d ) for d in data ]
elif query . aggregate == ' sum ' :
data = model . read_group (
domain , field_names , [ ] )
s = AutoStruct ( count = data [ 0 ] [ ' __count ' ] )
for field_name in field_names :
v = data [ 0 ] [ field_name ]
setattr ( s , field_name , v )
res [ query . name ] = s
else :
data = model . search_read ( domain , field_names )
s = AutoStruct ( count = len ( data ) )
if query . aggregate == ' min ' :
agg = _min
elif query . aggregate == ' max ' :
agg = _max
elif query . aggregate == ' avg ' :
agg = _avg
for field_name in field_names :
setattr ( s , field_name ,
agg ( [ d [ field_name ] for d in data ] ) )
res [ query . name ] = s
return res
@api.multi
def _compute ( self , lang_id , aep ) :
def _compute ( self , lang_id , aep ) :
res = { }
localdict = {
' registry ' : self . pool ,
' sum ' : _sum ,
' min ' : _min ,
' max ' : _max ,
' len ' : len ,
' avg ' : _avg ,
' AccountingNone ' : AccountingNone ,
}
localdict . update ( self . _fetch_queries ( ) )
aep . do_queries ( self . date_from , self . date_to ,
self . ensure_one ( )
return self . report_instance_id . report_id . _compute (
lang_id , aep ,
self . date_from , self . date_to ,
self . period_from , self . period_to ,
self . period_from , self . period_to ,
self . report_instance_id . target_move ,
self . report_instance_id . target_move ,
self . _get_additional_move_line_filter ( ) )
compute_queue = self . report_instance_id . report_id . kpi_ids
recompute_queue = [ ]
while True :
for kpi in compute_queue :
try :
kpi_val_comment = kpi . name + " = " + kpi . expression
kpi_eval_expression = aep . replace_expr ( kpi . expression )
kpi_val = safe_eval ( kpi_eval_expression , localdict )
localdict [ kpi . name ] = kpi_val
except ZeroDivisionError :
kpi_val = None
kpi_val_rendered = ' #DIV/0 '
kpi_val_comment + = ' \n \n %s ' % ( traceback . format_exc ( ) , )
except ( NameError , ValueError ) :
recompute_queue . append ( kpi )
kpi_val = None
kpi_val_rendered = ' #ERR '
kpi_val_comment + = ' \n \n %s ' % ( traceback . format_exc ( ) , )
except :
kpi_val = None
kpi_val_rendered = ' #ERR '
kpi_val_comment + = ' \n \n %s ' % ( traceback . format_exc ( ) , )
else :
kpi_val_rendered = kpi . render ( lang_id , kpi_val )
try :
kpi_style = None
if kpi . css_style :
kpi_style = safe_eval ( kpi . css_style , localdict )
except :
_logger . warning ( " error evaluating css stype expression %s " ,
kpi . css_style , exc_info = True )
kpi_style = None
drilldown = ( kpi_val is not None and
AEP . has_account_var ( kpi . expression ) )
res [ kpi . name ] = {
' val ' : None if kpi_val is AccountingNone else kpi_val ,
' val_r ' : kpi_val_rendered ,
' val_c ' : kpi_val_comment ,
' style ' : kpi_style ,
' prefix ' : kpi . prefix ,
' suffix ' : kpi . suffix ,
' dp ' : kpi . dp ,
' is_percentage ' : kpi . type == ' pct ' ,
' period_id ' : self . id ,
' expr ' : kpi . expression ,
' drilldown ' : drilldown ,
}
if len ( recompute_queue ) == 0 :
# nothing to recompute, we are done
break
if len ( recompute_queue ) == len ( compute_queue ) :
# could not compute anything in this iteration
# (ie real Value errors or cyclic dependency)
# so we stop trying
break
# try again
compute_queue = recompute_queue
recompute_queue = [ ]
return res
self . _get_additional_move_line_filter ,
self . _get_additional_query_filter ,
period_id = self . id ,
)
class MisReportInstance ( models . Model ) :
class MisReportInstance ( models . Model ) :
@ -698,13 +772,9 @@ class MisReportInstance(models.Model):
@api.multi
@api.multi
def compute ( self ) :
def compute ( self ) :
assert len ( self ) == 1
self . ensure_one ( )
# prepare AccountingExpressionProcessor
aep = AEP ( self . env )
for kpi in self . report_id . kpi_ids :
aep . parse_expr ( kpi . expression )
aep . done_parsing ( self . root_account )
aep = self . report_id . _prepare_aep ( self . root_account )
# fetch user language only once
# fetch user language only once
# TODO: is this necessary?
# TODO: is this necessary?