def parse_report_row(self, row, user, total=False): return ReportRow( row[0], # unit_name row[1].lower().strip(), # violation int(row[2]) if row[2] else 1, # violation_count self.duration_cache.get(row[0], 0) if total else parse_timedelta(row[3]).total_seconds(), # duration parse_float(row[4], default=.0), # fine parse_float(row[5], default=.0), # rating local_to_utc_time( parse_wialon_report_datetime( row[6]['t'] if isinstance(row[6], dict) else row[6] ), user.timezone ), # from_dt local_to_utc_time( parse_wialon_report_datetime( row[7]['t'] if isinstance(row[7], dict) else row[7] ), user.timezone ), # to_dt self.mileage_cache.get(row[0], .0) if total else parse_float(row[9], default=.0) # mileage_corrected )
def get_report_data(self): self.get_report_data_tables() self.request_dt_from, self.request_dt_to = get_period( self.input_data['date_begin'], self.input_data['date_end']) cleanup_and_request_report(self.request.user, self.report_template_id, self.sess_id, item_id=self.unit_id) try: r = exec_report(self.request.user, self.report_template_id, self.sess_id, self.request_dt_from, self.request_dt_to, object_id=self.unit_id) except ReportException as e: raise WialonException( 'Не удалось получить в Wialon отчет о поездках: %s' % e) for table_index, table_info in enumerate(r['reportResult']['tables']): if table_info['name'] not in self.report_data: continue try: rows = get_report_rows(self.sess_id, table_index, table_info['rows'], level=1) if table_info['name'] != 'unit_sensors_tracing': self.report_data[table_info['name']] = rows else: for row in rows: if isinstance(row['c'][0], dict): key = row['c'][0]['v'] else: key = parse_wialon_report_datetime(row['c'][0]) key = int( local_to_utc_time( key, self.request.user.timezone).timestamp()) if key and row['c'][1]: self.fuel_data[key] = parse_float( row['c'][1]) or .0 except ReportException as e: raise WialonException( 'Не удалось получить в Wialon отчет о поездках: %s' % e)
def report_post_processing(self, unit_info): job_date_begin = utc_to_local_time( self.job.date_begin.replace(tzinfo=None), self.request.user.timezone) job_date_end = utc_to_local_time( self.job.date_end.replace(tzinfo=None), self.request.user.timezone) for point in self.ride_points: point['time_in'] = utc_to_local_time( datetime.datetime.utcfromtimestamp(point['time_in']), self.request.user.timezone) point['time_out'] = utc_to_local_time( datetime.datetime.utcfromtimestamp(point['time_out']), self.request.user.timezone) if point['time_in'] >= job_date_begin and point[ 'time_out'] <= job_date_end: point['job_id'] = self.job.pk for row in self.report_data['unit_thefts']: volume = parse_float(row['c'][2]) if volume > .0 and row['c'][1]: dt = utc_to_local_time( parse_wialon_report_datetime( row['c'][1]['t'] if isinstance(row['c'][1], dict ) else row['c'][1]), self.request.user.timezone) for point in self.ride_points: if point['time_in'] <= dt <= point['time_out']: point['params']['fuelDrain'] += volume break for row in self.report_data['unit_fillings']: volume = parse_float(row['c'][1]) if volume > .0: dt = utc_to_local_time( parse_wialon_report_datetime( row['c'][0]['t'] if isinstance(row['c'][0], dict ) else row['c'][0]), self.request.user.timezone) for point in self.ride_points: if point['time_in'] <= dt <= point['time_out']: point['params']['fuelRefill'] += volume break # рассчитываем моточасы пропорционально интервалам for row in self.report_data['unit_engine_hours']: time_from = utc_to_local_time( parse_wialon_report_datetime(row['c'][0]['t'] if isinstance( row['c'][0], dict) else row['c'][0]), self.request.user.timezone) time_until_value = row['c'][1]['t'] \ if isinstance(row['c'][1], dict) else row['c'][1] if 'unknown' in time_until_value.lower(): time_until = utc_to_local_time(self.input_data['date_end'], self.request.user.timezone) else: time_until = utc_to_local_time( parse_wialon_report_datetime(time_until_value), self.request.user.timezone) for point in self.ride_points: if point['time_in'] > time_until: # дальнейшие строки точно не совпадут (виалон все сортирует по дате) break # если интервал точки меньше даты начала моточасов, значит еще не дошли if point['time_out'] < time_from: continue delta = min(time_until, point['time_out']) - max( time_from, point['time_in']) # не пересекаются: if delta.total_seconds() <= 0: continue point['params']['motoHours'] += delta.total_seconds() for row in self.report_data['unit_chronology']: row_data = row['c'] if not isinstance(row_data[0], str): send_trigger_email( 'В хронологии первое поле отчета не строка!', extra_data={ 'POST': self.request.body, 'row_data': row_data, 'user': self.request.user }) continue time_from = utc_to_local_time( parse_wialon_report_datetime(row_data[1]['t'] if isinstance( row_data[1], dict) else row_data[1]), self.request.user.timezone) time_until_value = row_data[2]['t'] \ if isinstance(row_data[2], dict) else row_data[2] if 'unknown' in time_until_value.lower(): time_until = self.input_data['date_end'] else: time_until = utc_to_local_time( parse_wialon_report_datetime(time_until_value), self.request.user.timezone) for point in self.ride_points: if point['time_in'] > time_until: # дальнейшие строки точно не совпадут (виалон все сортирует по дате) break # если интервал точки меньше даты начала хронологии, значит еще не дошли if point['time_out'] < time_from: continue delta = min(time_until, point['time_out']) - max( time_from, point['time_in']) # не пересекаются: if delta.total_seconds() < 0: continue if row_data[0].lower() in ('поездка', 'trip'): point['params']['moveMinutes'] += delta.total_seconds() # рассчитываем время работы крановой установки пропорционально интервалам # (только лишь ради кэша, необходимости в расчетах нет) for row in self.report_data['unit_digital_sensors']: time_from = utc_to_local_time( parse_wialon_report_datetime(row['c'][0]['t'] if isinstance( row['c'][0], dict) else row['c'][0]), self.request.user.timezone) time_until_value = row['c'][1]['t'] \ if isinstance(row['c'][1], dict) else row['c'][1] if 'unknown' in time_until_value.lower(): time_until = utc_to_local_time(self.input_data['date_end'], self.request.user.timezone) else: time_until = utc_to_local_time( parse_wialon_report_datetime(time_until_value), self.request.user.timezone) for point in self.ride_points: if point['time_in'] > time_until: # дальнейшие строки точно не совпадут (виалон все сортирует по дате) break # если интервал точки меньше даты начала моточасов, значит еще не дошли if point['time_out'] < time_from: continue delta = min(time_until, point['time_out']) - max( time_from, point['time_in']) # не пересекаются: if delta.total_seconds() <= 0: continue point['params']['GPMTime'] += delta.total_seconds() for point in self.ride_points: point['params']['stopMinutes'] = ( point['time_out'] - point['time_in'] ).total_seconds() - point['params']['moveMinutes']
def __init__(self, dt, volume, *args, **kwargs): tz = kwargs.pop('tz') self.dt = local_to_utc_time(parse_wialon_report_datetime(dt), tz) self.volume = parse_float(volume)
def get_context_data(self, **kwargs): kwargs = super(DischargeView, self).get_context_data(**kwargs) form = kwargs['form'] report_data = None sess_id = self.request.session.get('sid') if not sess_id: raise ReportException(WIALON_NOT_LOGINED) try: units_list = get_units(sess_id, extra_fields=True) except WialonException as e: raise ReportException(str(e)) kwargs['units'] = units_list if self.request.POST: if form.is_valid(): report_data = OrderedDict() self.user = User.objects.filter(is_active=True)\ .filter(wialon_username=self.request.session.get('user')).first() if not self.user: raise ReportException(WIALON_USER_NOT_FOUND) normal_ratio = 1 + ( form.cleaned_data['overspanding_percentage'] / 100) units_dict = OrderedDict((u['id'], u) for u in units_list) selected_unit = form.cleaned_data.get('unit') if selected_unit and selected_unit in units_dict: units_dict = {selected_unit: units_dict[selected_unit]} jobs_count = len(units_dict) print('Всего ТС: %s' % jobs_count) dt_from_utc = local_to_utc_time(form.cleaned_data['dt_from'], self.user.timezone) dt_to_utc = local_to_utc_time( form.cleaned_data['dt_to'].replace(hour=23, minute=59, second=59), self.user.timezone) ura_user = self.user.ura_user if self.user.ura_user_id else self.user jobs = Job.objects.filter(user=ura_user, date_begin__lt=dt_to_utc, date_end__gt=dt_from_utc).order_by( 'date_begin', 'date_end') jobs_cache = defaultdict(list) for job in jobs: try: jobs_cache[int(job.unit_id)].append(job) except ValueError: pass template_id = get_wialon_report_template_id( 'discharge_individual', self.user, sess_id) device_fields = defaultdict(lambda: {'extras': .0, 'idle': .0}) i = 0 for unit_id, unit in units_dict.items(): i += 1 unit_name = unit['name'] print('%s/%s) %s' % (i, jobs_count, unit_name)) # норматив потребления доп.оборудования, л / час extras_values = [ x['v'] for x in unit.get('fields', []) if x.get('n') == 'механизм' ] # норматив потребления на холостом ходу, л / час idle_values = [ x['v'] for x in unit.get('fields', []) if x.get('n') == 'хх' ] if extras_values: try: device_fields[unit_name]['extras'] = float( extras_values[0]) except ValueError: pass if idle_values: try: device_fields[unit_name]['idle'] = float( idle_values[0]) except ValueError: pass if unit_id not in report_data: report_data[unit_id] = self.get_new_grouping() report_row = report_data[unit_id] report_row['unit_name'] = unit_name report_row['unit_number'] = unit.get('number', '') report_row['vehicle_type'] = unit.get('vehicle_type', '') unit_jobs = jobs_cache.get(unit_id) if not unit_jobs: report_row['periods'].append( self.get_new_period(dt_from_utc, dt_to_utc)) else: if unit_jobs[0].date_begin > dt_from_utc: # если начало периода не попадает на смену report_row['periods'].append( self.get_new_period(dt_from_utc, unit_jobs[0].date_begin)) previous_job = None for unit_job in unit_jobs: # если между сменами есть перерыв, то тоже добавляем период if previous_job and unit_job.date_begin > previous_job.date_end: report_row['periods'].append( self.get_new_period( previous_job.date_end, unit_job.date_begin)) report_row['periods'].append( self.get_new_period(unit_job.date_begin, unit_job.date_end, unit_job)) previous_job = unit_job if unit_jobs[-1].date_end < dt_to_utc: # если смена закончилась до конца периода report_row['periods'].append( self.get_new_period(unit_jobs[-1].date_end, dt_to_utc)) # получим полный диапазон запроса dt_from = int( time.mktime( report_row['periods'][0]['dt_from'].timetuple())) dt_to = int( time.mktime( report_row['periods'][-1]['dt_to'].timetuple())) cleanup_and_request_report(self.user, template_id, sess_id) r = exec_report(self.user, template_id, sess_id, dt_from, dt_to, object_id=unit_id) wialon_report_rows = {} for table_index, table_info in enumerate( r['reportResult']['tables']): rows = get_report_rows(sess_id, table_index, table_info['rows'], level=2 if table_info['name'] == 'unit_thefts' else 1) wialon_report_rows[table_info['name']] = [ row['c'] for row in rows ] for period in report_row['periods']: for row in wialon_report_rows.get('unit_trips', []): row_dt_from, row_dt_to = self.parse_wialon_report_datetime( row) if row_dt_to is None: row_dt_to = period['dt_to'] if period['dt_from'] < row_dt_from and period[ 'dt_to'] > row_dt_to: delta = (min(row_dt_to, period['dt_to']) - max(row_dt_from, period['dt_from']) ).total_seconds() if not delta: print('empty trip period') continue trip_ratio = 1 total_delta = (row_dt_to - row_dt_from).total_seconds() if total_delta > 0: trip_ratio = delta / total_delta period['mileage'] += parse_float( row[3]) * trip_ratio period['move_hours'] += parse_timedelta(row[4]).total_seconds()\ * trip_ratio for row in wialon_report_rows.get( 'unit_digital_sensors', []): row_dt_from, row_dt_to = self.parse_wialon_report_datetime( row) if row_dt_to is None: row_dt_to = period['dt_to'] if period['dt_from'] < row_dt_from and period[ 'dt_to'] > row_dt_to: delta = min(row_dt_to, period['dt_to']) - \ max(row_dt_from, period['dt_from']) period[ 'extra_device_hours'] += delta.total_seconds( ) for row in wialon_report_rows.get('unit_thefts', []): dt = parse_wialon_report_datetime( row[1]['t'] if isinstance(row[1], dict ) else row[1]) utc_dt = local_to_utc_time(dt, self.user.timezone) if period['dt_from'] <= utc_dt <= period['dt_to']: place = row[0]['t'] if isinstance( row[0], dict) else (row[0] or '') if place and not period['discharge']['place']: period['discharge']['place'] = place if not period['discharge']['dt']: period['discharge']['dt'] = dt try: volume = float( row[2].split(' ')[0] if row[2] else .0) except ValueError: volume = .0 period['discharge']['volume'] += volume self.stats['discharge_total'] += volume self.stats['overspanding_count'] += 1 period['details'].append({ 'place': place, 'dt': dt, 'volume': volume }) extras_value = device_fields.get(unit_name, {}).get('extras', .0) idle_value = device_fields.get(unit_name, {}).get('idle', .0) for row in wialon_report_rows.get( 'unit_engine_hours', []): row_dt_from, row_dt_to = self.parse_wialon_report_datetime( row) if row_dt_to is None: row_dt_to = period['dt_to'] if period['dt_from'] < row_dt_from and period[ 'dt_to'] > row_dt_to: delta = (min(row_dt_to, period['dt_to']) - max(row_dt_from, period['dt_from']) ).total_seconds() if not delta: print('empty motohours period') continue total_delta = (row_dt_to - row_dt_from).total_seconds() period['moto_hours'] += delta # доля моточасов в периоде и общих моточасов в строке # данный множитель учтем в расчетах потребления moto_ratio = 1 if total_delta > 0: moto_ratio = delta / total_delta try: period['fact_dut'] += (float( parse_float(row[3])) if row[3] else .0) * moto_ratio except ValueError: pass try: period['fact_mileage'] += (float( parse_float(row[4])) if row[4] else .0) * moto_ratio except ValueError: pass try: period['idle_hours'] += (parse_timedelta( row[6]).total_seconds() if row[6] else .0) * moto_ratio except ValueError: pass if idle_value: period['fact_motohours'] = max( period['moto_hours'] - period['move_hours'] - period['extra_device_hours'], .0) / 3600.0 * idle_value if extras_value: period['fact_extra_device'] = \ period['extra_device_hours'] / 3600.0 * extras_value total_facts = period['fact_extra_device'] \ + period['fact_motohours'] \ + period['fact_mileage'] if total_facts: ratio = period['fact_dut'] / total_facts if ratio >= normal_ratio: overspanding = period['fact_dut'] \ - total_facts period['overspanding'] = overspanding self.stats[ 'overspanding_total'] += overspanding period['dt_from'] = utc_to_local_time( period['dt_from'], self.user.timezone) period['dt_to'] = utc_to_local_time( period['dt_to'], self.user.timezone) if report_data: for k, v in report_data.items(): v['periods'] = [ p for p in v['periods'] if p['discharge']['volume'] > .0 or p['overspanding'] > 0 ] report_data = OrderedDict( (k, v) for k, v in report_data.items() if v['periods']) kwargs.update(enumerate=enumerate, report_data=report_data, today=datetime.date.today(), stats=self.stats) return kwargs
def get_context_data(self, **kwargs): kwargs = super(VchmDrivingStyleView, self).get_context_data(**kwargs) self.is_total = bool(self.request.POST.get('total_report')) total_report_data = [] form = kwargs['form'] sess_id = self.request.session.get('sid') if not sess_id: raise ReportException(WIALON_NOT_LOGINED) try: units_list = get_units(sess_id, extra_fields=True) except WialonException as e: raise ReportException(str(e)) kwargs['units'] = units_list if not self.request.POST: return kwargs units_dict = {u['name']: u for u in units_list} if form.is_valid(): if self.is_total: user = User.objects.filter( is_active=True, username=self.request.session.get('user') ).first() if not user: raise ReportException(WIALON_USER_NOT_FOUND) report_users = UserTotalReportUser.objects\ .published()\ .order_by('ordering')\ .filter(executor_user=user)\ .select_related('report_user', 'report_user__ura_user') users = set(user.report_user for user in report_users) else: user = User.objects.filter( is_active=True, wialon_username=self.request.session.get('user') ).first() if not user: raise ReportException(WIALON_USER_NOT_FOUND) users = {user} dt_from = local_to_utc_time(datetime.datetime.combine( form.cleaned_data['dt_from'], datetime.time(0, 0, 0) ), user.timezone) dt_to = local_to_utc_time(datetime.datetime.combine( form.cleaned_data['dt_to'], datetime.time(23, 59, 59) ), user.timezone) for user in users: report_data = [] print('Evaluating report for user %s' % user) ura_user = user.ura_user if user.ura_user_id else user print('URA user is %s' % ura_user) if self.request.POST.get('total_report'): sess_id = get_wialon_session_key(user) if not sess_id: raise ReportException(WIALON_NOT_LOGINED) try: units_list = get_units(sess_id, extra_fields=True) except WialonException as e: raise ReportException(str(e)) kwargs['units'] = units_list units_dict = {u['name']: u for u in units_list} jobs = Job.objects\ .filter(user=ura_user, date_begin__lt=dt_to, date_end__gt=dt_from) if form.cleaned_data.get('unit'): jobs = jobs.filter(unit_id=str(form.cleaned_data['unit'])) self.driver_cache = { j.driver_id: j.driver_fio for j in jobs if j.driver_fio.lower() != 'нет в.а.' } self.driver_id_cache = { int(j.unit_id): j.driver_id for j in jobs if j.driver_fio.lower() != 'нет в.а.' } template_id = get_wialon_report_template_id('driving_style', user, sess_id) mobile_vehicle_types = set() if user.wialon_mobile_vehicle_types: mobile_vehicle_types = set( x.strip() for x in user.wialon_mobile_vehicle_types.lower().split(',') ) cleanup_and_request_report(user, template_id, sess_id) report_kwargs = {} if form.cleaned_data.get('unit'): report_kwargs['object_id'] = form.cleaned_data['unit'] print('Executing report...') r = exec_report( user, template_id, sess_id, int(time.mktime(dt_from.timetuple())), int(time.mktime(dt_to.timetuple())), **report_kwargs ) wialon_report_rows = {} for table_index, table_info in enumerate(r['reportResult']['tables']): wialon_report_rows[table_info['name']] = get_report_rows( sess_id, table_index, table_info['rows'], level=2 if table_info['name'] == 'unit_group_ecodriving' else 1 ) self.mileage_cache = { row['c'][0]: parse_float(row['c'][1], default=.0) for row in wialon_report_rows.get('unit_group_trips', []) } self.duration_cache = { row['c'][0]: parse_timedelta(row['c'][2]).total_seconds() for row in wialon_report_rows.get('unit_group_trips', []) } i = 0 for row in wialon_report_rows.get('unit_group_ecodriving', []): i += 1 violations = [ self.parse_report_row(x['c'], user, total=False) for x in row['r'] ] row = self.parse_report_row(row['c'], user, total=True) unit = units_dict.get(row.unit_name) if not unit: print('%s) Unit not found: %s' % (i, row.unit_name)) continue vehicle_type = unit['vehicle_type'].lower() if mobile_vehicle_types and vehicle_type \ and vehicle_type not in mobile_vehicle_types: print('%s) Skip vehicle type "%s" of item %s' % ( i, vehicle_type, row.unit_name )) continue print('%s) Processing %s' % (i, row.unit_name)) ecodriving = get_drive_rank_settings(unit['id'], sess_id) ecodriving = {k.lower(): v for k, v in ecodriving.items()} report_row = self.new_grouping(row, unit) # собственно расчеты метрик for violation in violations: verbose = violation.violation_name violation_name = '' violation_scope = 'violations_measures' if 'превышение скорости' in verbose: if 'cреднее' in verbose or 'среднее' in verbose: violation_name = 'avg_overspeed' elif 'опасное' in verbose: violation_name = 'critical_overspeed' elif 'ремень' in verbose or 'ремня' in verbose: violation_name = 'belt' elif 'фар' in verbose: violation_name = 'lights' elif 'кму' in verbose or 'стрел' in verbose: violation_name = 'jib' elif 'разгон' in verbose or 'ускорение' in verbose: violation_scope = 'per_100km_count' violation_name = 'accelerations' elif 'торможение' in verbose: violation_scope = 'per_100km_count' violation_name = 'brakings' elif 'поворот' in verbose: violation_scope = 'per_100km_count' violation_name = 'turns' if not violation_name: print( '%s) %s: unknown violaton name %s' % ( i, row.unit_name, verbose ) ) continue scope = report_row[violation_scope][violation_name] if violation_scope == 'per_100km_count': scope['count'] += violation.violation_count / row.mileage * 100 scope['total_count'] += violation.violation_count else: scope['count'] += violation.violation_count scope['total_time_percentage'] += ( violation.duration / row.duration * 100 ) scope['time_sec'] += violation.duration # суммируем штрафы rating_violation_name = violation_name if 'overspeed' in violation_name: rating_violation_name = 'overspeed' # извлечем настройки объектов и узнаем, нужно ли рассчитывать # относительно пробега settings = ecodriving.get(verbose) devider = 1 if settings and settings.get('flags', 0) in (2, 3, 7, 10): devider = max(1.0, row.mileage) fine = violation.fine / devider report_row['rating'][rating_violation_name]['fine'] += fine report_row['rating_total']['avg']['fine'] += fine if rating_violation_name in ('belt', 'lights', 'jib', 'brakings'): report_row['rating_total']['critical_avg']['fine'] += fine # расчет статистики (рейтинга) for key in report_row['rating']: scope = report_row['rating'][key] scope['rating'] = self.calculate_rating(scope['fine']) report_row['rating_total']['avg']['rating'] = self.calculate_rating( report_row['rating_total']['avg']['fine'] ) report_row['rating_total']['critical_avg']['rating'] = self.calculate_rating( report_row['rating_total']['critical_avg']['fine'] ) report_data.append(report_row) # группируем строки по нарушителям и сортируем, самых нарушающих наверх groups = defaultdict(lambda: { 'rows': [], 'driver_id': '', 'driver_fio': '', 'company_name': '' }) # сначала отсортируем без группировки, # чтобы внутри групп была правильная сортировка report_data = sorted( report_data, key=lambda x: x['rating_total']['critical_avg']['rating'] ) for row in report_data: group = groups[row['driver_id']] group['driver_id'] = row['driver_id'] group['driver_fio'] = self.driver_cache.get(row['driver_id'], DRIVER_NO_NAME) group['company_name'] = user.company_name or user.wialon_resource_name or '' group['stats'] = self.new_grouping() group['rows'].append(row) # собираем суммарную статистику по каждому водителю report_data = list(groups.values()) for group in report_data: for row in group['rows']: group['stats']['total_mileage'] += row['total_mileage'] group['stats']['total_duration'] += row['total_duration'] for field in ('avg_overspeed', 'critical_overspeed', 'belt', 'lights', 'jib'): for row in group['rows']: group['stats']['violations_measures'][field]['count'] += \ row['violations_measures'][field]['count'] group['stats']['violations_measures'][field]['time_sec'] += \ row['violations_measures'][field]['time_sec'] group['stats']['violations_measures'][field]['total_time_percentage'] = \ group['stats']['violations_measures'][field]['time_sec'] \ / group['stats']['total_duration'] * 100.0 for field in ('brakings', 'accelerations', 'turns'): for row in group['rows']: group['stats']['per_100km_count'][field]['total_count'] += \ row['per_100km_count'][field]['total_count'] group['stats']['per_100km_count'][field]['count'] = \ group['stats']['per_100km_count'][field]['total_count'] \ / group['stats']['total_mileage'] * 100 for field in ( 'overspeed', 'belt', 'lights', 'brakings', 'accelerations', 'turns', 'jib' ): for row in group['rows']: fine = row['rating'][field]['fine'] group['stats']['rating'][field]['fine'] += fine group['stats']['rating_total']['avg']['fine'] += fine if field in ('belt', 'lights', 'jib', 'brakings'): group['stats']['rating_total']['critical_avg']['fine'] += fine # расчет статистики (рейтинга) for key in group['stats']['rating']: scope = group['stats']['rating'][key] scope['rating'] = self.calculate_rating(scope['fine']) group['stats']['rating_total']['avg']['rating'] = self.calculate_rating( group['stats']['rating_total']['avg']['fine'] ) group['stats']['rating_total']['critical_avg']['rating'] = \ self.calculate_rating( group['stats']['rating_total']['critical_avg']['fine'] ) total_report_data.extend(report_data) # финально отсортируем всю группу total_report_data = sorted( total_report_data, key=lambda x: x['stats']['rating_total']['critical_avg']['rating'] ) kwargs['report_data'] = total_report_data return kwargs