wbportfolio 1.49.0__py2.py3-none-any.whl → 1.49.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 (25) hide show
  1. wbportfolio/factories/dividends.py +1 -0
  2. wbportfolio/import_export/handlers/dividend.py +1 -1
  3. wbportfolio/import_export/handlers/fees.py +1 -1
  4. wbportfolio/import_export/handlers/portfolio_cash_flow.py +1 -1
  5. wbportfolio/import_export/handlers/trade.py +5 -2
  6. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +1 -1
  7. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +1 -1
  8. wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py +126 -0
  9. wbportfolio/models/portfolio.py +7 -3
  10. wbportfolio/models/transactions/claim.py +8 -7
  11. wbportfolio/models/transactions/dividends.py +3 -20
  12. wbportfolio/models/transactions/trade_proposals.py +21 -18
  13. wbportfolio/models/transactions/trades.py +9 -12
  14. wbportfolio/models/transactions/transactions.py +37 -37
  15. wbportfolio/tests/models/test_portfolios.py +1 -1
  16. wbportfolio/viewsets/configs/display/assets.py +0 -11
  17. wbportfolio/viewsets/configs/display/products.py +0 -13
  18. wbportfolio/viewsets/configs/display/trades.py +1 -9
  19. wbportfolio/viewsets/configs/endpoints/transactions.py +6 -0
  20. wbportfolio/viewsets/portfolios.py +17 -1
  21. wbportfolio/viewsets/transactions/trades.py +20 -2
  22. {wbportfolio-1.49.0.dist-info → wbportfolio-1.49.1.dist-info}/METADATA +1 -1
  23. {wbportfolio-1.49.0.dist-info → wbportfolio-1.49.1.dist-info}/RECORD +25 -24
  24. {wbportfolio-1.49.0.dist-info → wbportfolio-1.49.1.dist-info}/WHEEL +0 -0
  25. {wbportfolio-1.49.0.dist-info → wbportfolio-1.49.1.dist-info}/licenses/LICENSE +0 -0
@@ -11,5 +11,6 @@ class DividendTransactionsFactory(TransactionFactory):
11
11
  class Meta:
12
12
  model = DividendTransaction
13
13
 
14
+ retrocession = 1.0
14
15
  shares = factory.LazyAttribute(lambda o: random.randint(10, 10000))
15
16
  price = factory.LazyAttribute(lambda o: random.randint(10, 10000))
@@ -23,7 +23,7 @@ class DividendImportHandler(ImportExportHandler):
23
23
  data["value_date"] = datetime.strptime(data["value_date"], "%Y-%m-%d").date()
24
24
  from wbportfolio.models import Portfolio
25
25
 
26
- data["portfolio"] = Portfolio.objects.get(id=data["portfolio"])
26
+ data["portfolio"] = Portfolio.all_objects.get(id=data["portfolio"])
27
27
  instrument = self.instrument_handler.process_object(
28
28
  data["underlying_instrument"], only_security=False, read_only=True
29
29
  )[0]
@@ -26,7 +26,7 @@ class FeesImportHandler(ImportExportHandler):
26
26
 
27
27
  data["linked_product"] = Product.objects.get(id=data["linked_product"])
28
28
  if "porfolio" in data:
29
- data["portfolio"] = Portfolio.objects.get(id=data["portfolio"])
29
+ data["portfolio"] = Portfolio.all_objects.get(id=data["portfolio"])
30
30
  else:
31
31
  data["portfolio"] = data["linked_product"].primary_portfolio
32
32
  data["underlying_instrument"] = Cash.objects.filter(currency=data["portfolio"].currency).first()
@@ -19,7 +19,7 @@ class DailyPortfolioCashFlowImportHandler(ImportExportHandler):
19
19
 
20
20
  def _deserialize(self, data):
21
21
  data["value_date"] = datetime.strptime(data["value_date"], "%Y-%m-%d").date()
22
- data["portfolio"] = Portfolio.objects.get(id=data["portfolio"])
22
+ data["portfolio"] = Portfolio.all_objects.get(id=data["portfolio"])
23
23
  if "cash" in data:
24
24
  data["cash"] = Decimal(data["cash"])
25
25
 
@@ -1,3 +1,4 @@
1
+ import math
1
2
  from datetime import datetime
2
3
  from decimal import Decimal
3
4
  from typing import Any, Dict, Optional
@@ -71,7 +72,10 @@ class TradeImportHandler(ImportExportHandler):
71
72
 
72
73
  for field in self.model._meta.get_fields():
73
74
  if not (value := data.get(field.name, None)) is None and isinstance(field, models.DecimalField):
74
- data[field.name] = Decimal(value)
75
+ q = (
76
+ 1 / (math.pow(10, 4))
77
+ ) # we need that convertion mechanism otherwise there is floating point approximation error while casting to decimal and get_instance does not work as expected
78
+ data[field.name] = Decimal(value).quantize(Decimal(str(q)))
75
79
 
76
80
  def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
77
81
  if "transaction_date" not in data: # we might get only book date and not transaction date
@@ -81,7 +85,6 @@ class TradeImportHandler(ImportExportHandler):
81
85
 
82
86
  def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
83
87
  self.import_source.log += "\nGet Trade Instance."
84
-
85
88
  if transaction_date := data.get("transaction_date"):
86
89
  dates_lookup = {"transaction_date": transaction_date}
87
90
  elif book_date := data.get("book_date"):
@@ -29,7 +29,7 @@ def get_portfolio_id(row: pd.Series) -> int:
29
29
  Raises: Portfolio.DoesNotExist: We raise an error intentionally if the portfolio does not exist to make the import fail
