"""Util functions for creating single product overview.""" from collections import OrderedDict from functools import reduce from itertools import islice from reporting.constants import physical_reporting as constants from reporting.utils.physical_reporting import get_returns_disposition def territory_to_supply_chain(territory): """Return supply chain by its territory or None. Args: territory (string): A territory abbreviation US or CA. Returns: int or None: the supply chain ID or None. """ return { 'CA': constants.CA_SUPPLY_CHAIN, 'US': constants.US_SUPPLY_CHAIN, }.get(territory) def _add(val_one, val_two): """Add two values and round using two decimal precision.""" if val_one is None and val_two is None: return None if val_one is None and val_two is not None: return round(val_two, 2) if val_one is not None and val_two is None: return round(val_one, 2) return round(val_one + val_two, 2) def _subtract(val_one, val_two): """Subtract two values and round using two decimal precision.""" if val_one is None or val_two is None: return 0 return round(val_one - val_two, 2) def _percentage(part, whole): """Percentage helper.""" if part is None or whole is None: return None if whole <= 0: return 0 return round(100 * abs(part) / abs(whole), 2) def _dollar_format(amount): """Format as dollars.""" if amount is None: return '-' if amount < 0: abs_amount = abs(amount) return '-${:0.2f}'.format(abs_amount) return '${:0.2f}'.format(amount) def _percentage_format(percent_val): """Format a percent value.""" if percent_val is None: return '-' if percent_val <= 0: return '-' return '{0:.2f}%'.format(percent_val) def _month_year_format(date): """Format a date as Month Year ex: January 2018. Args: date (datetime): a datetime object Returns: (str) full month and year combination "February 2018" """ return date.strftime('%B %Y') def _month_abbrev_format(dates): """Format a list of dates. Args: dates (list): a list of date objects. Return: (str): a string of capitalized month abbreviations. """ date_list = [date.strftime('%b') for date in sorted(dates)] return '-'.join(date_list).upper() def _extract_rtd_row(result, key, row_name): """Extract a row for display on RTD table. Args: result (dict): db result key (str): the key prefix to pull from the result. Return: dict: a dictionary representing a row in a report. """ ship_qt = result.get('{}_ship_qt'.format(key)) return_qt = result.get('{}_return_qt'.format(key)) ship_am = result.get('{}_ship_am'.format(key)) if ship_am is not None: ship_am = float(ship_am) return_am = result.get('{}_return_am'.format(key)) if return_am is not None: return_am = float(return_am) return { 'row_name': row_name, 'ship_num': ship_qt, 'return_num': return_qt, 'net_num': _add(ship_qt, return_qt), 'ship_amount': ship_am, 'return_amount': return_am, 'net_amount': _add(ship_am, return_am), 'return_percentage': _percentage(return_qt, ship_qt), } def _sum_dict(dict_one, dict_two): """Sum two dictionaries using a higher order function like reduce.""" for key, value in dict_one.items(): dict_one[key] = _add(value, dict_two.get(key, 0)) return dict_one def _without(_key, _dict): """Return a new copy of a dictionary without indicated key.""" return {i: _dict[i] for i in _dict if i != _key} def _extract_summary_row(rows, row_name): """Summarize a collection of rows. Args: rows (dict): a dict of dicts. Return: dict: dict with new key and reduced values. ex: {'return_num': 100, 'net_num': 3000, 'row_name': 'my_cool_row'} """ summary = dict() without_labels = list(map(lambda _row: _without('row_name', _row), rows)) summary.update(reduce(_sum_dict, without_labels)) ship_num = summary['ship_num'] return_num = summary['return_num'] return_percentage = _percentage(return_num, ship_num) summary.update( {'return_percentage': return_percentage, 'row_name': row_name} ) return summary def _extract_rtd_table(result): """Extract a row from PRODUCT_PHYSICAL_SALES_RTD_VIEW and format. Args: result (dict): database result. Return: dict: dictionary representing table for RTD view. """ first_five_days = [ _extract_rtd_row(result, 'day1', 'Day 1'), _extract_rtd_row(result, 'day2', 'Day 2'), _extract_rtd_row(result, 'day3', 'Day 3'), _extract_rtd_row(result, 'day4', 'Day 4'), _extract_rtd_row(result, 'day5', 'Day 5'), ] five_day_total = [_extract_summary_row(first_five_days, '5 Day Total')] first_four_weeks = [ _extract_rtd_row(result, 'week1', 'Week 1'), _extract_rtd_row(result, 'week2', 'Week 2'), _extract_rtd_row(result, 'week3', 'Week 3'), _extract_rtd_row(result, 'week4', 'Week 4'), ] four_week_total = [_extract_summary_row(first_four_weeks, '4 Week Total')] final_rows = [ _extract_rtd_row(result, 'mtd', 'Month To Date (MTD)'), _extract_rtd_row(result, 'cytd', 'Calendar Year to Date (CYTD)'), _extract_rtd_row(result, 'last_12_mth', 'Prior 12 Months (12M)'), _extract_rtd_row(result, 'last_24_mth', 'Prior 24 Months (24M)'), _extract_rtd_row(result, 'prior_cyr', 'Prior Calendar Year (PRIORCY)'), { 'row_name': 'First 4 Weeks', 'ship_num': result.get('first_4week_qt') or '-', }, { 'row_name': 'Second 4 Weeks', 'ship_num': result.get('second_4week_qt') or '-', }, ] rtd = [_extract_rtd_row(result, 'rtd', 'Release To Date (RTD)')] table = [ first_five_days, five_day_total, first_four_weeks, four_week_total, final_rows, rtd, ] return [_format_values(entry) for entry in table] def _group_list(coll, size=3): """Group a list into `size` chunks. Args: coll (list): a list of items. size (int): the number to break the collections into. Return: (list): list of `n` lists. """ it = iter(coll) for i in range(0, len(coll), size): yield [v for v in islice(it, size)] def _extract_month_row(row): """Reformat a dict to have the desired keys.""" return { 'ship_num': row.get('ship_qt'), 'return_num': row.get('return_qt'), 'net_num': _add(row.get('ship_qt'), row.get('return_qt')), 'ship_amount': float(row.get('ship_am')), 'return_amount': float(row.get('return_am')), 'net_amount': _add( float(row.get('ship_am')), float(row.get('return_am')) ), 'return_percentage': _percentage( float(row.get('return_qt')), float(row.get('ship_qt')) ), } def _extract_month_row_with_date(row): """Reformat a dict and add date label.""" record = _extract_month_row(row) record.update({'row_name': _month_year_format(row.get('reporting_dt'))}) return record def _summarize_monthly_rows(interval): """Get the values for each interval. Args: interval (list) : the values for a given interval. Return: (dict): all rows summarized into a single dictionary. """ intervals = [_extract_month_row(value) for value in interval] return reduce(_sum_dict, list(intervals)) def _get_date_keys(interval): """Get a list of date keys. Args: interval (dict): interval rows. Return: (list): list of date objects. """ return list(map(lambda val: val.get('reporting_dt'), interval)) def _extract_summary_for_interval(interval): """Get a summary row for a group of months. Args: interval (dict): dict representing a three month interval. Return: (list): list with a single summary row. We only return a list for ease of use by the frontend. """ summary = _summarize_monthly_rows(interval) date_label = _month_abbrev_format(_get_date_keys(interval)) ship_num = summary['ship_num'] return_num = summary['return_num'] return_percentage = _percentage(return_num, ship_num) summary.update( {'return_percentage': return_percentage, 'row_name': date_label} ) return summary def _format_values(rows): """Format each dict in a list where keys match keys_to_format. Args: rows (list): list of dicts to format values. Return: (list): list of formatted dicts. """ keys_to_format = { 'ship_amount': _dollar_format, 'net_amount': _dollar_format, 'return_amount': _dollar_format, 'return_percentage': _percentage_format, } formatted_rows = [] for entry in rows: new_dict = dict() for k, v in entry.items(): if k in keys_to_format.keys(): new_dict[k] = keys_to_format.get(k)(v) else: if v is None: new_dict[k] = '-' else: new_dict[k] = v formatted_rows.append(new_dict) return formatted_rows def _extract_metadata(result): """Format product metadata for table.""" return OrderedDict( [ ('Artist Name', result.get('artist_nm')), ('Product Name', result.get('product_nm')), ('Label Name', result.get('label_nm')), ('Product Code', str(result.get('product_cd')).strip()), ('Display Configuration', result.get('display_configuration')), ('Genre', result.get('genre_nm')), ('Sub Genre', result.get('genre_nm')), ('UPC', result.get('upc_cd')), ('Orchard Price', result.get('price_cd') or '-'), ('Wholesale Price', _dollar_format(result.get('wholesales_pr'))), ('Release Date', result.get('release_dt').strftime('%Y-%m-%d')), ('Product Status', result.get('status_nm')), ] ) def _extract_account(result): """Extract vendor and subaccount ids for GraphQL object resolution. Args: result (dict): db result. Return: dict: vendor and subaccount ids that own the product. """ return { 'vendor_id': result.get('vendor_id'), 'subaccount_id': result.get('subacct_id'), } def _get_return_disposition(disp_cd, override_cd): """Get return disposition based on crazy rules.""" if disp_cd is None or override_cd is None: return '' returns_disposition = get_returns_disposition(disp_cd, override_cd) return returns_disposition def _extract_inventory(result): """Format inventory for table.""" return_allowed = result.get('returns_allowed_in').strip().upper() return [ {'row_name': 'On Hand', 'total': result.get('on_hand_qt', 0)}, {'row_name': 'Open Orders', 'total': result.get('order_qt') or 0}, {'row_name': 'Future Orders', 'total': result.get('fut_dated_qt', 0)}, {'row_name': 'Available', 'total': result.get('available_qt', 0)}, { 'row_name': 'Backorders', 'total': result.get('back_order_qt', 0) or 0, }, {'row_name': 'Purchase Orders', 'total': result.get('open_po_qt', 0)}, { 'row_name': 'Potential', 'total': _add( result.get('available_qt', 0), result.get('open_po_qt', 0) ), }, {'row_name': 'Returns in Process', 'total': result.get('rip_qt', 0)}, {'row_name': 'On Hold', 'total': result.get('hold_qt', 0)}, { 'row_name': 'Returnability', 'total': 'Yes' if return_allowed == 'Y' else 'No', }, { 'row_name': 'Return Disposition', 'total': _get_return_disposition( result.get('returns_disp_cd'), result.get('returns_disp_override_cd'), ).strip(), }, {'row_name': 'Open Scrap', 'total': result.get('open_scrap_qt', 0)}, { 'row_name': '12M Overstock', 'total': result.get('overstock_12_mtd_qt', 0), }, { 'row_name': '24M Overstock', 'total': result.get('overstock_24_mtd_qt', 0), }, ] def _empty_sales_by_month_table(): """Return an empty sales by month table.""" return {constants.PHYS_MONTHLY_SALES: []} def extract_sales_by_month_table(result): """Extract monthly sales table. Args: result (list): list of dicts organized by date. Return: (list): list of lists, alternating three month/summary rows. """ if not result: return _empty_sales_by_month_table() months = [] three_month_intervals = list(_group_list(result)) for interval in three_month_intervals: months.append( _format_values([_extract_month_row_with_date(i) for i in interval]) ) months.append( _format_values([_extract_summary_for_interval(interval)]) ) return {constants.PHYS_MONTHLY_SALES: months} def _empty_overview_tables(): """Return an empty overview result.""" return { constants.PHYS_SHIPMENTS_AND_SALES: [], constants.PHYS_PRODUCT_INFO: {}, constants.PHYS_PRODUCT_INVENTORY: [], constants.PHYS_PRODUCT_ACCOUNT: {}, } def extract_overview(result): """Extract product overview tables. Args: result (ResultProxy): SqlAlchemy result from querying Snowflake. Return: (list): list of lists, alternating three month/summary rows. """ if not result: return _empty_overview_tables() return { constants.PHYS_SHIPMENTS_AND_SALES: _extract_rtd_table(dict(result)), constants.PHYS_PRODUCT_INFO: _extract_metadata(dict(result)), constants.PHYS_PRODUCT_INVENTORY: _extract_inventory(dict(result)), constants.PHYS_PRODUCT_ACCOUNT: _extract_account(dict(result)), }