TransactionView.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #
  2. # Copyright (c) Daniel Sheffield 2021 - 2023
  3. #
  4. # All rights reserved
  5. #
  6. # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
  7. from collections import (
  8. OrderedDict,
  9. )
  10. from psycopg.sql import (
  11. Composable,
  12. Identifier,
  13. SQL,
  14. Literal,
  15. )
  16. from .util import get_select, get_from, get_groupby
  17. ALIAS_TO_TABLE = {
  18. 'product': 'products',
  19. 'category': 'categories',
  20. 'group': 'groups',
  21. 'tags': 'tags',
  22. 'unit': 'units',
  23. 'store': 'stores',
  24. }
  25. NON_IDENTIFIER_COLUMNS = [
  26. 'ts',
  27. 'store',
  28. 'quantity',
  29. 'unit',
  30. 'price',
  31. 'organic',
  32. 'tags',
  33. ]
  34. def get_table_statement(alias):
  35. tables = ALIAS_TO_TABLE
  36. return SQL("SELECT {column} AS {alias} FROM {table}").format(
  37. column=Identifier(tables[alias], 'name'),
  38. table=Identifier(tables[alias]),
  39. alias=Identifier(alias),
  40. )
  41. def get_transactions_statement(name, **kwargs):
  42. where = []
  43. for k,v in kwargs.items():
  44. where.append(SQL('{key} {condition} {value}').format(
  45. key=Identifier(ALIAS_TO_TABLE[k], 'name'),
  46. condition=SQL('ILIKE' if k == name else '='),
  47. value=Literal(f'%{v}%' if k == name else v),
  48. ) if k in ALIAS_TO_TABLE and (k not in NON_IDENTIFIER_COLUMNS or k == name) and v else SQL('TRUE'))
  49. statement = SQL('\n').join([
  50. get_select(dict([('tags', Identifier('tags','name')), *[
  51. (k,v) for k,v in SELECT.items() if k != 'tags'
  52. ]])),
  53. get_from("transactions", JOINS),
  54. SQL('').join([SQL('WHERE '), SQL('\n AND ').join(where)]),
  55. ])
  56. return statement
  57. SELECT = OrderedDict([
  58. ('id', Identifier('transactions', 'id')),
  59. ('ts', Identifier('transactions', 'ts')),
  60. ('store', Identifier('stores', 'name')),
  61. ('code', Identifier('stores', 'code')),
  62. ('description', Identifier('transactions', 'description')),
  63. ('volume', Identifier('quantity')),
  64. ('unit', Identifier('units', 'name')),
  65. ('price', Identifier('price')),
  66. ('quantity', Identifier('quantity')),
  67. ('$/unit', SQL("""TRUNC(price/quantity,4)""")),
  68. ('total', SQL("""sum(transactions.price)
  69. OVER (
  70. PARTITION BY transactions.ts::date
  71. ORDER BY transactions.id
  72. ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  73. )""")),
  74. ('group', Identifier('groups', 'name')),
  75. ('category', Identifier('categories', 'name')),
  76. ('product', Identifier('products', 'name')),
  77. ('organic', Identifier('organic')),
  78. ('tags', SQL(
  79. """array_agg({tag_name}) FILTER (WHERE {tag_name} IS NOT NULL)"""
  80. ).format(
  81. tag_name=Identifier('tags','name'),
  82. ))
  83. ])
  84. GROUPBY = OrderedDict([
  85. ('id', Identifier('transactions', 'id')),
  86. ('ts', Identifier('transactions', 'ts')),
  87. ('store', Identifier('stores', 'name')),
  88. ('code', Identifier('stores', 'code')),
  89. ('description', Identifier('transactions', 'description')),
  90. ('volume', Identifier('quantity')),
  91. ('unit', Identifier('units', 'name')),
  92. ('price', Identifier('price')),
  93. ('quantity', Identifier('quantity')),
  94. ('group', Identifier('groups', 'name')),
  95. ('category', Identifier('categories', 'name')),
  96. ('product', Identifier('products', 'name')),
  97. ('organic', Identifier('organic')),
  98. ])
  99. JOINS = OrderedDict([
  100. ('units', ('id', 'unit_id')),
  101. ('stores', ('id', 'store_id')),
  102. ('products', ('id', 'product_id')),
  103. ('categories', ('id', 'category_id')),
  104. ('groups', ('id', 'group_id')),
  105. ('tags_map', ('transaction_id', ('transactions','id'))),
  106. ('tags', ('id', ('tags_map','tag_id'))),
  107. ])
  108. def get_where(date, store, full_name=False, exact_time=False):
  109. where = []
  110. if store is not None:
  111. where.append(SQL(' ').join([
  112. Identifier('stores', 'name' if full_name else 'code'),
  113. SQL('='),
  114. Literal(store)
  115. ]))
  116. where.append(
  117. SQL("{ts} at time zone 'utc' BETWEEN {date}::date AND {date}::date + {interval}::interval").format(
  118. ts=Identifier('ts'),
  119. date=Literal(str(date)),
  120. interval=Literal('23 hours 59 minutes 59 seconds'),
  121. ) if not exact_time else SQL("{ts} at time zone 'utc' = {date}").format(
  122. ts=Identifier('ts'),
  123. date=Literal(str(date)),
  124. )
  125. )
  126. return SQL('').join([
  127. SQL("WHERE"
  128. "\n "),
  129. SQL("\n AND ").join(where),
  130. ])
  131. def get_sort() -> Composable:
  132. return SQL('ORDER BY {_id} DESC').format(
  133. _id=Identifier('transactions', 'id')
  134. )
  135. def get_session_transactions_statement(date, store, full_name=False, exact_time=False):
  136. statement = SQL('\n').join([
  137. get_select(SELECT),
  138. get_from("transactions", JOINS),
  139. get_where(
  140. date if exact_time else date.replace(
  141. hour=0, minute=0, second=0, microsecond=0
  142. ),
  143. store,
  144. full_name=full_name,
  145. exact_time=exact_time
  146. ),
  147. get_groupby(GROUPBY),
  148. get_sort(),
  149. ])
  150. return statement