30
30
  """
31
31
  iban = str(IBAN.generate(BANK_COUNTRY_CODE, bank_code=BANK_CODE, account_code=str(row["account_number"])))
32
- return Portfolio.objects.get(bank_accounts__iban=iban).id
32
+ return Portfolio.all_objects.get(bank_accounts__iban=iban).id
33
33
 
34
34
 
35
35
  def parse(import_source: "ImportSource") -> dict:
@@ -13,7 +13,7 @@ def parse(import_source):
13
13
 
14
14
  def get_portfolio_id(row) -> int | None:
15
15
  with suppress(Portfolio.DoesNotExist):
16
- return Portfolio.objects.get(instruments__children__isin__in=[row["Isin"]]).pk
16
+ return Portfolio.all_objects.get(instruments__children__isin__in=[row["Isin"]]).pk
17
17
 
18
18
  df = df[~df["Isin"].isnull()]
19
19
  df["Trade date"] = df["Trade date"].ffill()
@@ -0,0 +1,126 @@
1
+ # Generated by Django 5.0.13 on 2025-03-21 12:30
2
+
3
+ import django.db.models.expressions
4
+ from decimal import Decimal
5
+ from django.db import migrations, models
6
+
7
+ def migrate_default_values(apps, schema_editor):
8
+ Transaction = apps.get_model("wbportfolio", "Transaction")
9
+ Trade = apps.get_model("wbportfolio", "Trade")
10
+ Expiry = apps.get_model("wbportfolio", "Expiry")
11
+ DividendTransaction = apps.get_model("wbportfolio", "DividendTransaction")
12
+ Transaction.objects.filter(book_date__isnull=True).update(book_date=models.F("transaction_date"))
13
+ Transaction.objects.filter(value_date__isnull=True).update(value_date=models.F("transaction_date"))
14
+ Trade.objects.filter(price_gross__isnull=True).update(price_gross=models.F("price"))
15
+ Expiry.objects.filter(price_gross__isnull=True).update(price_gross=models.F("price"))
16
+ DividendTransaction.objects.filter(price_gross__isnull=True).update(price_gross=models.F("price"))
17
+ Transaction.objects.filter(total_value_gross__isnull=True).update(total_value_gross=models.F("total_value"))
18
+
19
+
20
+ class Migration(migrations.Migration):
21
+
22
+ dependencies = [
23
+ ('wbportfolio', '0075_portfolio_initial_position_date_and_more'),
24
+ ]
25
+
26
+ operations = [
27
+ migrations.RunPython(migrate_default_values),
28
+ migrations.AlterField(
29
+ model_name='transaction',
30
+ name='book_date',
31
+ field=models.DateField(help_text='The date that this transaction was booked.',
32
+ verbose_name='Trade Date'),
33
+ preserve_default=False,
34
+ ),
35
+ migrations.AlterField(
36
+ model_name='transaction',
37
+ name='value_date',
38
+ field=models.DateField(help_text='The date that this transaction was valuated.',
39
+ verbose_name='Value Date'),
40
+ preserve_default=False,
41
+ ),
42
+ migrations.AlterField(
43
+ model_name='dividendtransaction',
44
+ name='price',
45
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The price per share.', max_digits=16, verbose_name='Price'),
46
+ ),
47
+ migrations.AlterField(
48
+ model_name='dividendtransaction',
49
+ name='price_gross',
50
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The gross price per share.', max_digits=16, verbose_name='Gross Price'),
51
+ preserve_default=False,
52
+ ),
53
+ migrations.AlterField(
54
+ model_name='dividendtransaction',
55
+ name='shares',
56
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The number of shares that were traded.', max_digits=15, verbose_name='Shares'),
57
+ ),
58
+ migrations.AlterField(
59
+ model_name='expiry',
60
+ name='price',
61
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The price per share.', max_digits=16, verbose_name='Price'),
62
+ ),
63
+ migrations.AlterField(
64
+ model_name='expiry',
65
+ name='price_gross',
66
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The gross price per share.', max_digits=16, verbose_name='Gross Price'),
67
+ preserve_default=False,
68
+ ),
69
+ migrations.AlterField(
70
+ model_name='expiry',
71
+ name='shares',
72
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The number of shares that were traded.', max_digits=15, verbose_name='Shares'),
73
+ ),
74
+ migrations.AlterField(
75
+ model_name='trade',
76
+ name='price',
77
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The price per share.', max_digits=16, verbose_name='Price'),
78
+ ),
79
+ migrations.AlterField(
80
+ model_name='trade',
81
+ name='price_gross',
82
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The gross price per share.', max_digits=16, verbose_name='Gross Price'),
83
+ preserve_default=False,
84
+ ),
85
+ migrations.AlterField(
86
+ model_name='trade',
87
+ name='shares',
88
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The number of shares that were traded.', max_digits=15, verbose_name='Shares'),
89
+ ),
90
+
91
+ migrations.AlterField(
92
+ model_name='transaction',
93
+ name='total_value',
94
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), max_digits=20, verbose_name='Total Value'),
95
+ preserve_default=False,
96
+ ),
97
+ migrations.AlterField(
98
+ model_name='transaction',
99
+ name='total_value_gross',
100
+ field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), max_digits=20, verbose_name='Total Value Gross'),
101
+ preserve_default=False,
102
+ ),
103
+ migrations.RemoveField(
104
+ model_name='transaction',
105
+ name='total_value_fx_portfolio',
106
+ ),
107
+ migrations.RemoveField(
108
+ model_name='transaction',
109
+ name='total_value_gross_fx_portfolio',
110
+ ),
111
+ migrations.AddField(
112
+ model_name='transaction',
113
+ name='total_value_fx_portfolio',
114
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(
115
+ models.F('currency_fx_rate'), '*', models.F('total_value')),
116
+ output_field=models.DecimalField(decimal_places=4, max_digits=20)),
117
+ preserve_default=False,
118
+ ),
119
+ migrations.AddField(
120
+ model_name='transaction',
121
+ name='total_value_gross_fx_portfolio',
122
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('total_value_gross')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
123
+ preserve_default=False,
124
+ ),
125
+
126
+ ]
@@ -411,7 +411,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
411
411
  )
412
412
 
413
413
  def __str__(self):
414
- return f"{self.id:06} ({self.name})"
414
+ return f"{self.id:06}: {self.name}"
415
415
 
416
416
  class Meta:
417
417
  verbose_name = "Portfolio"
@@ -979,7 +979,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
979
979
  for position in positions:
980
980
  position.portfolio = self
981
981
  update_dates.add(position.date)
982
- # self.assets.filter(date__in=update_dates, is_estimated=True).delete()
982
+
983
+ # we need to delete the existing estimated portfolio because otherwise we risk to have existing and not
984
+ # overlapping positions remaining (as they will not be updating by the bulk create). E.g. when someone
985
+ # change completely the trades of a portfolio model and drift it.
986
+ self.assets.filter(date__in=update_dates, is_estimated=True).delete()
983
987
  leftover_positions_ids = list(
984
988
  self.assets.filter(date__in=update_dates).values_list("id", flat=True)
985
989
  ) # we need to get the ids otherwise the queryset is reevaluated later
@@ -1014,7 +1018,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
1014
1018
  @classmethod
1015
1019
  def _get_or_create_portfolio(cls, instrument_handler, portfolio_data):
1016
1020
  if isinstance(portfolio_data, int):
1017
- return Portfolio.objects.get(id=portfolio_data)
1021
+ return Portfolio.all_objects.get(id=portfolio_data)
1018
1022
  instrument = portfolio_data
1019
1023
  if isinstance(portfolio_data, dict):
1020
1024
  instrument = instrument_handler.process_object(instrument, only_security=False, read_only=True)[0]
@@ -14,6 +14,7 @@ from django.db.models import (
14
14
  )
15
15
  from django.db.models.functions import Greatest
16
16
  from django.dispatch import receiver
17
+ from django.utils.translation import gettext_lazy as _
17
18
  from django_fsm import FSMField, transition
18
19
  from wbcore.contrib.ai.llm.config import add_llm_prompt
19
20
  from wbcore.contrib.authentication.models import User
@@ -335,13 +336,13 @@ class Claim(ReferenceIDMixin, WBModel):
335
336
  errors = dict()
336
337
 
337
338
  if not self.trade:
338
- errors["trade"] = ["With this status, this has to be provided."]
339
+ errors["trade"] = [_("With this status, this has to be provided.")]
339
340
 
340
341
  if not self.product:
341
- errors["product"] = ["With this status, this has to be provided."]
342
+ errors["product"] = [_("With this status, this has to be provided.")]
342
343
 
343
344
  if not self.account:
344
- errors["account"] = ["With this status, this has to be provided."]
345
+ errors["account"] = [_("With this status, this has to be provided.")]
345
346
 
346
347
  # check if the specified product have a valid nav at the specified date
347
348
  if (
@@ -350,13 +351,13 @@ class Claim(ReferenceIDMixin, WBModel):
350
351
  and not product.valuations.filter(date=claim_date).exists()
351
352
  ):
352
353
  if (prices_qs := product.valuations.filter(date__lt=claim_date)).exists():
353
- errors["date"] = (
354
+ errors["date"] = [
354
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."
355
- )
356
+ ]
356
357
  else:
357
- errors["date"] = (
358
+ errors["date"] = [
358
359
  f"There is no valuation before {claim_date:%Y-%m-%d} for product {product.name}: Please select a valid date."
359
- )
360
+ ]
360
361
  return errors
361
362
 
362
363
  @transition(
@@ -1,3 +1,5 @@
1
+ from decimal import Decimal
2
+
1
3
  from django.db import models
2
4
 
3
5
  from wbportfolio.import_export.handlers.dividend import DividendImportHandler
@@ -10,23 +12,4 @@ class DividendTransaction(Transaction, ShareMixin, models.Model):
10
12
  retrocession = models.FloatField(default=1)
11
13
 
12
14
  def save(self, *args, **kwargs):
13
- if (
14
- self.shares is not None
15
- and self.price is not None
16
- and self.retrocession is not None
17
- and self.total_value is None
18
- ):
19
- self.total_value = self.shares * self.price * self.retrocession
20
-
21
- if self.price is not None and self.price_gross is None:
22
- self.price_gross = self.price
23
-
24
- if (
25
- self.price_gross is not None
26
- and self.retrocession is not None
27
- and self.shares is not None
28
- and self.total_value_gross is None
29
- ):
30
- self.total_value_gross = self.shares * self.price_gross * self.retrocession
31
-
32
- super().save(*args, **kwargs)
15
+ super().save(*args, factor=Decimal(self.retrocession), **kwargs)
@@ -8,6 +8,7 @@ from celery import shared_task
8
8
  from django.core.exceptions import ValidationError
9
9
  from django.db import models
10
10
  from django.utils.functional import cached_property
11
+ from django.utils.translation import gettext_lazy as _
11
12
  from django_fsm import FSMField, transition
12
13
  from pandas._libs.tslibs.offsets import BDay
13
14
  from wbcompliance.models.risk_management.mixins import RiskCheckMixin
@@ -228,8 +229,6 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
228
229
  """
