wbportfolio 2.2.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wbportfolio might be problematic. Click here for more details.

Files changed (486) hide show
  1. wbportfolio/__init__.py +1 -0
  2. wbportfolio/admin/__init__.py +12 -0
  3. wbportfolio/admin/asset.py +47 -0
  4. wbportfolio/admin/custodians.py +9 -0
  5. wbportfolio/admin/portfolio.py +127 -0
  6. wbportfolio/admin/portfolio_relationships.py +22 -0
  7. wbportfolio/admin/product_groups.py +42 -0
  8. wbportfolio/admin/products.py +80 -0
  9. wbportfolio/admin/reconciliations.py +14 -0
  10. wbportfolio/admin/registers.py +17 -0
  11. wbportfolio/admin/roles.py +19 -0
  12. wbportfolio/admin/synchronization/__init__.py +2 -0
  13. wbportfolio/admin/synchronization/admin.py +114 -0
  14. wbportfolio/admin/synchronization/portfolio_synchronization.py +18 -0
  15. wbportfolio/admin/synchronization/price_computation.py +21 -0
  16. wbportfolio/admin/transactions/__init__.py +5 -0
  17. wbportfolio/admin/transactions/claim.py +16 -0
  18. wbportfolio/admin/transactions/dividends.py +14 -0
  19. wbportfolio/admin/transactions/fees.py +35 -0
  20. wbportfolio/admin/transactions/trades.py +49 -0
  21. wbportfolio/admin/transactions/transactions.py +37 -0
  22. wbportfolio/analysis/__init__.py +0 -0
  23. wbportfolio/analysis/claims.py +235 -0
  24. wbportfolio/apps.py +5 -0
  25. wbportfolio/contrib/__init__.py +0 -0
  26. wbportfolio/contrib/company_portfolio/__init__.py +0 -0
  27. wbportfolio/contrib/company_portfolio/admin.py +28 -0
  28. wbportfolio/contrib/company_portfolio/apps.py +29 -0
  29. wbportfolio/contrib/company_portfolio/configs/__init__.py +3 -0
  30. wbportfolio/contrib/company_portfolio/configs/display.py +182 -0
  31. wbportfolio/contrib/company_portfolio/configs/endpoints.py +34 -0
  32. wbportfolio/contrib/company_portfolio/configs/previews.py +37 -0
  33. wbportfolio/contrib/company_portfolio/constants.py +1 -0
  34. wbportfolio/contrib/company_portfolio/dynamic_preferences_registry.py +87 -0
  35. wbportfolio/contrib/company_portfolio/factories.py +32 -0
  36. wbportfolio/contrib/company_portfolio/filters.py +127 -0
  37. wbportfolio/contrib/company_portfolio/management.py +19 -0
  38. wbportfolio/contrib/company_portfolio/migrations/0001_initial.py +214 -0
  39. wbportfolio/contrib/company_portfolio/migrations/__init__.py +0 -0
  40. wbportfolio/contrib/company_portfolio/models.py +334 -0
  41. wbportfolio/contrib/company_portfolio/scripts.py +76 -0
  42. wbportfolio/contrib/company_portfolio/serializers.py +303 -0
  43. wbportfolio/contrib/company_portfolio/tasks.py +19 -0
  44. wbportfolio/contrib/company_portfolio/tests/__init__.py +0 -0
  45. wbportfolio/contrib/company_portfolio/tests/conftest.py +161 -0
  46. wbportfolio/contrib/company_portfolio/tests/test_models.py +161 -0
  47. wbportfolio/contrib/company_portfolio/urls.py +29 -0
  48. wbportfolio/contrib/company_portfolio/viewsets.py +195 -0
  49. wbportfolio/defaults/__init__.py +0 -0
  50. wbportfolio/defaults/fees/__init__.py +0 -0
  51. wbportfolio/defaults/fees/default.py +92 -0
  52. wbportfolio/defaults/portfolio/__init__.py +0 -0
  53. wbportfolio/defaults/portfolio/default_rebalancing.py +45 -0
  54. wbportfolio/dynamic_preferences_registry.py +58 -0
  55. wbportfolio/factories/__init__.py +35 -0
  56. wbportfolio/factories/adjustments.py +17 -0
  57. wbportfolio/factories/assets.py +75 -0
  58. wbportfolio/factories/claim.py +39 -0
  59. wbportfolio/factories/custodians.py +11 -0
  60. wbportfolio/factories/dividends.py +14 -0
  61. wbportfolio/factories/fees.py +15 -0
  62. wbportfolio/factories/indexes.py +17 -0
  63. wbportfolio/factories/portfolio_cash_flow.py +20 -0
  64. wbportfolio/factories/portfolio_cash_targets.py +15 -0
  65. wbportfolio/factories/portfolio_swing_pricings.py +15 -0
  66. wbportfolio/factories/portfolios.py +59 -0
  67. wbportfolio/factories/product_groups.py +28 -0
  68. wbportfolio/factories/products.py +56 -0
  69. wbportfolio/factories/pytest_utils.py +121 -0
  70. wbportfolio/factories/reconciliations.py +23 -0
  71. wbportfolio/factories/roles.py +20 -0
  72. wbportfolio/factories/synchronization.py +40 -0
  73. wbportfolio/factories/trades.py +35 -0
  74. wbportfolio/factories/transactions.py +21 -0
  75. wbportfolio/fdm/__init__.py +0 -0
  76. wbportfolio/fdm/tasks.py +12 -0
  77. wbportfolio/filters/__init__.py +32 -0
  78. wbportfolio/filters/assets.py +485 -0
  79. wbportfolio/filters/assets_and_net_new_money_progression.py +42 -0
  80. wbportfolio/filters/custodians.py +10 -0
  81. wbportfolio/filters/esg.py +22 -0
  82. wbportfolio/filters/performances.py +171 -0
  83. wbportfolio/filters/portfolios.py +24 -0
  84. wbportfolio/filters/positions.py +178 -0
  85. wbportfolio/filters/products.py +157 -0
  86. wbportfolio/filters/roles.py +26 -0
  87. wbportfolio/filters/signals.py +92 -0
  88. wbportfolio/filters/transactions/__init__.py +20 -0
  89. wbportfolio/filters/transactions/claim.py +394 -0
  90. wbportfolio/filters/transactions/fees.py +66 -0
  91. wbportfolio/filters/transactions/trades.py +224 -0
  92. wbportfolio/filters/transactions/transactions.py +98 -0
  93. wbportfolio/import_export/__init__.py +0 -0
  94. wbportfolio/import_export/backends/__init__.py +2 -0
  95. wbportfolio/import_export/backends/ubs/__init__.py +3 -0
  96. wbportfolio/import_export/backends/ubs/asset_position.py +45 -0
  97. wbportfolio/import_export/backends/ubs/fees.py +63 -0
  98. wbportfolio/import_export/backends/ubs/instrument_price.py +44 -0
  99. wbportfolio/import_export/backends/ubs/mixin.py +15 -0
  100. wbportfolio/import_export/backends/utils.py +58 -0
  101. wbportfolio/import_export/backends/wbfdm/__init__.py +2 -0
  102. wbportfolio/import_export/backends/wbfdm/adjustment.py +50 -0
  103. wbportfolio/import_export/backends/wbfdm/dividend.py +16 -0
  104. wbportfolio/import_export/backends/wbfdm/mixin.py +15 -0
  105. wbportfolio/import_export/handlers/__init__.py +0 -0
  106. wbportfolio/import_export/handlers/adjustment.py +39 -0
  107. wbportfolio/import_export/handlers/asset_position.py +167 -0
  108. wbportfolio/import_export/handlers/dividend.py +80 -0
  109. wbportfolio/import_export/handlers/fees.py +58 -0
  110. wbportfolio/import_export/handlers/portfolio_cash_flow.py +57 -0
  111. wbportfolio/import_export/handlers/register.py +43 -0
  112. wbportfolio/import_export/handlers/trade.py +191 -0
  113. wbportfolio/import_export/parsers/__init__.py +0 -0
  114. wbportfolio/import_export/parsers/default_mapping.py +30 -0
  115. wbportfolio/import_export/parsers/jpmorgan/__init__.py +0 -0
  116. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +63 -0
  117. wbportfolio/import_export/parsers/jpmorgan/fees.py +64 -0
  118. wbportfolio/import_export/parsers/jpmorgan/strategy.py +116 -0
  119. wbportfolio/import_export/parsers/jpmorgan/valuation.py +41 -0
  120. wbportfolio/import_export/parsers/leonteq/__init__.py +0 -0
  121. wbportfolio/import_export/parsers/leonteq/customer_trade.py +47 -0
  122. wbportfolio/import_export/parsers/leonteq/equity.py +81 -0
  123. wbportfolio/import_export/parsers/leonteq/fees.py +70 -0
  124. wbportfolio/import_export/parsers/leonteq/trade.py +94 -0
  125. wbportfolio/import_export/parsers/leonteq/valuation.py +39 -0
  126. wbportfolio/import_export/parsers/natixis/__init__.py +0 -0
  127. wbportfolio/import_export/parsers/natixis/customer_trade.py +62 -0
  128. wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +66 -0
  129. wbportfolio/import_export/parsers/natixis/d1_equity.py +80 -0
  130. wbportfolio/import_export/parsers/natixis/d1_fees.py +58 -0
  131. wbportfolio/import_export/parsers/natixis/d1_trade.py +70 -0
  132. wbportfolio/import_export/parsers/natixis/d1_valuation.py +41 -0
  133. wbportfolio/import_export/parsers/natixis/dividend.py +53 -0
  134. wbportfolio/import_export/parsers/natixis/equity.py +60 -0
  135. wbportfolio/import_export/parsers/natixis/fees.py +53 -0
  136. wbportfolio/import_export/parsers/natixis/trade.py +63 -0
  137. wbportfolio/import_export/parsers/natixis/utils.py +76 -0
  138. wbportfolio/import_export/parsers/natixis/valuation.py +46 -0
  139. wbportfolio/import_export/parsers/refinitiv/__init__.py +0 -0
  140. wbportfolio/import_export/parsers/refinitiv/adjustment.py +24 -0
  141. wbportfolio/import_export/parsers/sg_lux/__init__.py +0 -0
  142. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +70 -0
  143. wbportfolio/import_export/parsers/sg_lux/customer_trade.py +75 -0
  144. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +140 -0
  145. wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +80 -0
  146. wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +57 -0
  147. wbportfolio/import_export/parsers/sg_lux/equity.py +137 -0
  148. wbportfolio/import_export/parsers/sg_lux/fees.py +56 -0
  149. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +51 -0
  150. wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +29 -0
  151. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +36 -0
  152. wbportfolio/import_export/parsers/sg_lux/registers.py +210 -0
  153. wbportfolio/import_export/parsers/sg_lux/sylk.py +248 -0
  154. wbportfolio/import_export/parsers/sg_lux/utils.py +36 -0
  155. wbportfolio/import_export/parsers/sg_lux/valuation.py +53 -0
  156. wbportfolio/import_export/parsers/societe_generale/__init__.py +0 -0
  157. wbportfolio/import_export/parsers/societe_generale/customer_trade.py +54 -0
  158. wbportfolio/import_export/parsers/societe_generale/strategy.py +94 -0
  159. wbportfolio/import_export/parsers/societe_generale/valuation.py +37 -0
  160. wbportfolio/import_export/parsers/tellco/__init__.py +0 -0
  161. wbportfolio/import_export/parsers/tellco/customer_trade.py +64 -0
  162. wbportfolio/import_export/parsers/tellco/equity.py +86 -0
  163. wbportfolio/import_export/parsers/tellco/valuation.py +52 -0
  164. wbportfolio/import_export/parsers/ubs/__init__.py +0 -0
  165. wbportfolio/import_export/parsers/ubs/api/__init__.py +0 -0
  166. wbportfolio/import_export/parsers/ubs/api/asset_position.py +106 -0
  167. wbportfolio/import_export/parsers/ubs/api/fees.py +31 -0
  168. wbportfolio/import_export/parsers/ubs/api/instrument_price.py +20 -0
  169. wbportfolio/import_export/parsers/ubs/api/utils.py +0 -0
  170. wbportfolio/import_export/parsers/ubs/customer_trade.py +60 -0
  171. wbportfolio/import_export/parsers/ubs/equity.py +97 -0
  172. wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +67 -0
  173. wbportfolio/import_export/parsers/ubs/valuation.py +52 -0
  174. wbportfolio/import_export/parsers/vontobel/__init__.py +0 -0
  175. wbportfolio/import_export/parsers/vontobel/asset_position.py +97 -0
  176. wbportfolio/import_export/parsers/vontobel/customer_trade.py +54 -0
  177. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +40 -0
  178. wbportfolio/import_export/parsers/vontobel/instrument.py +34 -0
  179. wbportfolio/import_export/parsers/vontobel/management_fees.py +86 -0
  180. wbportfolio/import_export/parsers/vontobel/performance_fees.py +35 -0
  181. wbportfolio/import_export/parsers/vontobel/trade.py +38 -0
  182. wbportfolio/import_export/parsers/vontobel/utils.py +17 -0
  183. wbportfolio/import_export/parsers/vontobel/valuation.py +29 -0
  184. wbportfolio/import_export/resources/__init__.py +0 -0
  185. wbportfolio/import_export/resources/assets.py +68 -0
  186. wbportfolio/import_export/resources/trades.py +41 -0
  187. wbportfolio/import_export/utils.py +42 -0
  188. wbportfolio/metric/__init__.py +0 -0
  189. wbportfolio/metric/backends/__init__.py +2 -0
  190. wbportfolio/metric/backends/base.py +86 -0
  191. wbportfolio/metric/backends/constants.py +222 -0
  192. wbportfolio/metric/backends/portfolio_base.py +255 -0
  193. wbportfolio/metric/backends/portfolio_esg.py +66 -0
  194. wbportfolio/metric/tests/__init__.py +0 -0
  195. wbportfolio/metric/tests/conftest.py +4 -0
  196. wbportfolio/metric/tests/test_portfolio_base.py +135 -0
  197. wbportfolio/metric/tests/test_portfolio_esg.py +69 -0
  198. wbportfolio/migrations/0001_initial_squashed.py +13848 -0
  199. wbportfolio/migrations/0002_product_default_sub_account_squashed_0039_alter_assetallocation_company_and_more.py +3836 -0
  200. wbportfolio/migrations/0040_instrument_financial_instrument.py +26 -0
  201. wbportfolio/migrations/0041_remove_listresearch_research_ptr_and_more.py +129 -0
  202. wbportfolio/migrations/0042_instrumentlist_instrumentlistthroughmodel_and_more.py +71 -0
  203. wbportfolio/migrations/0043_alter_instrumentlistthroughmodel_options_and_more.py +238 -0
  204. wbportfolio/migrations/0044_alter_instrumentlist_identifier.py +35 -0
  205. wbportfolio/migrations/0045_alter_instrument_financial_instrument.py +26 -0
  206. wbportfolio/migrations/0046_add_product_default_account.py +166 -0
  207. wbportfolio/migrations/0047_remove_product_default_sub_account.py +14 -0
  208. wbportfolio/migrations/0048_alter_trade_status.py +29 -0
  209. wbportfolio/migrations/0049_trade_claimed_shares.py +25 -0
  210. wbportfolio/migrations/0050_fees_fee_date_fees_wbportfolio_transac_1f7a29_idx.py +44 -0
  211. wbportfolio/migrations/0051_delete_macroreview.py +11 -0
  212. wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +888 -0
  213. wbportfolio/migrations/0053_remove_product_group.py +132 -0
  214. wbportfolio/migrations/0054_portfolioinstrumentpreferredclassificationthroughmodel_and_more.py +270 -0
  215. wbportfolio/migrations/0055_remove_product__custom_management_rebates_and_more.py +139 -0
  216. wbportfolio/migrations/0056_remove_companyportfoliodata_assets_under_management_currency_and_more.py +56 -0
  217. wbportfolio/migrations/0057_alter_portfolio_preferred_instrument_classifications_and_more.py +36 -0
  218. wbportfolio/migrations/0058_pmsinstrument.py +23 -0
  219. wbportfolio/migrations/0059_fees_unique_fees.py +51 -0
  220. wbportfolio/migrations/0060_alter_portfolioportfoliothroughmodel_type.py +21 -0
  221. wbportfolio/migrations/0061_portfolio_bank_accounts_product_bank_account_and_more.py +175 -0
  222. wbportfolio/migrations/0062_alter_dailyportfoliocashflow_options.py +20 -0
  223. wbportfolio/migrations/0063_accountreconciliation_accountreconciliationline_and_more.py +133 -0
  224. wbportfolio/migrations/0064_alter_portfolio_managers_portfolio_is_tracked_and_more.py +40 -0
  225. wbportfolio/migrations/0065_alter_portfolio_managers_claim_as_shares_and_more.py +73 -0
  226. wbportfolio/migrations/0066_assetposition_initial_shares_at_custodian_and_more.py +108 -0
  227. wbportfolio/migrations/0067_assetposition_unique_asset_position.py +77 -0
  228. wbportfolio/migrations/0068_trade_internal_trade_trade_marked_as_internal_and_more.py +59 -0
  229. wbportfolio/migrations/0069_remove_portfolio_is_invested_and_more.py +56 -0
  230. wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +82 -0
  231. wbportfolio/migrations/0071_alter_trade_options_alter_trade_order.py +22 -0
  232. wbportfolio/migrations/__init__.py +0 -0
  233. wbportfolio/models/__init__.py +26 -0
  234. wbportfolio/models/adjustments.py +246 -0
  235. wbportfolio/models/asset.py +869 -0
  236. wbportfolio/models/custodians.py +101 -0
  237. wbportfolio/models/indexes.py +33 -0
  238. wbportfolio/models/mixins/__init__.py +0 -0
  239. wbportfolio/models/mixins/instruments.py +127 -0
  240. wbportfolio/models/mixins/liquidity_stress_test.py +1307 -0
  241. wbportfolio/models/portfolio.py +1039 -0
  242. wbportfolio/models/portfolio_cash_flow.py +167 -0
  243. wbportfolio/models/portfolio_cash_targets.py +46 -0
  244. wbportfolio/models/portfolio_relationship.py +135 -0
  245. wbportfolio/models/portfolio_swing_pricings.py +51 -0
  246. wbportfolio/models/product_groups.py +230 -0
  247. wbportfolio/models/products.py +569 -0
  248. wbportfolio/models/reconciliations/__init__.py +2 -0
  249. wbportfolio/models/reconciliations/account_reconciliation_lines.py +192 -0
  250. wbportfolio/models/reconciliations/account_reconciliations.py +102 -0
  251. wbportfolio/models/reconciliations/reconciliations.py +25 -0
  252. wbportfolio/models/registers.py +132 -0
  253. wbportfolio/models/roles.py +208 -0
  254. wbportfolio/models/synchronization/__init__.py +3 -0
  255. wbportfolio/models/synchronization/portfolio_synchronization.py +292 -0
  256. wbportfolio/models/synchronization/price_computation.py +200 -0
  257. wbportfolio/models/synchronization/synchronization.py +188 -0
  258. wbportfolio/models/transactions/__init__.py +7 -0
  259. wbportfolio/models/transactions/claim.py +634 -0
  260. wbportfolio/models/transactions/dividends.py +31 -0
  261. wbportfolio/models/transactions/expiry.py +7 -0
  262. wbportfolio/models/transactions/fees.py +153 -0
  263. wbportfolio/models/transactions/trade_proposals.py +502 -0
  264. wbportfolio/models/transactions/trades.py +704 -0
  265. wbportfolio/models/transactions/transactions.py +211 -0
  266. wbportfolio/models/utils.py +12 -0
  267. wbportfolio/permissions.py +13 -0
  268. wbportfolio/pms/__init__.py +0 -0
  269. wbportfolio/pms/statistics/__init__.py +0 -0
  270. wbportfolio/pms/trading/__init__.py +1 -0
  271. wbportfolio/pms/trading/handler.py +164 -0
  272. wbportfolio/pms/typing.py +194 -0
  273. wbportfolio/preferences.py +6 -0
  274. wbportfolio/reports/__init__.py +0 -0
  275. wbportfolio/reports/monthly_position_report.py +74 -0
  276. wbportfolio/risk_management/__init__.py +0 -0
  277. wbportfolio/risk_management/backends/__init__.py +11 -0
  278. wbportfolio/risk_management/backends/accounts.py +166 -0
  279. wbportfolio/risk_management/backends/controversy_portfolio.py +63 -0
  280. wbportfolio/risk_management/backends/exposure_portfolio.py +203 -0
  281. wbportfolio/risk_management/backends/instrument_list_portfolio.py +89 -0
  282. wbportfolio/risk_management/backends/liquidity_risk.py +86 -0
  283. wbportfolio/risk_management/backends/liquidity_stress_instrument.py +86 -0
  284. wbportfolio/risk_management/backends/mixins.py +220 -0
  285. wbportfolio/risk_management/backends/product_integrity.py +111 -0
  286. wbportfolio/risk_management/backends/stop_loss_instrument.py +24 -0
  287. wbportfolio/risk_management/backends/stop_loss_portfolio.py +36 -0
  288. wbportfolio/risk_management/backends/ucits_portfolio.py +63 -0
  289. wbportfolio/risk_management/tests/__init__.py +0 -0
  290. wbportfolio/risk_management/tests/conftest.py +15 -0
  291. wbportfolio/risk_management/tests/test_accounts.py +98 -0
  292. wbportfolio/risk_management/tests/test_controversy_portfolio.py +33 -0
  293. wbportfolio/risk_management/tests/test_exposure_portfolio.py +94 -0
  294. wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +60 -0
  295. wbportfolio/risk_management/tests/test_liquidity_risk.py +47 -0
  296. wbportfolio/risk_management/tests/test_product_integrity.py +55 -0
  297. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +110 -0
  298. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +119 -0
  299. wbportfolio/risk_management/tests/test_ucits_portfolio.py +39 -0
  300. wbportfolio/serializers/__init__.py +42 -0
  301. wbportfolio/serializers/adjustments.py +24 -0
  302. wbportfolio/serializers/assets.py +166 -0
  303. wbportfolio/serializers/custodians.py +26 -0
  304. wbportfolio/serializers/portfolio_cash_flow.py +48 -0
  305. wbportfolio/serializers/portfolio_cash_targets.py +20 -0
  306. wbportfolio/serializers/portfolio_relationship.py +53 -0
  307. wbportfolio/serializers/portfolio_swing_pricing.py +20 -0
  308. wbportfolio/serializers/portfolios.py +143 -0
  309. wbportfolio/serializers/positions.py +76 -0
  310. wbportfolio/serializers/product_group.py +88 -0
  311. wbportfolio/serializers/products.py +331 -0
  312. wbportfolio/serializers/reconciliations.py +171 -0
  313. wbportfolio/serializers/registers.py +72 -0
  314. wbportfolio/serializers/roles.py +60 -0
  315. wbportfolio/serializers/signals.py +157 -0
  316. wbportfolio/serializers/synchronization.py +18 -0
  317. wbportfolio/serializers/transactions/__init__.py +24 -0
  318. wbportfolio/serializers/transactions/claim.py +310 -0
  319. wbportfolio/serializers/transactions/dividends.py +18 -0
  320. wbportfolio/serializers/transactions/expiry.py +18 -0
  321. wbportfolio/serializers/transactions/fees.py +32 -0
  322. wbportfolio/serializers/transactions/trades.py +315 -0
  323. wbportfolio/serializers/transactions/transactions.py +84 -0
  324. wbportfolio/tasks.py +125 -0
  325. wbportfolio/tests/__init__.py +0 -0
  326. wbportfolio/tests/conftest.py +164 -0
  327. wbportfolio/tests/models/__init__.py +0 -0
  328. wbportfolio/tests/models/test_account_reconciliation.py +191 -0
  329. wbportfolio/tests/models/test_assets.py +193 -0
  330. wbportfolio/tests/models/test_custodians.py +12 -0
  331. wbportfolio/tests/models/test_customer_trades.py +113 -0
  332. wbportfolio/tests/models/test_dividends.py +7 -0
  333. wbportfolio/tests/models/test_imports.py +192 -0
  334. wbportfolio/tests/models/test_instrument_mixins.py +48 -0
  335. wbportfolio/tests/models/test_merge.py +133 -0
  336. wbportfolio/tests/models/test_portfolio_cash_flow.py +112 -0
  337. wbportfolio/tests/models/test_portfolio_cash_targets.py +27 -0
  338. wbportfolio/tests/models/test_portfolio_swing_pricings.py +42 -0
  339. wbportfolio/tests/models/test_portfolios.py +676 -0
  340. wbportfolio/tests/models/test_product_groups.py +80 -0
  341. wbportfolio/tests/models/test_products.py +187 -0
  342. wbportfolio/tests/models/test_roles.py +82 -0
  343. wbportfolio/tests/models/test_splits.py +233 -0
  344. wbportfolio/tests/models/test_synchronization.py +617 -0
  345. wbportfolio/tests/models/transactions/__init__.py +0 -0
  346. wbportfolio/tests/models/transactions/test_claim.py +129 -0
  347. wbportfolio/tests/models/transactions/test_fees.py +65 -0
  348. wbportfolio/tests/models/transactions/test_trades.py +204 -0
  349. wbportfolio/tests/models/utils.py +13 -0
  350. wbportfolio/tests/serializers/__init__.py +0 -0
  351. wbportfolio/tests/serializers/test_claims.py +21 -0
  352. wbportfolio/tests/signals.py +151 -0
  353. wbportfolio/tests/tests.py +31 -0
  354. wbportfolio/tests/viewsets/__init__.py +0 -0
  355. wbportfolio/tests/viewsets/test_assets.py +67 -0
  356. wbportfolio/tests/viewsets/test_performances.py +72 -0
  357. wbportfolio/tests/viewsets/test_products.py +92 -0
  358. wbportfolio/tests/viewsets/transactions/__init__.py +0 -0
  359. wbportfolio/tests/viewsets/transactions/test_claims.py +146 -0
  360. wbportfolio/urls.py +247 -0
  361. wbportfolio/utils.py +30 -0
  362. wbportfolio/viewsets/__init__.py +57 -0
  363. wbportfolio/viewsets/adjustments.py +46 -0
  364. wbportfolio/viewsets/assets.py +562 -0
  365. wbportfolio/viewsets/assets_and_net_new_money_progression.py +117 -0
  366. wbportfolio/viewsets/charts/__init__.py +1 -0
  367. wbportfolio/viewsets/charts/assets.py +247 -0
  368. wbportfolio/viewsets/configs/__init__.py +6 -0
  369. wbportfolio/viewsets/configs/buttons/__init__.py +23 -0
  370. wbportfolio/viewsets/configs/buttons/adjustments.py +13 -0
  371. wbportfolio/viewsets/configs/buttons/assets.py +145 -0
  372. wbportfolio/viewsets/configs/buttons/claims.py +83 -0
  373. wbportfolio/viewsets/configs/buttons/custodians.py +76 -0
  374. wbportfolio/viewsets/configs/buttons/fees.py +14 -0
  375. wbportfolio/viewsets/configs/buttons/mixins.py +88 -0
  376. wbportfolio/viewsets/configs/buttons/portfolios.py +115 -0
  377. wbportfolio/viewsets/configs/buttons/products.py +41 -0
  378. wbportfolio/viewsets/configs/buttons/reconciliations.py +65 -0
  379. wbportfolio/viewsets/configs/buttons/registers.py +11 -0
  380. wbportfolio/viewsets/configs/buttons/signals.py +68 -0
  381. wbportfolio/viewsets/configs/buttons/trade_proposals.py +25 -0
  382. wbportfolio/viewsets/configs/buttons/trades.py +144 -0
  383. wbportfolio/viewsets/configs/display/__init__.py +61 -0
  384. wbportfolio/viewsets/configs/display/adjustments.py +81 -0
  385. wbportfolio/viewsets/configs/display/assets.py +265 -0
  386. wbportfolio/viewsets/configs/display/claim.py +299 -0
  387. wbportfolio/viewsets/configs/display/custodians.py +24 -0
  388. wbportfolio/viewsets/configs/display/esg.py +88 -0
  389. wbportfolio/viewsets/configs/display/fees.py +133 -0
  390. wbportfolio/viewsets/configs/display/portfolio_cash_flow.py +103 -0
  391. wbportfolio/viewsets/configs/display/portfolio_relationship.py +38 -0
  392. wbportfolio/viewsets/configs/display/portfolios.py +125 -0
  393. wbportfolio/viewsets/configs/display/positions.py +75 -0
  394. wbportfolio/viewsets/configs/display/product_groups.py +54 -0
  395. wbportfolio/viewsets/configs/display/product_performance.py +241 -0
  396. wbportfolio/viewsets/configs/display/products.py +249 -0
  397. wbportfolio/viewsets/configs/display/reconciliations.py +151 -0
  398. wbportfolio/viewsets/configs/display/registers.py +71 -0
  399. wbportfolio/viewsets/configs/display/roles.py +49 -0
  400. wbportfolio/viewsets/configs/display/trade_proposals.py +97 -0
  401. wbportfolio/viewsets/configs/display/trades.py +359 -0
  402. wbportfolio/viewsets/configs/display/transactions.py +55 -0
  403. wbportfolio/viewsets/configs/endpoints/__init__.py +75 -0
  404. wbportfolio/viewsets/configs/endpoints/adjustments.py +17 -0
  405. wbportfolio/viewsets/configs/endpoints/assets.py +115 -0
  406. wbportfolio/viewsets/configs/endpoints/claim.py +106 -0
  407. wbportfolio/viewsets/configs/endpoints/custodians.py +6 -0
  408. wbportfolio/viewsets/configs/endpoints/esg.py +14 -0
  409. wbportfolio/viewsets/configs/endpoints/fees.py +26 -0
  410. wbportfolio/viewsets/configs/endpoints/portfolio_relationship.py +23 -0
  411. wbportfolio/viewsets/configs/endpoints/portfolios.py +43 -0
  412. wbportfolio/viewsets/configs/endpoints/positions.py +18 -0
  413. wbportfolio/viewsets/configs/endpoints/product_groups.py +11 -0
  414. wbportfolio/viewsets/configs/endpoints/product_performance.py +29 -0
  415. wbportfolio/viewsets/configs/endpoints/products.py +37 -0
  416. wbportfolio/viewsets/configs/endpoints/reconciliations.py +31 -0
  417. wbportfolio/viewsets/configs/endpoints/roles.py +9 -0
  418. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +17 -0
  419. wbportfolio/viewsets/configs/endpoints/trades.py +82 -0
  420. wbportfolio/viewsets/configs/endpoints/transactions.py +17 -0
  421. wbportfolio/viewsets/configs/menu/__init__.py +30 -0
  422. wbportfolio/viewsets/configs/menu/adjustments.py +8 -0
  423. wbportfolio/viewsets/configs/menu/assets.py +8 -0
  424. wbportfolio/viewsets/configs/menu/claim.py +41 -0
  425. wbportfolio/viewsets/configs/menu/custodians.py +11 -0
  426. wbportfolio/viewsets/configs/menu/fees.py +13 -0
  427. wbportfolio/viewsets/configs/menu/instrument_prices.py +10 -0
  428. wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +8 -0
  429. wbportfolio/viewsets/configs/menu/portfolios.py +15 -0
  430. wbportfolio/viewsets/configs/menu/positions.py +14 -0
  431. wbportfolio/viewsets/configs/menu/product_groups.py +10 -0
  432. wbportfolio/viewsets/configs/menu/product_performance.py +25 -0
  433. wbportfolio/viewsets/configs/menu/products.py +15 -0
  434. wbportfolio/viewsets/configs/menu/reconciliations.py +7 -0
  435. wbportfolio/viewsets/configs/menu/registers.py +10 -0
  436. wbportfolio/viewsets/configs/menu/roles.py +16 -0
  437. wbportfolio/viewsets/configs/menu/trades.py +18 -0
  438. wbportfolio/viewsets/configs/menu/transactions.py +8 -0
  439. wbportfolio/viewsets/configs/previews/__init__.py +1 -0
  440. wbportfolio/viewsets/configs/previews/portfolios.py +21 -0
  441. wbportfolio/viewsets/configs/titles/__init__.py +65 -0
  442. wbportfolio/viewsets/configs/titles/adjustments.py +19 -0
  443. wbportfolio/viewsets/configs/titles/assets.py +57 -0
  444. wbportfolio/viewsets/configs/titles/assets_and_net_new_money_progression.py +6 -0
  445. wbportfolio/viewsets/configs/titles/claim.py +81 -0
  446. wbportfolio/viewsets/configs/titles/custodians.py +12 -0
  447. wbportfolio/viewsets/configs/titles/esg.py +10 -0
  448. wbportfolio/viewsets/configs/titles/fees.py +25 -0
  449. wbportfolio/viewsets/configs/titles/instrument_prices.py +20 -0
  450. wbportfolio/viewsets/configs/titles/portfolios.py +32 -0
  451. wbportfolio/viewsets/configs/titles/positions.py +11 -0
  452. wbportfolio/viewsets/configs/titles/product_groups.py +12 -0
  453. wbportfolio/viewsets/configs/titles/product_performance.py +16 -0
  454. wbportfolio/viewsets/configs/titles/products.py +6 -0
  455. wbportfolio/viewsets/configs/titles/registers.py +12 -0
  456. wbportfolio/viewsets/configs/titles/roles.py +23 -0
  457. wbportfolio/viewsets/configs/titles/trades.py +51 -0
  458. wbportfolio/viewsets/configs/titles/transactions.py +8 -0
  459. wbportfolio/viewsets/custodians.py +66 -0
  460. wbportfolio/viewsets/esg.py +165 -0
  461. wbportfolio/viewsets/mixins.py +48 -0
  462. wbportfolio/viewsets/portfolio_cash_flow.py +31 -0
  463. wbportfolio/viewsets/portfolio_cash_targets.py +8 -0
  464. wbportfolio/viewsets/portfolio_relationship.py +46 -0
  465. wbportfolio/viewsets/portfolio_swing_pricing.py +8 -0
  466. wbportfolio/viewsets/portfolios.py +154 -0
  467. wbportfolio/viewsets/positions.py +292 -0
  468. wbportfolio/viewsets/product_groups.py +84 -0
  469. wbportfolio/viewsets/product_performance.py +646 -0
  470. wbportfolio/viewsets/products.py +529 -0
  471. wbportfolio/viewsets/reconciliations.py +160 -0
  472. wbportfolio/viewsets/registers.py +75 -0
  473. wbportfolio/viewsets/roles.py +44 -0
  474. wbportfolio/viewsets/signals.py +42 -0
  475. wbportfolio/viewsets/synchronization.py +25 -0
  476. wbportfolio/viewsets/transactions/__init__.py +40 -0
  477. wbportfolio/viewsets/transactions/claim.py +933 -0
  478. wbportfolio/viewsets/transactions/fees.py +190 -0
  479. wbportfolio/viewsets/transactions/mixins.py +19 -0
  480. wbportfolio/viewsets/transactions/trade_proposals.py +93 -0
  481. wbportfolio/viewsets/transactions/trades.py +395 -0
  482. wbportfolio/viewsets/transactions/transactions.py +123 -0
  483. wbportfolio-2.2.1.dist-info/METADATA +21 -0
  484. wbportfolio-2.2.1.dist-info/RECORD +486 -0
  485. wbportfolio-2.2.1.dist-info/WHEEL +5 -0
  486. wbportfolio-2.2.1.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,7 @@
