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,192 @@
1
+ from decimal import Decimal
2
+ from typing import TYPE_CHECKING
3
+
4
+ from django.db import models
5
+ from django.db.models import Case, F, Q, When
6
+ from django.db.models.functions import Cast
7
+ from django.utils.translation import gettext_lazy as _
8
+ from wbcore.models import WBModel
9
+ from wbcrm.models import Account
10
+ from wbfdm.models.instruments import InstrumentPrice
11
+ from wbportfolio.models.transactions.claim import Claim
12
+
13
+ if TYPE_CHECKING:
14
+ from wbportfolio.models import AccountReconciliation
15
+
16
+
17
+ class AccountReconciliationLineQuerySet(models.QuerySet):
18
+ def update_or_create_for_reconciliation(self, reconciliation: "AccountReconciliation"):
19
+ holding_per_product = (
20
+ Claim.objects.filter(
21
+ status=Claim.Status.APPROVED,
22
+ account__in=reconciliation.account.get_descendants(include_self=True),
23
+ account__status=Account.Status.OPEN,
24
+ date__lte=reconciliation.reconciliation_date,
25
+ )
26
+ .values("product")
27
+ .annotate(
28
+ holdings=models.Sum("shares"),
29
+ price=InstrumentPrice.subquery_closest_value(
30
+ "net_value", val_date=reconciliation.reconciliation_date, instrument_pk_name="product_id"
31
+ ),
32
+ price_date=InstrumentPrice.subquery_closest_value(
33
+ "date", val_date=reconciliation.reconciliation_date, instrument_pk_name="product_id"
34
+ ),
35
+ )
36
+ .filter(~Q(holdings=0))
37
+ .values("product_id", "holdings", "price", "price_date")
38
+ )
39
+ lines = []
40
+ for holding in holding_per_product:
41
+ line = AccountReconciliationLine(
42
+ reconciliation=reconciliation,
43
+ product_id=holding["product_id"],
44
+ shares=holding["holdings"],
45
+ shares_external=holding["holdings"],
46
+ price=holding["price"],
47
+ price_date=holding["price_date"],
48
+ )
49
+ line.calculate_fields()
50
+ lines.append(line)
51
+
52
+ return AccountReconciliationLine.objects.bulk_create(
53
+ lines,
54
+ update_conflicts=True,
55
+ update_fields=["shares", "nominal_value", "price", "price_date"],
56
+ unique_fields=["reconciliation", "product"],
57
+ )
58
+
59
+ def annotate_currency(self) -> "AccountReconciliationLineQuerySet":
60
+ return self.annotate(currency=F("product__currency__symbol"))
61
+
62
+ def annotate_currency_key(self) -> "AccountReconciliationLineQuerySet":
63
+ return self.annotate(currency_key=F("product__currency__key"))
64
+
65
+ def annotate_assets_under_management(self) -> "AccountReconciliationLineQuerySet":
66
+ return self.annotate(assets_under_management=F("shares") * F("price"))
67
+
68
+ def annotate_assets_under_management_external(self) -> "AccountReconciliationLineQuerySet":
69
+ return self.annotate(assets_under_management_external=F("shares_external") * F("price"))
70
+
71
+ def annotate_is_equal(self) -> "AccountReconciliationLineQuerySet":
72
+ return self.annotate(
73
+ is_equal=models.Case(
74
+ models.When(shares=F("shares_external"), then=True), default=False, output_field=models.BooleanField()
75
+ )
76
+ )
77
+
78
+ def annotate_shares_diff(self) -> "AccountReconciliationLineQuerySet":
79
+ return self.annotate(shares_diff=F("shares_external") - F("shares"))
80
+
81
+ def annotate_pct_diff(self) -> "AccountReconciliationLineQuerySet":
82
+ ff = models.FloatField()
83
+ return self.annotate(
84
+ pct_diff=Case(
85
+ When(~Q(shares=0), then=(Cast("shares_external", ff) - Cast("shares", ff)) / Cast("shares", ff)),
86
+ default=None,
87
+ )
88
+ )
89
+
90
+ def annotate_nominal_value_diff(self) -> "AccountReconciliationLineQuerySet":
91
+ return self.annotate(nominal_value_diff=F("nominal_value_external") - F("nominal_value"))
92
+
93
+ def annotate_assets_under_management_diff(self) -> "AccountReconciliationLineQuerySet":
94
+ """Annotates the AuM diff. This will fail if annotate_assets_under_management was not called beforehand"""
95
+ return self.annotate(
96
+ assets_under_management_diff=F("assets_under_management_external") - F("assets_under_management")
97
+ )
98
+
99
+
100
+ class AccountReconciliationLine(WBModel):
101
+ reconciliation = models.ForeignKey(
102
+ to="wbportfolio.AccountReconciliation",
103
+ related_name="lines",
104
+ on_delete=models.CASCADE,
105
+ )
106
+
107
+ product = models.ForeignKey(
108
+ to="wbportfolio.Product",
109
+ related_name="account_reconciliation_lines",
110
+ on_delete=models.CASCADE,
111
+ )
112
+
113
+ price = models.DecimalField(
114
+ decimal_places=4,
115
+ max_digits=18,
116
+ default=Decimal(0),
117
+ help_text=_("The last share price of the product"),
118
+ )
119
+
120
+ price_date = models.DateField()
121
+
122
+ shares = models.DecimalField(
123
+ decimal_places=4,
124
+ max_digits=18,
125
+ default=Decimal(0),
126
+ help_text=_("The number of shares computed through the Workbench"),
127
+ )
128
+ nominal_value = models.DecimalField(
129
+ decimal_places=4,
130
+ max_digits=18,
131
+ default=Decimal(0),
132
+ help_text=_("The nominal value computed through the Workbench"),
133
+ )
134
+ shares_external = models.DecimalField(
135
+ decimal_places=4,
136
+ max_digits=18,
137
+ default=Decimal(0),
138
+ help_text=_(
139
+ "The number of shares externally provided through the Account holder. Initially set to the number of shares computed through the Workbench"
140
+ ),
141
+ )
142
+ nominal_value_external = models.DecimalField(
143
+ decimal_places=4,
144
+ max_digits=18,
145
+ default=Decimal(0),
146
+ help_text=_(
147
+ "The nominal value externally provided through the Account holder. Initially set to the number of shares computed through the Workbench"
148
+ ),
149
+ )
150
+
151
+ objects = AccountReconciliationLineQuerySet.as_manager()
152
+
153
+ class Meta:
154
+ verbose_name = _("Account Reconciliation (Product)")
155
+ verbose_name_plural = _("Account Reconciliation (Products)")
156
+ constraints = [
157
+ models.UniqueConstraint(fields=["reconciliation", "product"], name="unique_reconcilation_product"),
158
+ ]
159
+
160
+ def save(self, *args, **kwargs):
161
+ self.calculate_fields()
162
+ super().save(*args, **kwargs)
163
+
164
+ def calculate_fields(self):
165
+ if self.shares is not None:
166
+ self.nominal_value = self.shares * self.product.share_price
167
+ elif self.nominal_value is not None:
168
+ self.shares = self.nominal_value / self.product.share_price
169
+
170
+ if self.shares_external is not None:
171
+ self.nominal_value_external = self.shares_external * self.product.share_price
172
+ elif self.nominal_value_external is not None:
173
+ self.shares_external = self.nominal_value_external / self.product.share_price
174
+
175
+ def __str__(self) -> str:
176
+ return f"{self.reconciliation}: {self.product} {self.shares} shares"
177
+
178
+ @classmethod
179
+ def get_endpoint_basename(cls):
180
+ return "wbportfolio:accountreconciliationline"
181
+
182
+ @classmethod
183
+ def get_representation_endpoint(cls):
184
+ return "wbportfolio:accountreconciliationlinerepresentation-list"
185
+
186
+ @classmethod
187
+ def get_representation_value_key(cls):
188
+ return "id"
189
+
190
+ @classmethod
191
+ def get_representation_label_key(cls):
192
+ return "{{reconciliation}}"
@@ -0,0 +1,102 @@
1
+ from contextlib import suppress
2
+
3
+ from django.db import models
4
+ from django.db.models.signals import post_save
5
+ from django.dispatch import receiver
6
+
7
+ # from django.utils.translation import gettext_lazy as _
8
+ from dynamic_preferences.registries import global_preferences_registry
9
+ from wbcore.contrib.authentication.models import User
10
+ from wbcore.contrib.notifications.dispatch import send_notification
11
+ from wbcore.contrib.notifications.utils import create_notification_type
12
+ from wbportfolio.models.reconciliations.account_reconciliation_lines import (
13
+ AccountReconciliationLine,
14
+ )
15
+ from wbportfolio.models.reconciliations.reconciliations import Reconciliation
16
+
17
+
18
+ class AccountReconciliationQuerySet(models.QuerySet):
19
+ def create(self, **kwargs) -> models.QuerySet["AccountReconciliation"]:
20
+ reconciliation = super().create(**kwargs)
21
+ AccountReconciliationLine.objects.update_or_create_for_reconciliation(reconciliation)
22
+ return reconciliation
23
+
24
+ def filter_for_user(self, user: User) -> models.QuerySet["AccountReconciliation"]:
25
+ if user.is_superuser or user.is_internal:
26
+ return self
27
+
28
+ if not user.has_perm("wbportfolio.view_accountreconciliation"):
29
+ return self.none()
30
+
31
+ return self.filter(
32
+ account__roles__role_type__key="reconciliation-manager",
33
+ account__roles__entry_id=user.profile_id,
34
+ )
35
+
36
+
37
+ class AccountReconciliation(Reconciliation):
38
+ lines: models.QuerySet[AccountReconciliationLine]
39
+
40
+ account = models.ForeignKey(
41
+ to="wbcrm.Account",
42
+ related_name="wbportfolio_reconciliations",
43
+ on_delete=models.CASCADE,
44
+ )
45
+
46
+ objects = AccountReconciliationQuerySet.as_manager()
47
+
48
+ class Meta:
49
+ verbose_name = "Account Holdings Reconciliation"
50
+ verbose_name_plural = "Account Holdings Reconciliations"
51
+ constraints = [
52
+ models.UniqueConstraint(fields=["reconciliation_date", "account"], name="unique_date_account"),
53
+ ]
54
+ notification_types = [
55
+ create_notification_type(
56
+ "wbportfolio.account_reconciliation.notify",
57
+ "Account Reconciliation Events",
58
+ "A notification that informs the user about an account reconciliation event.",
59
+ email=True,
60
+ ),
61
+ ]
62
+
63
+ def __str__(self) -> str:
64
+ return f"{self.account} ({self.reconciliation_date:%d.%m.%Y})"
65
+
66
+ @classmethod
67
+ def get_endpoint_basename(cls):
68
+ return "wbportfolio:accountreconciliation"
69
+
70
+ @classmethod
71
+ def get_representation_endpoint(cls):
72
+ return "wbportfolio:accountreconciliationrepresentation-list"
73
+
74
+ @classmethod
75
+ def get_representation_value_key(cls):
76
+ return "id"
77
+
78
+ @classmethod
79
+ def get_representation_label_key(cls):
80
+ return "{{account}} {{reconciliation_date}}"
81
+
82
+ def notify(self, update: bool = False):
83
+ registry = global_preferences_registry.manager()
84
+ title = registry["wbportfolio__account_holding_reconciliation_notification_title"]
85
+ body = registry[f"wbportfolio__account_holding_reconciliation_notification_body{'_update' if update else ''}"]
86
+ for role in self.account.roles.filter(role_type__key="reconciliation-manager"):
87
+ with suppress(User.DoesNotExist):
88
+ user = User.objects.get(profile_id=role.entry.id)
89
+ send_notification(
90
+ code="wbportfolio.account_reconciliation.notify",
91
+ title=title,
92
+ body=body.format(account=self.account, reconciliation_date=self.reconciliation_date),
93
+ user=user,
94
+ reverse_name="wbportfolio:accountreconciliation-detail",
95
+ reverse_args=[self.id],
96
+ )
97
+
98
+
99
+ @receiver(post_save, sender=AccountReconciliation)
100
+ def notify_on_create(sender, instance: AccountReconciliation, created: bool, **kwargs):
101
+ if created:
102
+ instance.notify()
@@ -0,0 +1,25 @@
1
+ from django.db import models
2
+ from django.utils.translation import gettext_lazy as _
3
+ from wbcore.models import WBModel
4
+
5
+
6
+ class Reconciliation(WBModel):
7
+ reconciliation_date = models.DateField()
8
+
9
+ creator = models.ForeignKey(
10
+ to="authentication.User",
11
+ on_delete=models.PROTECT,
12
+ related_name="+",
13
+ )
14
+ approved_by = models.ForeignKey(
15
+ to="authentication.User",
16
+ on_delete=models.PROTECT,
17
+ null=True,
18
+ blank=True,
19
+ verbose_name=_("Approved By"),
20
+ related_name="+",
21
+ )
22
+ approved_dt = models.DateTimeField(null=True, blank=True, verbose_name=_("Approved Timestamp"))
23
+
24
+ class Meta:
25
+ abstract = True
@@ -0,0 +1,132 @@
1
+ from django.db import models
2
+ from wbcore.contrib.io.mixins import ImportMixin
3
+ from wbcore.models import WBModel
4
+ from wbportfolio.import_export.handlers.register import RegisterImportHandler
5
+
6
+
7
+ class Register(ImportMixin, WBModel):
8
+ import_export_handler_class = RegisterImportHandler
9
+
10
+ class RegisterStatus(models.TextChoices):
11
+ ACTIVE = ("ACTIVE", "Active")
12
+ INACTIVE = ("INACTIVE", "Inactive")
13
+ WARNING = ("WARNING", "Warning")
14
+
15
+ class RegisterInvestorType(models.TextChoices):
16
+ BANK = ("BANK", "Bank and Financial Institution")
17
+ NOMINEE = ("NOMINEE", "Nominee")
18
+ GLOBAL = ("GLOBAL", "Global")
19
+ NON_FINANCIAL_ENTITY = ("NON_FINANCIAL_ENTITY", "Non Financial Entities/Corporate")
20
+
21
+ register_reference = models.CharField(max_length=255, unique=True)
22
+ register_name_1 = models.CharField(max_length=255)
23
+ register_name_2 = models.CharField(max_length=255, default="")
24
+ global_register_reference = models.CharField(max_length=255, default="")
25
+ external_register_reference = models.CharField(max_length=255, default="")
26
+
27
+ custodian_reference = models.CharField(max_length=255)
28
+ custodian_name_1 = models.CharField(max_length=255)
29
+ custodian_name_2 = models.CharField(max_length=255, default="")
30
+ custodian_address = models.TextField(default="", blank=True)
31
+ custodian_postcode = models.CharField(max_length=255, default="")
32
+
33
+ custodian_city = models.ForeignKey(
34
+ "geography.Geography",
35
+ related_name="register_custodian_city_registers",
36
+ on_delete=models.PROTECT,
37
+ limit_choices_to={"level": 3},
38
+ null=True,
39
+ blank=True,
40
+ )
41
+ custodian_country = models.ForeignKey(
42
+ "geography.Geography",
43
+ related_name="register_custodian_country_registers",
44
+ on_delete=models.PROTECT,
45
+ limit_choices_to={"level": 1},
46
+ null=True,
47
+ blank=True,
48
+ )
49
+
50
+ sales_reference = models.CharField(max_length=255, default="")
51
+ dealer_reference = models.CharField(max_length=255, default="")
52
+
53
+ outlet_reference = models.CharField(max_length=255)
54
+ outlet_name = models.CharField(max_length=255)
55
+ outlet_address = models.TextField(default="", blank=True)
56
+ outlet_postcode = models.CharField(max_length=255, default="")
57
+
58
+ outlet_city = models.ForeignKey(
59
+ "geography.Geography",
60
+ related_name="register_outlet_city_registers",
61
+ null=True,
62
+ blank=True,
63
+ on_delete=models.SET_NULL,
64
+ limit_choices_to={"level": 3},
65
+ )
66
+ outlet_country = models.ForeignKey(
67
+ "geography.Geography",
68
+ related_name="register_outlet_country_registers",
69
+ null=True,
70
+ blank=True,
71
+ on_delete=models.SET_NULL,
72
+ limit_choices_to={"level": 1},
73
+ )
74
+
75
+ citizenship = models.ForeignKey(
76
+ "geography.Geography",
77
+ related_name="register_citizenship_registers",
78
+ on_delete=models.PROTECT,
79
+ limit_choices_to={"level": 1},
80
+ null=True,
81
+ blank=True,
82
+ )
83
+ residence = models.ForeignKey(
84
+ "geography.Geography",
85
+ related_name="register_residence_registers",
86
+ on_delete=models.PROTECT,
87
+ limit_choices_to={"level": 1},
88
+ null=True,
89
+ blank=True,
90
+ )
91
+
92
+ investor_type = models.CharField(
93
+ max_length=24, choices=RegisterInvestorType.choices, default=RegisterInvestorType.BANK
94
+ )
95
+ status = models.CharField(max_length=8, choices=RegisterStatus.choices, default=RegisterStatus.ACTIVE)
96
+ status_message = models.TextField(default="", blank=True)
97
+
98
+ opened = models.DateField()
99
+ opened_reference_1 = models.CharField(max_length=255, default="")
100
+ opened_reference_2 = models.CharField(max_length=255, default="")
101
+
102
+ updated_reference_1 = models.CharField(max_length=255, default="")
103
+ updated_reference_2 = models.CharField(max_length=255, default="")
104
+
105
+ computed_str = models.CharField(max_length=255, null=True, blank=True)
106
+
107
+ def __str__(self) -> str:
108
+ return f"{self.register_reference}: {self.register_name_1} {self.register_name_2}"
109
+
110
+ class Meta:
111
+ verbose_name = "Register"
112
+ verbose_name_plural = "Registers"
113
+
114
+ def save(self, *args, **kwargs):
115
+ self.computed_str = str(self)
116
+ super().save(*args, **kwargs)
117
+
118
+ @classmethod
119
+ def get_endpoint_basename(cls):
120
+ return "wbportfolio:register"
121
+
122
+ @classmethod
123
+ def get_representation_endpoint(cls):
124
+ return "wbportfolio:registerrepresentation-list"
125
+
126
+ @classmethod
127
+ def get_representation_value_key(cls):
128
+ return "id"
129
+
130
+ @classmethod
131
+ def get_representation_label_key(cls):
132
+ return "{{computed_str}}"
@@ -0,0 +1,208 @@
1
+ from datetime import date, timedelta
2
+
3
+ from django.db import models
4
+ from django.db.models import Q
5
+ from django.dispatch import receiver
6
+ from wbcore.contrib.directory.signals import deactivate_profile
7
+ from wbcore.signals import pre_merge
8
+ from wbfdm.models import Instrument
9
+
10
+
11
+ class PortfolioRole(models.Model):
12
+ default_error_messages = {
13
+ "manager": "If role type is Manager or Risk Manager, no {model} can be selected.",
14
+ "product_index": "Not both Product and Index can be selected.",
15
+ "start_end": "If a start and end date are selected, then the end date has be after the start date.",
16
+ }
17
+
18
+ class RoleType(models.TextChoices):
19
+ MANAGER = "MANAGER", "Manager"
20
+ RISK_MANAGER = "RISK_MANAGER", "Risk Manager"
21
+ PORTFOLIO_MANAGER = "PORTFOLIO_MANAGER", "Portfolio Manager"
22
+ ANALYST = "ANALYST", "Analyst"
23
+
24
+ role_type = models.CharField(max_length=24, choices=RoleType.choices, verbose_name="Type")
25
+ person = models.ForeignKey(
26
+ "directory.Person",
27
+ related_name="portfolio_roles",
28
+ on_delete=models.CASCADE,
29
+ verbose_name="Person",
30
+ )
31
+
32
+ start = models.DateField(null=True, blank=True, verbose_name="Start")
33
+ end = models.DateField(null=True, blank=True, verbose_name="End")
34
+
35
+ instrument = models.ForeignKey(
36
+ "wbfdm.Instrument",
37
+ related_name="portfolio_roles",
38
+ null=True,
39
+ blank=True,
40
+ limit_choices_to=models.Q(children__isnull=True),
41
+ on_delete=models.CASCADE,
42
+ verbose_name="Instrument",
43
+ )
44
+
45
+ weighting = models.FloatField(default=1, verbose_name="Weight")
46
+
47
+ class Meta:
48
+ verbose_name = "Portfolio Role"
49
+ verbose_name_plural = "Portfolio Roles"
50
+
51
+ def __str__(self):
52
+ return f"{self.role_type} {self.person.computed_str}"
53
+
54
+ def save(self, *args, **kwargs):
55
+ assert (
56
+ self.role_type in [self.RoleType.MANAGER, self.RoleType.RISK_MANAGER] and not self.instrument
57
+ ) or self.role_type in [
58
+ self.RoleType.PORTFOLIO_MANAGER,
59
+ self.RoleType.ANALYST,
60
+ ], self.default_error_messages[
61
+ "manager"
62
+ ].format(
63
+ model="instrument"
64
+ )
65
+
66
+ assert (self.start and self.end and self.start < self.end) or (
67
+ not self.start or not self.end
68
+ ), self.default_error_messages["start_end"]
69
+
70
+ super().save(*args, **kwargs)
71
+
72
+ @classmethod
73
+ def is_manager(cls, person):
74
+ """Determines whether a person can access manager things (or is a superuser)
75
+
76
+ Arguments:
77
+ person {crm.Person} -- The person to be checked
78
+
79
+ Returns:
80
+ bool -- Can manage or not
81
+ """
82
+
83
+ today = date.today()
84
+
85
+ manager = cls.objects.filter(
86
+ Q(person=person)
87
+ & Q(role_type__in=[cls.RoleType.MANAGER, cls.RoleType.RISK_MANAGER])
88
+ & (Q(start__isnull=True) | Q(start__lte=today))
89
+ & (Q(end__isnull=True) | Q(end__gte=today))
90
+ )
91
+
92
+ return person.user_account.is_superuser or manager.exists()
93
+
94
+ @classmethod
95
+ def is_portfolio_manager(cls, person, instrument=None, portfolio=None, include_superuser=True):
96
+ """Determines whether a person can access portfolio manager things (or is a superuser/manager)
97
+
98
+ Arguments:
99
+ person {crm.Person} -- The person to be checked
100
+
101
+ Returns:
102
+ bool -- Can manage or not
103
+ """
104
+ today = date.today()
105
+ manager = cls.objects.filter(
106
+ (
107
+ Q(person=person)
108
+ & Q(role_type__in=[cls.RoleType.PORTFOLIO_MANAGER, cls.RoleType.MANAGER, cls.RoleType.RISK_MANAGER])
109
+ )
110
+ & (Q(start__isnull=True) | Q(start__lte=today))
111
+ & (Q(end__isnull=True) | Q(end__gte=today))
112
+ )
113
+
114
+ if instrument:
115
+ manager = manager.filter(
116
+ Q(instrument=instrument)
117
+ | Q(instrument__isnull=True)
118
+ | Q(role_type__in=[cls.RoleType.MANAGER, cls.RoleType.RISK_MANAGER])
119
+ )
120
+ elif portfolio:
121
+ manager = manager.filter(
122
+ Q(instrument__in=portfolio.instruments.all())
123
+ | Q(instrument__isnull=True)
124
+ | Q(role_type__in=[cls.RoleType.MANAGER, cls.RoleType.RISK_MANAGER])
125
+ )
126
+
127
+ if include_superuser:
128
+ return person.user_account.is_superuser or manager.exists()
129
+ return manager.exists()
130
+
131
+ @classmethod
132
+ def is_analyst(cls, person, instrument=None, portfolio=None, include_superuser=True):
133
+ """Determines whether a person can access analyst things (or is a superuser/manager/portfolio manager)
134
+
135
+ Arguments:
136
+ person {crm.Person} -- The person to be checked
137
+
138
+ Returns:
139
+ bool -- Can manage or not
140
+ """
141
+ today = date.today()
142
+ manager = cls.objects.filter(
143
+ Q(person=person)
144
+ & (Q(start__isnull=True) | Q(start__lte=today))
145
+ & (Q(end__isnull=True) | Q(end__gte=today))
146
+ )
147
+
148
+ if instrument:
149
+ manager = manager.filter(
150
+ Q(instrument=instrument)
151
+ | Q(instrument__isnull=True)
152
+ | Q(role_type__in=[cls.RoleType.PORTFOLIO_MANAGER, cls.RoleType.MANAGER, cls.RoleType.RISK_MANAGER])
153
+ )
154
+ elif portfolio:
155
+ manager = manager.filter(
156
+ Q(instrument__in=portfolio.instruments.all())
157
+ | Q(instrument__isnull=True)
158
+ | Q(role_type__in=[cls.RoleType.PORTFOLIO_MANAGER, cls.RoleType.MANAGER, cls.RoleType.RISK_MANAGER])
159
+ )
160
+ if include_superuser:
161
+ return person.user_account.is_superuser or manager.exists()
162
+ return manager.exists()
163
+
164
+ @classmethod
165
+ def portfolio_managers(cls):
166
+ today = date.today()
167
+ return (
168
+ cls.objects.filter(
169
+ Q(role_type=cls.RoleType.PORTFOLIO_MANAGER)
170
+ & (Q(start__isnull=True) | Q(start__lte=today))
171
+ & (Q(end__isnull=True) | Q(end__gte=today))
172
+ )
173
+ .values("person")
174
+ .distinct("person")
175
+ )
176
+
177
+ @classmethod
178
+ def get_endpoint_basename(cls):
179
+ return "wbportfolio:portfoliorole"
180
+
181
+
182
+ @receiver(deactivate_profile)
183
+ def handle_user_deactivation(sender, instance, substitute_profile=None, **kwargs):
184
+ deactivation_date = date.today() - timedelta(days=1)
185
+ for role in PortfolioRole.objects.filter(person=instance, end__isnull=True):
186
+ role.end = deactivation_date
187
+ role.save()
188
+ if (
189
+ substitute_profile
190
+ and not PortfolioRole.objects.filter(
191
+ person=substitute_profile, end__isnull=True, instrument=role.instrument
192
+ ).exists()
193
+ ):
194
+ PortfolioRole.objects.create(
195
+ instrument=role.instrument,
196
+ person=substitute_profile,
197
+ role_type=role.role_type,
198
+ start=deactivation_date,
199
+ weighting=role.weighting,
200
+ )
201
+
202
+
203
+ @receiver(pre_merge, sender="wbfdm.Instrument")
204
+ def pre_merge_instrument(sender: models.Model, merged_object: Instrument, main_object: Instrument, **kwargs):
205
+ """
206
+ Simply reassign the portfolio roles linked to the merged instrument to the main instrument
207
+ """
208
+ merged_object.portfolio_roles.update(instrument=main_object)
@@ -0,0 +1,3 @@
1
+ from .portfolio_synchronization import PortfolioSynchronization
2
+ from .price_computation import PriceComputation
3
+ from .synchronization import SynchronizationTask