229
230
  Will delete all existing trades and recreate them from the method `create_or_update_trades`
230
231
  """
231
- if self.status != TradeProposal.Status.DRAFT:
232
- raise ValueError("Cannot reset non-draft trade proposal. Revert this trade proposal first.")
233
232
  # delete all existing trades
234
233
  last_effective_date = self.last_effective_date
235
234
  # Get effective and target portfolio
@@ -303,7 +302,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
303
302
  )
304
303
  last_trade_proposal = overriding_trade_proposal or next_trade_proposal
305
304
 
306
- def get_estimated_shares(self, weight: Decimal, underlying_quote: Instrument) -> Decimal | None:
305
+ def get_estimated_shares(self, weight: Decimal, underlying_quote: Instrument) -> Decimal:
307
306
  """
308
307
  Estimates the number of shares for a trade based on the given weight and underlying quote.
309
308
 
@@ -332,8 +331,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
332
331
  if price_fx_portfolio:
333
332
  return trade_total_value_fx_portfolio / price_fx_portfolio
334
333
  except Exception:
335
- # Suppress any ValueError and return None if the calculation fails
336
- return None
334
+ raise ValueError("We couldn't estimate the number of shares")
337
335
 
338
336
  def get_estimated_target_cash(self, currency: Currency) -> tuple[Decimal, Decimal]:
339
337
  """
@@ -373,7 +371,8 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
373
371
  )[0]
374
372
 
375
373
  # Estimate the target shares for the cash component
376
- total_target_shares = self.get_estimated_shares(target_cash_weight, cash_component)
374
+ with suppress(ValueError):
375
+ total_target_shares = self.get_estimated_shares(target_cash_weight, cash_component)
377
376
 
378
377
  return target_cash_weight, total_target_shares
379
378
 
@@ -428,7 +427,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
428
427
  errors = dict()
429
428
  errors_list = []
430
429
  if self.trades.exists() and self.trades.exclude(status=Trade.Status.DRAFT).exists():
431
- errors_list.append("All trades need to be draft before submitting")
430
+ errors_list.append(_("All trades need to be draft before submitting"))
432
431
  service = self.validated_trading_service
433
432
  try:
434
433
  service.is_valid(ignore_error=True)
@@ -437,7 +436,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
437
436
  # "There is no change detected in this trade proposal. Please submit at last one valid trade"
438
437
  # )
439
438
  if len(service.validated_trades) == 0:
440
- errors_list.append("There is no valid trade on this proposal")
439
+ errors_list.append(_("There is no valid trade on this proposal"))
441
440
  if service.errors:
442
441
  errors_list.extend(service.errors)
443
442
  if errors_list:
@@ -487,15 +486,17 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
487
486
  def can_approve(self):
488
487
  errors = dict()
489
488
  if not self.portfolio.can_be_rebalanced:
490
- errors["non_field_errors"] = "The portfolio does not allow manual rebalanced"
489
+ errors["non_field_errors"] = [_("The portfolio does not allow manual rebalanced")]
491
490
  if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
492
- errors["non_field_errors"] = "At least one trade needs to be submitted to be able to approve this proposal"
491
+ errors["non_field_errors"] = [
492
+ _("At least one trade needs to be submitted to be able to approve this proposal")
493
+ ]
493
494
  if not self.portfolio.can_be_rebalanced:
494
- errors["portfolio"] = (
495
- "The portfolio needs to be a model portfolio in order to approve this trade proposal manually"
496
- )
495
+ errors["portfolio"] = [
496
+ [_("The portfolio needs to be a model portfolio in order to approve this trade proposal manually")]
497
+ ]
497
498
  if self.has_non_successful_checks:
498
- errors["non_field_errors"] = "The pre trades rules did not passed successfully"
499
+ errors["non_field_errors"] = [_("The pre trades rules did not passed successfully")]
499
500
  return errors
500
501
 
501
502
  @transition(
@@ -526,7 +527,9 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
526
527
  def can_deny(self):
527
528
  errors = dict()
528
529
  if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
529
- errors["non_field_errors"] = "At least one trade needs to be submitted to be able to deny this proposal"
530
+ errors["non_field_errors"] = [
531
+ _("At least one trade needs to be submitted to be able to deny this proposal")
532
+ ]
530
533
  return errors
531
534
 
532
535
  @transition(
@@ -589,9 +592,9 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
589
592
  def can_revert(self):
590
593
  errors = dict()
591
594
  if not self.portfolio.can_be_rebalanced:
592
- errors["portfolio"] = (
593
- "The portfolio needs to be a model portfolio in order to revert this trade proposal manually"
594
- )
595
+ errors["portfolio"] = [
596
+ _("The portfolio needs to be a model portfolio in order to revert this trade proposal manually")
597
+ ]
595
598
  return errors
596
599
 
597
600
  # End FSM logics
@@ -20,6 +20,7 @@ from django.db.models.functions import Coalesce
20
20
  from django.db.models.signals import post_save
21
21
  from django.dispatch import receiver
22
22
  from django.utils.functional import cached_property
23
+ from django.utils.translation import gettext_lazy as _
23
24
  from django_fsm import GET_STATE, FSMField, transition
24
25
  from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet
25
26
  from wbcore.contrib.icons import WBIcon
@@ -320,9 +321,11 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
320
321
 
321
322
  def can_execute(self):
322
323
  if not self.last_underlying_quote_price:
323
- return {"underlying_instrument": "Cannot execute a trade without a valid quote price"}
324
+ return {"underlying_instrument": [_("Cannot execute a trade without a valid quote price")]}
324
325
  if not self.portfolio.is_manageable:
325
- return {"portfolio": "The portfolio needs to be a model portfolio in order to execute this trade manually"}
326
+ return {
327
+ "portfolio": [_("The portfolio needs to be a model portfolio in order to execute this trade manually")]
328
+ }
326
329
 
327
330
  @transition(
328
331
  field=status,
@@ -498,22 +501,16 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
498
501
  self.transaction_date = self.trade_proposal.trade_date
499
502
  self.value_date = self.trade_proposal.last_effective_date
500
503
  if not self.portfolio.only_weighting:
501
- self.shares = self.trade_proposal.get_estimated_shares(self.weighting, self.underlying_instrument)
504
+ with suppress(ValueError):
505
+ self.shares = self.trade_proposal.get_estimated_shares(self.weighting, self.underlying_instrument)
502
506
 
503
507
  if not self.custodian and self.bank:
504
508
  self.custodian = Custodian.get_by_mapping(self.bank)
505
509
  if self.price is None:
506
510
  # we try to get the price if not provided directly from the underlying instrument
507
- with suppress(InstrumentPrice.DoesNotExist):
508
- self.price = self.underlying_instrument.valuations.get(date=self.value_date).net_value
509
- if self.price is not None and self.price_gross is None:
510
- self.price_gross = self.price
511
-
512
- if self.price is not None and self.shares is not None and self.total_value is None:
513
- self.total_value = self.price * self.shares
511
+ with suppress(Exception):
512
+ self.price = self.underlying_instrument.get_price(self.value_date)
514
513
 
515
- if self.price_gross is not None and self.shares is not None and self.total_value_gross is None:
516
- self.total_value_gross = self.price_gross * self.shares
517
514
  self.transaction_type = Transaction.Type.TRADE
518
515
 
519
516
  if self.transaction_subtype is None or self.trade_proposal:
@@ -13,16 +13,16 @@ class ShareMixin(models.Model):
13
13
  shares = models.DecimalField(
14
14
  max_digits=15,
15
15
  decimal_places=4,
16
- null=True,
17
- blank=True,
16
+ default=Decimal("0.0"),
18
17
  help_text="The number of shares that were traded.",
19
18
  verbose_name="Shares",
20
19
  )
21
20
  price = models.DecimalField(
22
21
  max_digits=16,
23
22
  decimal_places=4,
24
- null=True,
25
- blank=True,
23
+ default=Decimal(
24
+ "0.0"
25
+ ), # we shouldn't default to anything but we have trade with price=None. Needs to be handled carefully
26
26
  help_text="The price per share.",
27
27
  verbose_name="Price",
28
28
  )
@@ -30,12 +30,23 @@ class ShareMixin(models.Model):
30
30
  price_gross = models.DecimalField(
31
31
  max_digits=16,
32
32
  decimal_places=4,
33
- null=True,
34
- blank=True,
35
33
  help_text="The gross price per share.",
36
34
  verbose_name="Gross Price",
37
35
  )
38
36
 
37
+ def save(
38
+ self,
39
+ *args,
40
+ factor: Decimal = Decimal("1.0"),
41
+ **kwargs,
42
+ ):
43
+ if self.price_gross is None:
44
+ self.price_gross = self.price
45
+
46
+ self.total_value = self.price * self.shares * factor
47
+ self.total_value_gross = self.price_gross * self.shares * factor
48
+ super().save(*args, **kwargs)
49
+
39
50
  class Meta:
40
51
  abstract = True
41
52
 
@@ -68,14 +79,10 @@ class Transaction(ImportMixin, models.Model):
68
79
  help_text="The date that this transaction was traded.",
69
80
  )
70
81
  book_date = models.DateField(
71
- null=True,
72
- blank=True,
73
82
  verbose_name="Trade Date",
74
83
  help_text="The date that this transaction was booked.",
75
84
  )
76
85
  value_date = models.DateField(
77
- null=True,
78
- blank=True,
79
86
  verbose_name="Value Date",
80
87
  help_text="The date that this transaction was valuated.",
81
88
  )
@@ -89,17 +96,23 @@ class Transaction(ImportMixin, models.Model):
89
96
  currency_fx_rate = models.DecimalField(
90
97
  max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
91
98
  )
92
- total_value = models.DecimalField(
93
- max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value"
94
- )
95
- total_value_fx_portfolio = models.DecimalField(
96
- max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Fx Portfolio"
97
- )
98
- total_value_gross = models.DecimalField(
99
- max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Gross"
100
- )
101
- total_value_gross_fx_portfolio = models.DecimalField(
102
- max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Gross Fx Portfolio"
99
+ total_value = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value")
100
+ total_value_gross = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value Gross")
101
+ total_value_fx_portfolio = models.GeneratedField(
102
+ expression=models.F("currency_fx_rate") * models.F("total_value"),
103
+ output_field=models.DecimalField(
104
+ max_digits=20,
105
+ decimal_places=4,
106
+ ),
107
+ db_persist=True,
108
+ )
109
+ total_value_gross_fx_portfolio = models.GeneratedField(
110
+ expression=models.F("currency_fx_rate") * models.F("total_value_gross"),
111
+ output_field=models.DecimalField(
112
+ max_digits=20,
113
+ decimal_places=4,
114
+ ),
115
+ db_persist=True,
103
116
  )
104
117
  external_id = models.CharField(
105
118
  max_length=255,
@@ -118,28 +131,15 @@ class Transaction(ImportMixin, models.Model):
118
131
 
119
132
  if not getattr(self, "currency", None) and self.underlying_instrument:
120
133
  self.currency = self.underlying_instrument.currency
121
- if not self.currency_fx_rate:
134
+ if self.currency_fx_rate is None:
122
135
  self.currency_fx_rate = self.underlying_instrument.currency.convert(
123
136
  self.value_date, self.portfolio.currency, exact_lookup=True
124
137
  )
125
138
  if not self.transaction_type:
126
139
  self.transaction_type = self.__class__.__name__
127
- if (
128
- self.total_value is not None
129
- and self.currency_fx_rate is not None
130
- and self.total_value_fx_portfolio is None
131
- ):
132
- self.total_value_fx_portfolio = self.total_value * self.currency_fx_rate
133
-
134
- if self.total_value is not None and self.total_value_gross is None and self.total_value_gross is None:
135
- self.total_value_gross = self.total_value
136
140
 
137
- if (
138
- self.currency_fx_rate is not None
139
- and self.total_value_gross is not None
140
- and self.total_value_gross_fx_portfolio is None
141
- ):
142
- self.total_value_gross_fx_portfolio = self.total_value_gross * self.currency_fx_rate
141
+ if self.total_value_gross is None:
142
+ self.total_value_gross = self.total_value
143
143
 
144
144
  super().save(*args, **kwargs)
145
145
 
@@ -33,7 +33,7 @@ class TestPortfolioModel(PortfolioTestMixin):
33
33
  assert portfolio.id is not None
34
34
 
35
35
  def test_str(self, portfolio):
36
- assert str(portfolio) == f"{portfolio.id:06} ({portfolio.name})"
36
+ assert str(portfolio) == f"{portfolio.id:06}: {portfolio.name}"
37
37
 
38
38
  def test_get_assets(self, portfolio, product, cash, asset_position_factory):
39
39
  asset_position_factory.create_batch(4, portfolio=portfolio, underlying_instrument=product)
@@ -148,17 +148,6 @@ class AssetPositionInstrumentDisplayConfig(DisplayViewConfig):
148
148
  items=[dp.LegendItem(icon=WBIcon.UNFILTER.icon, label="Not Invested", value=True)],
149
149
  )
150
150
  ],
151
- formatting=[
152
- dp.Formatting(
153
- column="is_invested",
154
- formatting_rules=[
155
- dp.FormattingRule(
156
- icon=WBIcon.UNFILTER.icon,
157
- condition=("==", False),
158
- )
159
- ],
160
- )
161
- ],
162
151
  )
163
152
 
164
153
 
@@ -1,7 +1,6 @@
1
1
  from typing import Optional
2
2
 
3
3
  from django.utils.translation import gettext_lazy as _
4
- from wbcore.contrib.icons import WBIcon
5
4
  from wbcore.enums import Unit
6
5
  from wbcore.metadata.configs import display as dp
7
6
  from wbcore.metadata.configs.display.formatting import Condition, Operator
@@ -27,18 +26,6 @@ class ProductDisplayConfig(DisplayViewConfig):
27
26
  label=_("Information"),
28
27
  open_by_default=False,
29
28
  children=[
30
- dp.Field(
31
- key="is_invested",
32
- label="",
33
- formatting_rules=[
34
- dp.FormattingRule(
35
- icon=WBIcon.UNFILTER.icon,
36
- condition=Condition(Operator("=="), False),
37
- ),
38
- ],
39
- width=30,
40
- show="open",
41
- ),
42
29
  dp.Field(key="name_repr", label="Name", width=250),
43
30
  dp.Field(key="parent", label="Parent"),
44
31
  dp.Field(key="isin", label="ISIN"),
@@ -267,15 +267,6 @@ class SubscriptionRedemptionDisplayConfig(TradeDisplayConfig):
267
267
  )
268
268
  ],
269
269
  ),
270
- dp.Formatting(
271
- column="pending",
272
- formatting_rules=[
273
- dp.FormattingRule(
274
- icon=WBIcon.FOLDERS_ADD.icon,
275
- condition=("==", True),
276
- )
277
- ],
278
- ),
279
270
  ],
280
271
  )
281
272
 
@@ -311,6 +302,7 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
311
302
  dp.Field(
312
303
  key="underlying_instrument_refinitiv_identifier_code", label="RIC", width=Unit.PIXEL(100)
313
304
  ),
305
+ dp.Field(key="underlying_instrument_instrument_type", label="Asset Class", width=Unit.PIXEL(125)),
314
306
  ],
315
307
  ),
316
308
  dp.Field(
@@ -3,6 +3,12 @@ from wbcore.metadata.configs.endpoints import EndpointViewConfig
3
3
 
4
4
 
5
5
  class TransactionEndpointConfig(EndpointViewConfig):
6
+ def get_endpoint(self, **kwargs):
7
+ return None
8
+
9
+ def get_list_endpoint(self, **kwargs):
10
+ return reverse("wbportfolio:transaction-list", request=self.request)
11
+
6
12
  def get_instance_endpoint(self, **kwargs):
7
13
  model = "{{transaction_url_type}}"
8
14
  return f"{self.request.scheme}://{self.request.get_host()}/api/portfolio/{model}/"
@@ -67,7 +67,23 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
67
67
  queryset = Portfolio.objects.all()
68
68
 
69
69
  search_fields = ("currency__key", "name")
70
- ordering_fields = search_fields
70
+ ordering_fields = (
71
+ "name",
72
+ "currency",
73
+ "hedged_currency",
74
+ "updated_at",
75
+ "initial_position_date",
76
+ "last_position_date",
77
+ "last_asset_under_management_usd",
78
+ "last_positions",
79
+ "automatic_rebalancer",
80
+ "last_trade_proposal_date",
81
+ "is_manageable",
82
+ "is_tracked",
83
+ "only_weighting",
84
+ "is_lookthrough",
85
+ "is_composition",
86
+ )
71
87
  ordering = ["name"]
72
88
 
73
89
  display_config_class = PortfolioDisplayConfig
@@ -367,7 +367,19 @@ class TradeTradeProposalModelViewSet(
367
367
  "trade_proposal",
368
368
  "order",
369
369
  )
370
- ordering_fields = ("target_weight", "effective_weight", "effective_shares", "target_shares", "shares", "weighting")
370
+ ordering_fields = (
371
+ "underlying_instrument__name",
372
+ "underlying_instrument_isin",
373
+ "underlying_instrument_ticker",
374
+ "underlying_instrument_refinitiv_identifier_code",
375
+ "underlying_instrument_instrument_type",
376
+ "target_weight",
377
+ "effective_weight",
378
+ "effective_shares",
379
+ "target_shares",
380
+ "shares",
381
+ "weighting",
382
+ )
371
383
  IDENTIFIER = "wbportfolio:tradeproposal"
372
384
  search_fields = ("underlying_instrument__name",)
373
385
  filterset_fields = {}
@@ -497,5 +509,11 @@ class TradeTradeProposalModelViewSet(
497
509
  underlying_instrument_isin=F("underlying_instrument__isin"),
498
510
  underlying_instrument_ticker=F("underlying_instrument__ticker"),
499
511
  underlying_instrument_refinitiv_identifier_code=F("underlying_instrument__refinitiv_identifier_code"),
500
- underlying_instrument_instrument_type=F("underlying_instrument__instrument_type__short_name"),
512
+ underlying_instrument_instrument_type=Case(
513
+ When(
514
+ underlying_instrument__parent__is_security=True,
515
+ then=F("underlying_instrument__parent__instrument_type__short_name"),
516
+ ),
517
+ default=F("underlying_instrument__instrument_type__short_name"),
518
+ ),
501
519
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.49.0
3
+ Version: 1.49.1
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -58,7 +58,7 @@ wbportfolio/factories/adjustments.py,sha256=aOHXJLy7DW4k1hcRBZteEe2mKe_wQg7z0bVn
58
58
  wbportfolio/factories/assets.py,sha256=1i7xDT6bpvnYQiEJpmukWadSvS1asQC-QuUJ1PCll0U,3173
59
59
  wbportfolio/factories/claim.py,sha256=LQzz8EbIveOYpMY-JOMR8ba-taoX3u53zjBwxiydtrM,1629
60
60
  wbportfolio/factories/custodians.py,sha256=XrNcpNhE4Rbv0y159E_MbP1TAQwe9POab-_RHOHy1Uk,311
61
- wbportfolio/factories/dividends.py,sha256=QqZy-Spdjv9I-dnFAuJUaWFZ5uEuWP-bkZMZ_RMuy64,381
61
+ wbportfolio/factories/dividends.py,sha256=ET2kp0A7U1x8WhNVUn6FM1GF4jfr5NwdULBiIQkSLh0,404
62
62
  wbportfolio/factories/fees.py,sha256=2B2ebj06a0ZI1ra2-TXtW5e0aQ9sXT-Plw6CzV-3_1c,400
63
63
  wbportfolio/factories/indexes.py,sha256=sInyNARyfUNbScxr_ng1HhxtJXIsK3awFf9BF1gr9zU,440
64
64
  wbportfolio/factories/portfolio_cash_flow.py,sha256=e8LWkk5Gy0FU4HoCL9xPseCtLlVIkMrvAmey5swqcfc,727
@@ -111,11 +111,11 @@ wbportfolio/import_export/backends/wbfdm/mixin.py,sha256=JNtjgqGLson1nu_Chqb8MWy
111
111
  wbportfolio/import_export/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
112
112
  wbportfolio/import_export/handlers/adjustment.py,sha256=6bdTIYFmc8_HFxcdwtnYwglMyCfAD8XrTIrEb2zWY0g,1757
113
113
  wbportfolio/import_export/handlers/asset_position.py,sha256=5wFnHcbq_zGp9rBUec_JEpzjCA0_v17VrV9F8Ps1ETs,8645
114
- wbportfolio/import_export/handlers/dividend.py,sha256=tftdVdAzNpKSSvouOtvJfzWL362HUPIC94F6Noha8CE,3998
115
- wbportfolio/import_export/handlers/fees.py,sha256=XYH752IkNGYhhhwatp8nYa1zG1-YZFDkYW15dyQgOIg,2824
116
- wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=2ODaquuC83RfmNwmQ-8TdhiASObfIems_B1g0yqaYTs,2733
114
+ wbportfolio/import_export/handlers/dividend.py,sha256=aIayzCn0n3h17_uHcNMLQK_QndRJSU5FHaEewD8jH9U,4002
115
+ wbportfolio/import_export/handlers/fees.py,sha256=phNZ3zc_RKxryj0IE81HUdiFJ8AFiWnVyZ_xtB_x3xY,2828
116
+ wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=W7QPNqEvvsq0RS016EAFBp1ezvc6G9Rk-hviRZh8o6Y,2737
117
117
  wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
118
- wbportfolio/import_export/handlers/trade.py,sha256=KMYnaEUGWyrft0l7OuPKHcy6WKdIr8I_wefoA8pzgjY,11297
118
+ wbportfolio/import_export/handlers/trade.py,sha256=Vp3nkeg5VvK3oTOiI0V8e0aBg5B2MySypuRx_fMd1VM,11575
119
119
  wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
120
  wbportfolio/import_export/parsers/default_mapping.py,sha256=KrO-X5CvQCeQoBYzFDxavoQGriyUSeI2QDx5ar_zo7A,1405
121
121
  wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -145,7 +145,7 @@ wbportfolio/import_export/parsers/natixis/valuation.py,sha256=mLjIw1GBlPPlzHJkxg
145
145
  wbportfolio/import_export/parsers/refinitiv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
146
  wbportfolio/import_export/parsers/refinitiv/adjustment.py,sha256=64AQoLOZQWn5HCLpflJ4OgQB3gCB3w_6Y4YLhcmuClg,819
147
147
  wbportfolio/import_export/parsers/sg_lux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
148
- wbportfolio/import_export/parsers/sg_lux/custodian_positions.py,sha256=pdrVQcehF1xF5fJCom78Y5kjtjYUCTM_qQ0Cjanl83Y,2479
148
+ wbportfolio/import_export/parsers/sg_lux/custodian_positions.py,sha256=hl-LqJbwojQOe0zGEH7W31i4hNTVme5L7nwXwyk6XBY,2483
149
149
  wbportfolio/import_export/parsers/sg_lux/customer_trade.py,sha256=gTEUIaxlZvXXCgYyg9FD3D3aQhffhruYmeZlaAghrK8,2617
150
150
  wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py,sha256=4KSxUGyflf3kY5BVJqMAF1aFkeSWEJegfDtLiA1BM6c,5065
151
151
  wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py,sha256=24e-vonaBAkvX-F_Gw4hLg4FBjgbpCTUbiFC_dZ-czw,2958
@@ -154,7 +154,7 @@ wbportfolio/import_export/parsers/sg_lux/equity.py,sha256=w13LAMNWReZzqmdFyXdazV
154
154
  wbportfolio/import_export/parsers/sg_lux/fees.py,sha256=xyPO9sYQyJW5w-1XhUMizY2RbH1Pr4n7LAUE-E67Yj4,1911
155
155
  wbportfolio/import_export/parsers/sg_lux/perf_fees.py,sha256=lH1gyswfefwo3Lxi0syydhJYl9EDGsrL_a6fLmpHh8w,1691
156
156
  wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py,sha256=ftLKWukUl9AcGWHQmJZnShVZpkO4rOl04PGlLcalDdQ,889
157
- wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py,sha256=wm0yTgCVYRw738y9DPRUP0ygIwCLHa29pbNzd6EUNjg,1070
157
+ wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py,sha256=2kkEOwZz1hwiPB9rlMp_vkv5azMrJUfyWl7GMaAwGUc,1074
158
158
  wbportfolio/import_export/parsers/sg_lux/registers.py,sha256=5Zaum-6XuQL8-p1MkwA0sN3z8MCymSz4nNzB8MPQDCA,6112
159
159
  wbportfolio/import_export/parsers/sg_lux/sylk.py,sha256=1ThyjneFFe_gNLDHeXQFdWIyanyktxm62P-Ta5Sm1v4,7656
160
160
  wbportfolio/import_export/parsers/sg_lux/utils.py,sha256=YzYIEDQ3CpkOuRWh2eEFRm-0SERu3SRAmx0nnqQS_og,1301
@@ -241,6 +241,7 @@ wbportfolio/migrations/0072_trade_diff_shares.py,sha256=aTKa1SbIiwmlXaFtBg-ENrSx
241
241
  wbportfolio/migrations/0073_remove_product_price_computation_and_more.py,sha256=J4puisDFwnbnfv2VLWaiCQ7ost6PCOkin9qKVQoLIWM,18725
242
242
  wbportfolio/migrations/0074_alter_rebalancer_frequency_and_more.py,sha256=o01rBj-ADgwCRtAai3e5z27alPGEzaiNxUqCwWm6peY,918
243
243
  wbportfolio/migrations/0075_portfolio_initial_position_date_and_more.py,sha256=rXRpsraVlmueAlO2UpBZV4qMf7dtPuptrhfLblZcJDo,1099
244
+ wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py,sha256=4g3ok79nw8mTAxHFAqBAdpGKetaPAjv06YSywywt4aU,6106
244
245
  wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
245
246
  wbportfolio/models/__init__.py,sha256=IIS_PNRxyX2Dcvyk1bcQOUzFt0B9SPC0WlM88CXqj04,881
246
247
  wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
@@ -248,7 +249,7 @@ wbportfolio/models/asset.py,sha256=yf4vBPfN1eZlWXG9SowQwr5-goG8rO1yYDitHDLZCBs,3
248
249
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
249
250
  wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
250
251
  wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
251
- wbportfolio/models/portfolio.py,sha256=dCLrGVIKqpIdQi78CF58S0GD7sSuzuHjpavKBZgTCm4,58775
252
+ wbportfolio/models/portfolio.py,sha256=cW27PFaQDpdd-UxHNgJ6sUBzit013-Rkr2J3aULoITo,59086
252
253
  wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
253
254
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
254
255
  wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
@@ -270,14 +271,14 @@ wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7h
270
271
  wbportfolio/models/reconciliations/account_reconciliations.py,sha256=_teOTNePxua9C894auy8CvdOFsmUnQiOM2_TZAHvzY4,3955
271
272
  wbportfolio/models/reconciliations/reconciliations.py,sha256=kF-BNhUoT4TCn1RIgPSkdEk1iX4NQeZlGGFd_ZulAZU,686
272
273
  wbportfolio/models/transactions/__init__.py,sha256=R-4fHylrUf3773kGSEt09vtYj3LUlia5yf7rxHXjvHA,295
273
- wbportfolio/models/transactions/claim.py,sha256=agdpGqxpO0FSzYDWV-Gv1tQY46k0LN9CY6hpqvnhPH8,25826
274
- wbportfolio/models/transactions/dividends.py,sha256=naL5xeDQfUBf5KyGt7y-tTcHL22nzZumT8DV6AaG8Bg,1064
274
+ wbportfolio/models/transactions/claim.py,sha256=hcx_tJ9luf2-s1qqsUZXtoDEuxFyty2A1rXSDkljoQo,25890
275
+ wbportfolio/models/transactions/dividends.py,sha256=92-jG8bZN9nU9oDubpu-UDH43Ri7kVjhqE_esOSmOzo,471
275
276
  wbportfolio/models/transactions/expiry.py,sha256=vnNHdcC1hf2HP4rAbmoGgOfagBYKNFytqOwzOI0MlVI,144
276
277
  wbportfolio/models/transactions/fees.py,sha256=ffvqo8I4A0l5rLi00jJ6sGot0jmnkoxaNsbDzdPLwCg,5712
277
278
  wbportfolio/models/transactions/rebalancing.py,sha256=nrBi6x6PssCtkLtOpV-2OoAHDUKnnYyrM6xH1PmnjSo,7240
278
- wbportfolio/models/transactions/trade_proposals.py,sha256=iZb0LHqWSqbWf3Rmyhb5igGGKoT3JN52O2Db8j3J_1g,26727
279
- wbportfolio/models/transactions/trades.py,sha256=zoqWsDpbUKbvdW60ytNg27kRFlH1N0rUz6mf0o6DdR4,28337
280
- wbportfolio/models/transactions/transactions.py,sha256=4THsE4xqdigZAwWKYfTNRLPJlkmAmsgE70Ribp9Lnrk,7127
279
+ wbportfolio/models/transactions/trade_proposals.py,sha256=CMeo_5KM58j1VU8bgA_r2uhvi3z6X_jqey4WKzwN8_E,26724
280
+ wbportfolio/models/transactions/trades.py,sha256=P9cSTEpZBsqKBvw25XY_RNctTiOBEzCvcuy0r8IkedE,28013
281
+ wbportfolio/models/transactions/transactions.py,sha256=fWoDf0TSV0L0gLUDOQpCRLzjMt1H4MUvUHGEaMsilCc,7027
281
282
  wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
282
283
  wbportfolio/pms/typing.py,sha256=b2pBWYt1E8ok-Kqm0lEFIakSnWJ6Ib57z-VX3C3gkQc,6081
283
284
  wbportfolio/pms/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -373,7 +374,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
373
374
  wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
374
375
  wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
375
376
  wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
376
- wbportfolio/tests/models/test_portfolios.py,sha256=DbOanYdyLEfnHqgmz0jckxnJRo4msiaaDjKWD4dQy4o,52208
377
+ wbportfolio/tests/models/test_portfolios.py,sha256=IxJy0fLkUx3m4qKzvkqixYl_0lR7TU-cQJ_DhTkh8V8,52207
377
378
  wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
378
379
  wbportfolio/tests/models/test_products.py,sha256=nBEgyUoY-4F_pfHYnAr7KXdNYvdIkSu-PWJrqp5tPHg,9482
379
380
  wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
@@ -408,7 +409,7 @@ wbportfolio/viewsets/portfolio_cash_flow.py,sha256=jkBfdZRQ3KsxGMJpltRjmdrZ2qEFJ
408
409
  wbportfolio/viewsets/portfolio_cash_targets.py,sha256=CvHlrDE8qnnnfRpTYnFu-Uu15MDbF5d5gTmEKth2S24,322
409
410
  wbportfolio/viewsets/portfolio_relationship.py,sha256=RGyvxd8NfFEs8YdqEvVD3VbrISvAO5UtCTlocSIuWQw,2109
410
411
  wbportfolio/viewsets/portfolio_swing_pricing.py,sha256=-57l3WLQZRslIV67OT0ucHE5JXTtTtLvd3t7MppdVn8,357
411
- wbportfolio/viewsets/portfolios.py,sha256=eUty1gVN0hLKMmW0IGe_l8u6CFSTJW9ruSPlZbZnU7E,14050
412
+ wbportfolio/viewsets/portfolios.py,sha256=1POzE9jrt2iLVMnIY_BWDr0A_zpOlO3Z8tM80TaXkhk,14454
412
413
  wbportfolio/viewsets/positions.py,sha256=MDf_0x9La2qE6qjaIqBtfV5VC0RfJ1chZIim45Emk10,13198
413
414
  wbportfolio/viewsets/product_groups.py,sha256=YvmuXPPy98K1J_rz6YPsx9gNK-tCS2P-wc1uRYgfyo0,2399
414
415
  wbportfolio/viewsets/product_performance.py,sha256=dRfRgifjGS1RgZSu9uJRM0SmB7eLnNUkPuqARMO4gyo,28371
@@ -436,7 +437,7 @@ wbportfolio/viewsets/configs/buttons/trade_proposals.py,sha256=eZBfYk5ZhancCVcu7
436
437
  wbportfolio/viewsets/configs/buttons/trades.py,sha256=X2B1l0iEIdHb3ZMf9rLVoiX_H8lSyLD12wopgumOIX4,2318
437
438
  wbportfolio/viewsets/configs/display/__init__.py,sha256=SmazY-YEp-Xf8G08Uz1-CzePZRCRtHrziRMIYYIGpCk,2176
438
439
  wbportfolio/viewsets/configs/display/adjustments.py,sha256=jIOEc23OCYBguLaZRlZxC916kocYT35ZV9Jsiocs9nk,3334
439
- wbportfolio/viewsets/configs/display/assets.py,sha256=8yLAXyrZLl7L1m1rhBnkh4uaqVpSQJhRVkcM7_AHBsc,10943
440
+ wbportfolio/viewsets/configs/display/assets.py,sha256=SkWwzvrn4gOVbCThGJMV63V-iJh517qfUs4f4Wt3dBA,10573
440
441
  wbportfolio/viewsets/configs/display/claim.py,sha256=MRtEdxWjsDjt1k6O-ltM-SpPmHWBk85sCycsRgzd9jI,12041
441
442
  wbportfolio/viewsets/configs/display/custodians.py,sha256=R-tnktfY48K-8Zz-Fg_M6IXKHv42J1ZeSpom7lvYVS8,801
442
443
  wbportfolio/viewsets/configs/display/esg.py,sha256=W8uetCPN2TjHtU2kvQjKOmkq7uoaYvzX2iLN6Nz78Pk,3111
@@ -447,13 +448,13 @@ wbportfolio/viewsets/configs/display/portfolios.py,sha256=ke60jc2yUjEHjkyt4yruM3
447
448
  wbportfolio/viewsets/configs/display/positions.py,sha256=yolWLxzGPIpSQSiVhVQChURqbomPt5kSjkYrmXT1Mik,3123
448
449
  wbportfolio/viewsets/configs/display/product_groups.py,sha256=PwI-A0_ofShT2pub9-C1HqreiqpHxKMHd51JYwEzvbM,2500
449
450
  wbportfolio/viewsets/configs/display/product_performance.py,sha256=6Mme48JBn_okwClR44dBK2OK26ejvdasDvBa5DI33_0,10070
450
- wbportfolio/viewsets/configs/display/products.py,sha256=KRDAaDIBYFkC4mYgiBR0_7E-jK8DwxjNsQuUjf_p_GE,11251
451
+ wbportfolio/viewsets/configs/display/products.py,sha256=WraPeMIS87Vd3wstA4Vt3zi41vgLUxK5eoqCIQLyr8k,10679
451
452
  wbportfolio/viewsets/configs/display/rebalancing.py,sha256=yw9X1Nf2-V_KP_mCX4pVKnJSjSstHMoocMkyO73KpkU,1112
452
453
  wbportfolio/viewsets/configs/display/reconciliations.py,sha256=YvMAuwmpX0HExvGsuf5UvcRQxe4eMo1iyNJX68GGC_k,6021
453
454
  wbportfolio/viewsets/configs/display/registers.py,sha256=1np75exIk5rfct6UkVN_RnfJ9ozvIkcWJgFV4_4rJns,3182
454
455
  wbportfolio/viewsets/configs/display/roles.py,sha256=SFUyCdxSlHZ3NsMrJmpVBSlg-XKGaEFteV89nyLMMAQ,1815
455
456
  wbportfolio/viewsets/configs/display/trade_proposals.py,sha256=sRLSUjKlarBhnTwg7tX_Juldor3beswJlyvZfFPvNEk,4315
456
- wbportfolio/viewsets/configs/display/trades.py,sha256=_J60o7s1gQ-2Jg3JvWLDWgfsobIy60kzYWdJ8ea0Fn8,16823
457
+ wbportfolio/viewsets/configs/display/trades.py,sha256=3rvIYMCe3IOyDw5HTrXcPvstdDg6Zt13oskh1yKQyRY,16613
457
458
  wbportfolio/viewsets/configs/display/transactions.py,sha256=DOM3eV1DxBwX6Iiw3C2sJamWh6A_3ZSYC9447Jc3Wmo,2586
458
459
  wbportfolio/viewsets/configs/endpoints/__init__.py,sha256=E13AYY3CIW4CZtmqwBVMPDYA5zNyKJeRZtiXKtad68Y,2871
459
460
  wbportfolio/viewsets/configs/endpoints/adjustments.py,sha256=9CcnfNuFcxsZ8YvfUitSeyCvpLxY-jU-gIw3GG0mIp4,697
@@ -473,7 +474,7 @@ wbportfolio/viewsets/configs/endpoints/reconciliations.py,sha256=xriq1KSctxQa6AE
473
474
  wbportfolio/viewsets/configs/endpoints/roles.py,sha256=5Nwi2sdUYc85YL0wJ7SUtt87ZBTOVdFnEexUgVrmcX4,367
474
475
  wbportfolio/viewsets/configs/endpoints/trade_proposals.py,sha256=sno7SOHWjKvvF_YdgYx5KthntM--F3DX-9hsrY5vRTM,783
475
476
  wbportfolio/viewsets/configs/endpoints/trades.py,sha256=5NKMSYaGWDd8OcV16HLRSlAgYYXObLikLf2w4ix2DoE,3483
476
- wbportfolio/viewsets/configs/endpoints/transactions.py,sha256=HQDuQ_wrnkCGTNhnx1zKsPifcDCnlpaOD2NYYiNYd54,633
477
+ wbportfolio/viewsets/configs/endpoints/transactions.py,sha256=6u2GWm67o4g1PoAtZXLdLyu3J4qhyOA1dm9Otu1i9f8,813
477
478
  wbportfolio/viewsets/configs/menu/__init__.py,sha256=gzUpv5f8FAK1Bdhn0AhcMLJR3r9RXHRXcHg21vuvGxs,1184
478
479
  wbportfolio/viewsets/configs/menu/adjustments.py,sha256=sQ25Jp2N8nsLSUOEJTJUBE0Y7bIPHK4Y6LE0e1OzX38,316
479
480
  wbportfolio/viewsets/configs/menu/assets.py,sha256=FkddF1_mu01e1mnkJkXPQcE0izI0Q3wU7VBFIkeT-Qw,340
@@ -518,9 +519,9 @@ wbportfolio/viewsets/transactions/fees.py,sha256=7VUXIogmRrXCz_D9tvDiiTae0t5j09W
518
519
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
519
520
  wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
520
521
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=nZsomopPfeLg1owPpOdIbhdHfCFa3-Qh-uyqoSATOjw,5821
521
- wbportfolio/viewsets/transactions/trades.py,sha256=2w6c5ExZNmyMBGTWR3_3SEuuJ727AjKr_HgZE3sDt1Y,20950
522
+ wbportfolio/viewsets/transactions/trades.py,sha256=D5-Qk8_OJze_6aNqeMCNjgcAz5B9dIP2M_8m_PCAtXI,21475
522
523
  wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
523
- wbportfolio-1.49.0.dist-info/METADATA,sha256=4ie6HSvXOoA_xHCciE716QeJG61w5Y93BDzIYvuOeVw,734
524
- wbportfolio-1.49.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
525
- wbportfolio-1.49.0.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
526
- wbportfolio-1.49.0.dist-info/RECORD,,
524
+ wbportfolio-1.49.1.dist-info/METADATA,sha256=HuQ5d8oOm9My1liVRK4vyUE6Z3ZbSlmiNHPloBz_5Yg,734
525
+ wbportfolio-1.49.1.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
526
+ wbportfolio-1.49.1.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
527
+ wbportfolio-1.49.1.dist-info/RECORD,,