1
+ from .claim import Claim
2
+ from .dividends import DividendTransaction
3
+ from .expiry import Expiry
4
+ from .fees import FeeCalculation, Fees
5
+ from .trade_proposals import TradeProposal
6
+ from .trades import Trade
7
+ from .transactions import Transaction
@@ -0,0 +1,634 @@
1
+ from datetime import date
2
+ from datetime import date as date_lib
3
+ from datetime import timedelta
4
+ from decimal import Decimal
5
+
6
+ from django.db import models
7
+ from django.db.models import (
8
+ DateField,
9
+ Exists,
10
+ ExpressionWrapper,
11
+ OuterRef,
12
+ Q,
13
+ QuerySet,
14
+ Sum,
15
+ )
16
+ from django.db.models.functions import Greatest
17
+ from django.dispatch import receiver
18
+ from django_fsm import FSMField, transition
19
+ from wbcore.contrib.ai.llm.config import add_llm_prompt
20
+ from wbcore.contrib.authentication.models import User
21
+ from wbcore.contrib.currency.models import CurrencyFXRates
22
+ from wbcore.contrib.directory.models import Entry
23
+ from wbcore.contrib.icons import WBIcon
24
+ from wbcore.enums import RequestType
25
+ from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
26
+ from wbcore.metadata.configs.display.instance_display.shortcuts import (
27
+ create_simple_display,
28
+ )
29
+ from wbcore.models import WBModel
30
+ from wbcore.signals import pre_merge
31
+ from wbcore.signals.models import pre_collection
32
+ from wbcore.utils.enum import ChoiceEnum
33
+ from wbcore.utils.strings import ReferenceIDMixin
34
+ from wbcrm.models import AccountRole
35
+ from wbcrm.models.accounts import Account
36
+ from wbfdm.models.instruments.instrument_prices import InstrumentPrice
37
+ from wbportfolio.models.llm.wbcrm.analyze_relationship import get_performances_prompt
38
+
39
+ from ..custodians import Custodian
40
+ from ..roles import PortfolioRole
41
+ from .trades import Trade
42
+
43
+
44
+ def can_administrate_claim(claim, user):
45
+ today = date_lib.today()
46
+ return (
47
+ user.profile.portfolio_roles.filter(
48
+ Q(role_type=PortfolioRole.RoleType.MANAGER)
49
+ & (Q(start__isnull=True) | Q(start__lte=today))
50
+ & (Q(end__isnull=True) | Q(end__gte=today))
51
+ ).exists()
52
+ or user.is_superuser
53
+ )
54
+
55
+
56
+ def can_edit_claim(claim, user):
57
+ return (claim.claimant and claim.claimant.id == user.profile.id) or user.profile.is_internal or user.is_superuser
58
+
59
+
60
+ class ClaimGroupbyChoice(ChoiceEnum):
61
+ ROOT_ACCOUNT = "Root Account"
62
+ ACCOUNT = "Account"
63
+ ROOT_ACCOUNT_OWNER = "Root Account Owner"
64
+ ACCOUNT_OWNER = "Account Owner"
65
+ PRODUCT = "Product"
66
+ PRODUCT_GROUP = "ProductGroup"
67
+ CLASSIFICATION = "Classification"
68
+
69
+ @classmethod
70
+ @property
71
+ def map(cls):
72
+ return {
73
+ "ROOT_ACCOUNT": {
74
+ "pk": "root_account",
75
+ "title_key": "root_account_repr",
76
+ "search_fields": ["root_account_repr"],
77
+ },
78
+ "ACCOUNT": {
79
+ "pk": "account",
80
+ "title_key": "account__computed_str",
81
+ "search_fields": ["account__computed_str"],
82
+ },
83
+ "ROOT_ACCOUNT_OWNER": {
84
+ "pk": "root_account_owner",
85
+ "title_key": "root_account_owner_repr",
86
+ "search_fields": ["root_account_owner_repr"],
87
+ },
88
+ "ACCOUNT_OWNER": {
89
+ "pk": "account__owner",
90
+ "title_key": "account__owner__computed_str",
91
+ "search_fields": ["account__owner__computed_str"],
92
+ },
93
+ "PRODUCT": {
94
+ "pk": "product",
95
+ "title_key": "product__computed_str",
96
+ "search_fields": ["product__computed_str"],
97
+ },
98
+ "PRODUCT_GROUP": {
99
+ "pk": "product__parent",
100
+ "title_key": "product__parent__name",
101
+ "search_fields": ["product__parent__name"],
102
+ },
103
+ "CLASSIFICATION": {
104
+ "pk": "classification_id",
105
+ "title_key": "classification_title",
106
+ "search_fields": ["classification_title"],
107
+ },
108
+ }
109
+
110
+ @classmethod
111
+ def get_map(cls, name):
112
+ return cls.map[name]
113
+
114
+
115
+ class ClaimDefaultQueryset(QuerySet):
116
+ def filter_for_user(self, user: User, validity_date: date_lib | None = None) -> QuerySet:
117
+ """
118
+ Protect the chained queryset and filter the claims that this user cannot see based on the following rules:
119
+ """
120
+ if user.has_perm("wbcrm.administrate_account"):
121
+ return self
122
+ allowed_accounts = Account.objects.filter_for_user(user, validity_date=validity_date).values(
123
+ "id"
124
+ ) # This is way faster
125
+ # accounts = self.annotate(can_see_account=Exists(allowed_accounts.filter(id=OuterRef("account"))))
126
+ if user.profile.is_internal:
127
+ return self.filter(Q(account__isnull=True) | Q(account__in=allowed_accounts))
128
+ return self.filter(Q(account__in=allowed_accounts) | (Q(account__isnull=True) & Q(creator_id=user.profile.id)))
129
+
130
+ def filter_for_customer(self, customer: Entry, include_related_roles: bool = False) -> QuerySet:
131
+ """
132
+ Filter the chained queryset to return only the claim that belongs to a certain customer
133
+ """
134
+ customer_account_ids = Account.get_accounts_for_customer(customer).values("id")
135
+
136
+ if not include_related_roles:
137
+ return self.filter(account__in=customer_account_ids)
138
+ return self.annotate(
139
+ role_exists=Exists(AccountRole.objects.filter(entry=customer, account=OuterRef("account")))
140
+ ).filter(Q(account__in=customer_account_ids) | Q(role_exists=True))
141
+
142
+ def annotate_asset_under_management_for_date(self, val_date: date):
143
+ return self.annotate(
144
+ net_value=InstrumentPrice.subquery_closest_value("net_value", val_date, instrument_pk_name="product"),
145
+ fx_rate=CurrencyFXRates.get_fx_rates_subquery(val_date),
146
+ asset_under_management=models.ExpressionWrapper(
147
+ models.F("net_value") * models.F("shares"),
148
+ output_field=models.DecimalField(max_digits=16, decimal_places=4, default=0.0),
149
+ ),
150
+ asset_under_management_usd=models.ExpressionWrapper(
151
+ models.F("asset_under_management") * models.F("fx_rate"),
152
+ output_field=models.DecimalField(max_digits=16, decimal_places=4, default=0.0),
153
+ ),
154
+ )
155
+
156
+
157
+ class ClaimManager(models.Manager):
158
+ def get_queryset(self) -> ClaimDefaultQueryset:
159
+ return ClaimDefaultQueryset(self.model)
160
+
161
+ def filter_for_user(self, user: User, validity_date: date_lib | None = None) -> QuerySet:
162
+ return self.get_queryset().filter_for_user(user, validity_date=validity_date)
163
+
164
+ def filter_for_customer(self, customer: Entry, include_related_roles: bool = False) -> QuerySet:
165
+ return self.get_queryset().filter_for_customer(customer)
166
+
167
+ def annotate_asset_under_management_for_date(self, val_date: date) -> QuerySet:
168
+ return self.get_queryset().annotate_asset_under_management_for_date(val_date)
169
+
170
+
171
+ # @workflow.register(serializer_class="wbportfolio.serializers.transactions.claim.ClaimModelSerializer") #we don't register for now. Uncomment as soon as we want to enable workflow on that model
172
+ class Claim(ReferenceIDMixin, WBModel):
173
+ """A customer can claim that a trade or part of a trade was executed my them"""
174
+
175
+ class Status(models.TextChoices):
176
+ PENDING = "PENDING", "Pending"
177
+ APPROVED = "APPROVED", "Approved"
178
+ WITHDRAWN = "WITHDRAWN", "Withdrawn"
179
+ DRAFT = "DRAFT", "Draft"
180
+ AUTO_MATCHED = "AUTO_MATCHED", "Auto-Matched"
181
+
182
+ status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name="Status")
183
+ account = models.ForeignKey(
184
+ to="wbcrm.Account",
185
+ related_name="claims",
186
+ null=True,
187
+ blank=True,
188
+ limit_choices_to=models.Q(is_terminal_account=True),
189
+ on_delete=models.SET_NULL,
190
+ verbose_name="Account",
191
+ # help_text="The account the claim is assigned to. If no sub-account is provided it will be tried to be assigned to a sub account based on the given claimant."
192
+ )
193
+
194
+ trade = models.ForeignKey(
195
+ to="wbportfolio.Trade",
196
+ related_name="claims",
197
+ blank=True,
198
+ null=True,
199
+ on_delete=models.PROTECT, # We protect the claim in case of trade deletion. This needs to be handled upstream
200
+ verbose_name="Trade",
201
+ help_text="Please select a Product first. The customer-trade the claim is consolidated against. The customer-trade and the claim don't necessarily have to have the same date, number of shares, etc.",
202
+ )
203
+ product = models.ForeignKey(
204
+ to="wbportfolio.Product", related_name="claims", on_delete=models.PROTECT, verbose_name="Product"
205
+ )
206
+ claimant = models.ForeignKey(
207
+ to="directory.Entry",
208
+ related_name="claimed",
209
+ null=True,
210
+ blank=True,
211
+ on_delete=models.SET_NULL,
212
+ verbose_name="Claimant",
213
+ # help_text="The person / company that claims this trade. If no claimant is given, it will assigned to the current user."
214
+ )
215
+ creator = models.ForeignKey(
216
+ to="directory.Entry",
217
+ related_name="claims_created",
218
+ null=True,
219
+ blank=True,
220
+ on_delete=models.SET_NULL,
221
+ verbose_name="Creator",
222
+ )
223
+ date = models.DateField(verbose_name="Trade Date")
224
+ bank = models.CharField(max_length=255, blank=True, verbose_name="Bank")
225
+ reference = models.CharField(max_length=255, null=True, blank=True, verbose_name="Additional Reference")
226
+ shares = models.DecimalField(
227
+ max_digits=15,
228
+ decimal_places=4,
229
+ help_text="The amount of shares purchased / sold.",
230
+ null=True,
231
+ blank=True,
232
+ verbose_name="Shares",
233
+ )
234
+
235
+ nominal_amount = models.DecimalField(
236
+ max_digits=15,
237
+ decimal_places=2,
238
+ help_text="The nominal amount purchased / sold. Either shares or nominal_amount has to be provided.",
239
+ null=True,
240
+ blank=True,
241
+ verbose_name="Nominal Amount",
242
+ )
243
+ external_id = models.CharField(
244
+ max_length=255,
245
+ null=True,
246
+ blank=True,
247
+ help_text="An External ID to reference a claim.",
248
+ verbose_name="External Identifier",
249
+ )
250
+ as_shares = models.BooleanField(default=True)
251
+
252
+ objects = ClaimManager()
253
+
254
+ class Meta:
255
+ verbose_name = "Claim"
256
+ verbose_name_plural = "Claims"
257
+
258
+ def __str__(self):
259
+ return f"{self.reference_id} {self.product.name} ({self.bank} - {self.shares:,} shares - {self.date}) "
260
+
261
+ def save(self, *args, auto_match: bool = True, **kwargs):
262
+ assert (
263
+ self.shares is not None or self.nominal_amount is not None
264
+ ), f"Either shares or nominal amount have to be provided. Shares={self.shares}, Nominal={self.nominal_amount}"
265
+ if self.product:
266
+ if self.shares is not None:
267
+ self.nominal_amount = self.shares * self.product.share_price
268
+ else:
269
+ self.shares = self.nominal_amount / self.product.share_price
270
+ if not self.trade and self.status == self.Status.DRAFT and auto_match:
271
+ self.trade = self.auto_match()
272
+ if self.status == self.Status.WITHDRAWN and self.trade:
273
+ self.trade = None
274
+ if self.trade:
275
+ if not self.trade.is_claimable:
276
+ raise ValueError("The selected trade is not a valid customer trade")
277
+ if self.product and self.trade.underlying_instrument.id != self.product.id:
278
+ raise ValueError("The selected product does not match the trade underlying instrument")
279
+ super().save(*args, **kwargs)
280
+
281
+ @classmethod
282
+ def get_valid_and_approved_claims(cls, val_date: date_lib | None = None, account: Account | None = None):
283
+ claims = cls.objects.annotate(
284
+ date_considered=ExpressionWrapper(
285
+ Greatest("trade__transaction_date", "date") + 1, output_field=DateField()
286
+ )
287
+ ).filter(Q(account__is_terminal_account=True) & Q(account__is_active=True) & Q(status=cls.Status.APPROVED))
288
+ if val_date:
289
+ claims = claims.filter(date_considered__lt=val_date)
290
+ if account:
291
+ claims = claims.filter(account__in=account.get_descendants(include_self=True))
292
+ return claims
293
+
294
+ @property
295
+ def nominal_value(self):
296
+ """Returns the nominal value of a claim
297
+
298
+ Shares x Share Price
299
+
300
+ Returns:
301
+ Decimal -- Nominal Value
302
+ """
303
+ return self.shares * self.product.share_price
304
+
305
+ @transition(
306
+ status,
307
+ [Status.PENDING, Status.AUTO_MATCHED],
308
+ Status.APPROVED,
309
+ permission=can_administrate_claim,
310
+ custom={
311
+ "_transition_button": ActionButton(
312
+ method=RequestType.PATCH,
313
+ identifiers=("wbportfolio:claim",),
314
+ icon=WBIcon.APPROVE.icon,
315
+ color=ButtonDefaultColor.SUCCESS,
316
+ key="approve",
317
+ label="Approve",
318
+ action_label="Approve",
319
+ description_fields="<p>Date: {{date}}</p><p>Shares: {{shares}}</p><p>Product: {{_product.name}} ({{_product.isin}})</p>",
320
+ instance_display=create_simple_display(
321
+ [
322
+ ["date", "shares", "bank"],
323
+ ["product", "claimant", "."],
324
+ ["account", "account", "account"],
325
+ ["trade", "trade", "trade"],
326
+ ]
327
+ ),
328
+ )
329
+ },
330
+ )
331
+ def approve(self, **kwargs):
332
+ pass
333
+
334
+ def can_approve(self):
335
+ errors = dict()
336
+
337
+ if not self.trade:
338
+ errors["trade"] = ["With this status, this has to be provided."]
339
+
340
+ if not self.product:
341
+ errors["product"] = ["With this status, this has to be provided."]
342
+
343
+ if not self.account:
344
+ errors["account"] = ["With this status, this has to be provided."]
345
+
346
+ # check if the specified product have a valid nav at the specified date
347
+ if (
348
+ (product := self.product)
349
+ and (claim_date := self.date)
350
+ and not product.valuations.filter(date=claim_date).exists()
351
+ ):
352
+ if (prices_qs := product.valuations.filter(date__lt=claim_date)).exists():
353
+ errors[
354
+ "date"
355
+ ] = f"For product {product.name}, the latest valid valuation date before {claim_date:%Y-%m-%d} is {prices_qs.latest('date').date:%Y-%m-%d}: Please select a valid date."
356
+ else:
357
+ errors[
358
+ "date"
359
+ ] = f"There is no valuation before {claim_date:%Y-%m-%d} for product {product.name}: Please select a valid date."
360
+ return errors
361
+
362
+ @transition(
363
+ status,
364
+ [Status.PENDING, Status.AUTO_MATCHED],
365
+ Status.DRAFT,
366
+ permission=lambda claim, user: user.profile.is_internal or user.is_superuser,
367
+ custom={
368
+ "_transition_button": ActionButton(
369
+ method=RequestType.PATCH,
370
+ color=ButtonDefaultColor.ERROR,
371
+ identifiers=("wbportfolio:claim",),
372
+ icon=WBIcon.UNDO.icon,
373
+ key="backtodraft",
374
+ label="Back to Draft",
375
+ action_label="Back to Draft",
376
+ description_fields="<p>Date: {{date}}</p><p>Shares: {{shares}}</p>",
377
+ )
378
+ },
379
+ )
380
+ def backtodraft(self, **kwargs):
381
+ pass
382
+
383
+ @transition(
384
+ status,
385
+ [Status.DRAFT],
386
+ Status.WITHDRAWN,
387
+ permission=can_edit_claim,
388
+ custom={
389
+ "_transition_button": ActionButton(
390
+ method=RequestType.PATCH,
391
+ identifiers=("wbportfolio:claim",),
392
+ color=ButtonDefaultColor.ERROR,
393
+ icon=WBIcon.DELETE.icon,
394
+ key="withdraw",
395
+ label="Withdraw",
396
+ action_label="Withdraw",
397
+ description_fields="<p>Date: {{date}}</p><p>Shares: {{shares}}</p>",
398
+ )
399
+ },
400
+ )
401
+ def withdraw(self, **kwargs):
402
+ pass
403
+
404
+ @transition(
405
+ status,
406
+ [Status.APPROVED],
407
+ Status.DRAFT,
408
+ permission=lambda claim, user: user.profile.is_internal or user.is_superuser,
409
+ custom={
410
+ "_transition_button": ActionButton(
411
+ method=RequestType.PATCH,
412
+ identifiers=("wbportfolio:claim",),
413
+ icon=WBIcon.EDIT.icon,
414
+ color=ButtonDefaultColor.WARNING,
415
+ key="revise",
416
+ label="Revise",
417
+ action_label="Revise",
418
+ description_fields="<p>Date: {{date}}</p><p>Product: {{_product.name}}</p>",
419
+ )
420
+ },
421
+ )
422
+ def revise(self, **kwargs):
423
+ pass
424
+
425
+ @transition(
426
+ status,
427
+ [Status.DRAFT, Status.AUTO_MATCHED],
428
+ Status.PENDING,
429
+ permission=lambda claim, user: user.profile.is_internal or user.is_superuser,
430
+ custom={
431
+ "_transition_button": ActionButton(
432
+ method=RequestType.PATCH,
433
+ identifiers=("wbportfolio:claim",),
434
+ icon=WBIcon.SEND.icon,
435
+ color=ButtonDefaultColor.WARNING,
436
+ key="submit",
437
+ label="Submit for approval",
438
+ action_label="Submit",
439
+ description_fields="<p>Date: {{date}}</p><p>Product: {{_product.name}}</p>",
440
+ )
441
+ },
442
+ )
443
+ def submit(self, **kwargs):
444
+ pass
445
+
446
+ def can_submit(self):
447
+ return self.can_approve()
448
+
449
+ def auto_match(self) -> Trade | None:
450
+ SHARES_EPSILON = 1 # share
451
+ auto_match_trade = None
452
+ # Obvious filtering
453
+ trades = Trade.valid_customer_trade_objects.filter(
454
+ Q(transaction_date__gte=self.date - timedelta(days=Trade.TRADE_WINDOW_INTERVAL))
455
+ & Q(transaction_date__lte=self.date + timedelta(days=Trade.TRADE_WINDOW_INTERVAL))
456
+ )
457
+ if self.product:
458
+ trades = trades.filter(underlying_instrument=self.product)
459
+ # Find trades by shares (or remaining to be claimed)
460
+ trades = trades.filter(
461
+ Q(diff_shares__lte=self.shares + SHARES_EPSILON) & Q(diff_shares__gte=self.shares - SHARES_EPSILON)
462
+ )
463
+ if trades.count() == 1:
464
+ auto_match_trade = trades.first()
465
+
466
+ # Find trades by custodian
467
+ if (
468
+ not auto_match_trade
469
+ and self.bank
470
+ and (custodian := Custodian.get_by_mapping(self.bank, use_similarity=True, create_missing=False))
471
+ ):
472
+ if trades.filter(custodian=custodian).exists():
473
+ trades = trades.filter(custodian=custodian)
474
+ if trades.count() == 1:
475
+ auto_match_trade = trades.first()
476
+
477
+ # Find trades by external_id
478
+ if not auto_match_trade and self.external_id and trades.count() > 1:
479
+ trades = trades.filter(
480
+ Q(external_id__icontains=self.external_id) | Q(external_identifier2__icontains=self.external_id)
481
+ )
482
+ if trades.count() == 1:
483
+ auto_match_trade = trades.first()
484
+
485
+ if auto_match_trade:
486
+ self.status = self.Status.AUTO_MATCHED
487
+ return auto_match_trade
488
+
489
+ @classmethod
490
+ def subquery_assets_under_management_for_account(cls, claims, price_date, account_key="account__pk"):
491
+ """Returns a subquery which annotates the assets in USD for each Sub Account based on a given queryset
492
+
493
+ Arguments:
494
+ claims {QuerySet<commission.Claim>} -- Prefiltered queryset of claims
495
+ price_date {datetime.date} -- the date which is used to calculate everything
496
+ account_key {str} -- the outer reference to the sub account pk (default: account__pk)
497
+
498
+ Returns:
499
+ Subquery -- Sub Account with assets in USD
500
+ """
501
+
502
+ return models.Subquery(
503
+ claims.filter(
504
+ status=cls.Status.APPROVED,
505
+ account__pk=models.OuterRef(account_key),
506
+ trade__transaction_date__lte=price_date,
507
+ )
508
+ .annotate_asset_under_management_for_date(price_date)
509
+ .values("account")
510
+ .annotate(sum_assets_usd=models.Sum("assets_under_management_usd"))
511
+ .values("sum_assets_usd")[:1],
512
+ output_field=models.DecimalField(max_digits=16, decimal_places=4, default=0.0),
513
+ )
514
+
515
+ @classmethod
516
+ def subquery_claim_sum_for_account(cls, claims, price_date, account_key="account__pk"):
517
+ return models.Subquery(
518
+ claims.filter(
519
+ status=cls.Status.APPROVED,
520
+ account__pk=models.OuterRef(account_key),
521
+ trade__transaction_date__lte=price_date,
522
+ )
523
+ .values("account")
524
+ .annotate(claim_sum=models.Sum("shares"))
525
+ .values("claim_sum")[:1],
526
+ output_field=models.DecimalField(max_digits=16, decimal_places=0, default=0.0),
527
+ )
528
+
529
+ @classmethod
530
+ def get_endpoint_basename(cls):
531
+ return "wbportfolio:claim"
532
+
533
+ @classmethod
534
+ def get_representation_endpoint(cls):
535
+ return "wbportfolio:claimrepresentation-list"
536
+
537
+ @classmethod
538
+ def get_representation_value_key(cls):
539
+ return "id"
540
+
541
+ @classmethod
542
+ def get_representation_label_key(cls):
543
+ return "{{shares}} ({{account}})"
544
+
545
+
546
+ @receiver(models.signals.post_save, sender="wbportfolio.Trade")
547
+ def post_save_trade(sender, instance, created, raw, **kwargs):
548
+ """
549
+ For every claimable trade, first try to autoclaim the already existing draft claims with this new trade. And auto claim them as well if the product has a default sub account
550
+ """
551
+ if not raw and instance.is_claimable and created:
552
+ min_transaction_date = instance.transaction_date - timedelta(days=Trade.TRADE_WINDOW_INTERVAL)
553
+ max_transaction_date = instance.transaction_date + timedelta(days=Trade.TRADE_WINDOW_INTERVAL)
554
+ unlinked_claims = Claim.objects.filter(
555
+ status=Claim.Status.DRAFT,
556
+ date__gte=min_transaction_date,
557
+ date__lte=max_transaction_date,
558
+ trade__isnull=True,
559
+ product__id=instance.underlying_instrument.id,
560
+ )
561
+ for claim in unlinked_claims.all():
562
+ claim.trade = claim.auto_match()
563
+ claim.save()
564
+ if instance.product and (account := instance.product.default_account):
565
+ shares = instance.shares or Decimal(0)
566
+ claimed_shares = instance.claims.filter(status=Claim.Status.APPROVED).aggregate(s=models.Sum("shares"))[
567
+ "s"
568
+ ] or Decimal(0)
569
+ rest_shares = shares - claimed_shares
570
+ if rest_shares != 0:
571
+ Claim.objects.create(
572
+ trade=instance,
573
+ shares=rest_shares,
574
+ bank=instance.bank,
575
+ date=instance.transaction_date,
576
+ product=instance.product,
577
+ account=account,
578
+ claimant=account.owner,
579
+ )
580
+ for claim in instance.claims.exclude(status=Claim.Status.APPROVED):
581
+ if claim.status == Claim.Status.DRAFT:
582
+ claim.submit()
583
+ if claim.status == Claim.Status.PENDING:
584
+ claim.approve()
585
+ claim.save()
586
+
587
+
588
+ @receiver(pre_collection, sender="wbportfolio.Trade")
589
+ def pre_trade_deletion(sender, instance, **kwargs):
590
+ # We can't use pre_delete signal here because the collector is collected before the signal and thus, it will
591
+ for claim in instance.claims.filter(
592
+ status__in=[Claim.Status.DRAFT, Claim.Status.AUTO_MATCHED, Claim.Status.WITHDRAWN]
593
+ ).all():
594
+ claim.trade = None
595
+ claim.save(auto_match=False)
596
+
597
+ if (instance.marked_for_deletion or instance.pending) and instance.claims.exists():
598
+ # If a default account exists on the trade's product, it means that the product's trade are automatically claims. In that case, we are sure that a valid trade still exists and the marked for deletion's claim can be safely remove
599
+ if instance.product and instance.product.default_account:
600
+ instance.claims.all().delete()
601
+ else:
602
+ similar_trades = Trade.objects.filter(
603
+ underlying_instrument=instance.underlying_instrument,
604
+ portfolio=instance.portfolio,
605
+ marked_for_deletion=True,
606
+ transaction_date=instance.transaction_date,
607
+ )
608
+ # We check if the sum of marked for deletion trades share sums to 0, in that case, we delete them without regards and set their potential claims to draft
609
+ if similar_trades.exists() and similar_trades.aggregate(s=Sum("shares"))["s"] == 0:
610
+ for t in similar_trades.all():
611
+ t.claims.update(trade=None, status=Claim.Status.DRAFT)
612
+ elif (other_unclaims_similar_trades := instance.get_alternative_valid_trades()).exists():
613
+ if other_unclaims_similar_trades.count() > 1:
614
+ other_unclaims_similar_trades = other_unclaims_similar_trades.filter(bank=instance.bank)
615
+ if other_trade := other_unclaims_similar_trades.first():
616
+ # If we find an unclaim trade with similar attributes, we forward the marked_for_deletion attribute to it, which will be handled/deleted in a next delete iteration
617
+ instance.claims.update(trade=other_trade)
618
+
619
+
620
+ @receiver(pre_merge, sender="wbcrm.Account")
621
+ def handle_pre_merge_account_for_claim(
622
+ sender: models.Model, merged_object: "Account", main_object: "Account", **kwargs
623
+ ):
624
+ """
625
+ Simply reassign the claim linked to the merged account to the main account
626
+ """
627
+ Claim.objects.filter(account=merged_object).update(account=main_object, reference=merged_object.reference_id)
628
+
629
+
630
+ @receiver(add_llm_prompt, sender="wbcrm.Account")
631
+ def add_holdings_to_account_heat(sender, instance, key, **kwargs):
632
+ if key == "analyze_relationship":
633
+ return get_performances_prompt(instance)
634
+ return []
@@ -0,0 +1,31 @@
1
+ from django.db import models
2
+ from wbportfolio.import_export.handlers.dividend import DividendImportHandler
3
+
4
+ from .transactions import ShareMixin, Transaction
5
+
6
+
7
+ class DividendTransaction(Transaction, ShareMixin, models.Model):
8
+ import_export_handler_class = DividendImportHandler
9
+ retrocession = models.FloatField(default=1)
10
+
11
+ def save(self, *args, **kwargs):
12
+ if (
13
+ self.shares is not None
14
+ and self.price is not None
15
+ and self.retrocession is not None
16
+ and self.total_value is None
17
+ ):
18
+ self.total_value = self.shares * self.price * self.retrocession
19
+
20
+ if self.price is not None and self.price_gross is None:
21
+ self.price_gross = self.price
22
+
23
+ if (
24
+ self.price_gross is not None
25
+ and self.retrocession is not None
26
+ and self.shares is not None
27
+ and self.total_value_gross is None
28
+ ):
29
+ self.total_value_gross = self.shares * self.price_gross * self.retrocession
30
+
31
+ super().save(*args, **kwargs)
@@ -0,0 +1,7 @@
1
+ from django.db import models
2
+
3
+ from .transactions import ShareMixin, Transaction
4
+
5
+
6
+ class Expiry(Transaction, ShareMixin, models.Model):
7
+ pass