Skip to content

Commit 43f1e27

Browse files
committed
NSDL support: first version
1 parent 6eaa108 commit 43f1e27

File tree

12 files changed

+2198
-67
lines changed

12 files changed

+2198
-67
lines changed

casparser/cli.py

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515

1616
from . import __version__, read_cas_pdf
1717
from .analysis.gains import CapitalGainsReport
18-
from .enums import CASFileType
18+
from .enums import CASFileType, FileType
1919
from .exceptions import GainsError, IncompleteCASError, ParserException
2020
from .parsers.utils import cas2csv, cas2csv_summary, cas2json, is_close
21-
from .types import CASData
21+
from .types import CASData, NSDLCASData
2222

2323
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
2424
console = Console()
@@ -55,6 +55,99 @@ def get_color(amount: Union[Decimal, float, int]):
5555
return "white"
5656

5757

58+
def print_nsdl(parsed_data: NSDLCASData):
59+
"""Print summary of parsed data."""
60+
61+
count = 0
62+
err = 0
63+
64+
data = parsed_data.model_dump(by_alias=True)
65+
# console.print(data)
66+
67+
summary_table = Table.grid(expand=True)
68+
summary_table.add_column(justify="right")
69+
summary_table.add_column(justify="left")
70+
spacing = (0, 1)
71+
summary_table.add_row(
72+
Padding("Statement Period :", spacing),
73+
f"[bold green]{data['statement_period']['from']}[/] To "
74+
f"[bold green]{data['statement_period']['to']}[/]",
75+
)
76+
summary_table.add_row(Padding("File Type :", spacing), f"[bold]{data['file_type']}[/]")
77+
# summary_table.add_row(Padding("CAS Type :", spacing), f"[bold]{data['cas_type']}[/]")
78+
for key, value in data["investor_info"].items():
79+
summary_table.add_row(
80+
Padding(f"{key.capitalize()} :", spacing), re.sub(r"[^\S\r\n]+", " ", value)
81+
)
82+
console.print(summary_table)
83+
console.print("")
84+
85+
table = Table(title="Portfolio Summary", show_lines=True)
86+
table.add_column("Name")
87+
table.add_column("ISIN")
88+
table.add_column("Units")
89+
table.add_column("Price")
90+
table.add_column("Value")
91+
92+
value = Decimal(0)
93+
94+
for account in parsed_data.accounts:
95+
balance = account.balance
96+
value += balance
97+
running_balance = 0
98+
table_rows = []
99+
if len(account.equities) > 0:
100+
table_rows.append(["[italic]Equities[/]"])
101+
for equity in account.equities:
102+
running_balance += equity.num_shares * equity.price
103+
table_rows.append(
104+
[
105+
equity.name,
106+
equity.isin,
107+
format_number(equity.num_shares),
108+
formatINR(equity.price),
109+
formatINR(equity.value),
110+
]
111+
)
112+
if len(account.mutual_funds) > 0:
113+
table_rows.append(["[italic]Mutual Funds[/]"])
114+
for mf in account.mutual_funds:
115+
running_balance += mf.nav * mf.balance
116+
table_rows.append(
117+
[
118+
mf.name,
119+
mf.isin,
120+
format_number(mf.balance),
121+
formatINR(mf.nav),
122+
formatINR(mf.value),
123+
]
124+
)
125+
if is_close(balance, running_balance, tol=float(balance or 1) * 0.01):
126+
status = "️✅"
127+
else:
128+
status = "❗️"
129+
err += 1
130+
count += 1
131+
table.add_row(
132+
f"[bold]{account.name}\n{account.dp_id} - {account.client_id}[/]", "", "", "", status
133+
)
134+
135+
for row in table_rows:
136+
table.add_row(*row)
137+
138+
console.print(table)
139+
140+
console.print(
141+
f"Portfolio Valuation : [bold green]{formatINR(value)}[/] "
142+
f"[As of {data['statement_period']['to']}]"
143+
)
144+
145+
console.print("[bold]Summary[/]")
146+
console.print(f"{'Total':8s}: [bold white]{count:4d}[/] accounts")
147+
console.print(f"{'Matched':8s}: [bold green]{count - err:4d}[/] accounts")
148+
console.print(f"{'Error':8s}: [bold red]{err:4d}[/] accounts")
149+
150+
58151
def print_summary(parsed_data: CASData, output_filename=None, include_zero_folios=False):
59152
"""Print summary of parsed data."""
60153
count = 0
@@ -348,15 +441,20 @@ def cli(output, summary, password, include_all, gains, gains_112a, force_pdfmine
348441
except ParserException as exc:
349442
console.print(f"Error parsing pdf file :: [bold red]{str(exc)}[/]")
350443
sys.exit(1)
351-
if summary:
444+
if isinstance(data, NSDLCASData):
445+
print_nsdl(data)
446+
elif summary:
352447
print_summary(
353448
data,
354449
include_zero_folios=include_all,
355450
output_filename=None if output_ext in (".csv", ".json") else output,
356451
)
357452

358453
if output_ext in (".csv", ".json"):
359-
if output_ext == ".csv":
454+
if output_ext == ".csv" and data.file_type in (
455+
FileType.CAMS.value,
456+
FileType.KFINTECH.value,
457+
):
360458
if summary or data.cas_type == CASFileType.SUMMARY.name:
361459
description = "Generating summary CSV file..."
362460
conv_fn = cas2csv_summary
@@ -370,7 +468,7 @@ def cli(output, summary, password, include_all, gains, gains_112a, force_pdfmine
370468
with open(output, "w", newline="", encoding="utf-8") as fp:
371469
fp.write(conv_fn(data))
372470
console.print(f"File saved : [bold]{output}[/]")
373-
if gains or gains_112a:
471+
if data.file_type in (FileType.CAMS.value, FileType.KFINTECH.value) and (gains or gains_112a):
374472
try:
375473
print_gains(
376474
data,

casparser/enums.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class FileType(AutoEnum):
1717
UNKNOWN = auto()
1818
CAMS = auto()
1919
KFINTECH = auto()
20+
CDSL = auto()
21+
NSDL = auto()
2022

2123

2224
class CASFileType(AutoEnum):

casparser/parsers/__init__.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Union
33

44
from casparser.process import process_cas_text
5-
from casparser.types import CASData
5+
from casparser.types import CASData, NSDLCASData, ProcessedCASData
66

77
from .utils import cas2csv, cas2json
88

@@ -32,31 +32,39 @@ def read_cas_pdf(
3232
from .pdfminer import cas_pdf_to_text
3333

3434
partial_cas_data = cas_pdf_to_text(filename, password)
35-
36-
processed_data = process_cas_text("\u2029".join(partial_cas_data.lines))
37-
38-
if sort_transactions:
39-
for folio in processed_data.folios:
40-
for idx, scheme in enumerate(folio.schemes):
41-
dates = [x.date for x in scheme.transactions]
42-
sorted_dates = list(sorted(dates))
43-
if dates != sorted_dates:
44-
sorted_transactions = []
45-
balance = scheme.open
46-
for transaction in sorted(scheme.transactions, key=lambda x: x.date):
47-
balance += transaction.units or 0
48-
transaction.balance = balance
49-
sorted_transactions.append(transaction)
50-
scheme.transactions = sorted_transactions
51-
folio.schemes[idx] = scheme
52-
53-
final_data = CASData(
54-
statement_period=processed_data.statement_period,
55-
folios=processed_data.folios,
56-
investor_info=partial_cas_data.investor_info,
57-
cas_type=processed_data.cas_type,
58-
file_type=partial_cas_data.file_type,
35+
processed_data = process_cas_text(
36+
"\u2029".join(partial_cas_data.lines), partial_cas_data.file_type
5937
)
38+
if isinstance(processed_data, ProcessedCASData):
39+
if sort_transactions:
40+
for folio in processed_data.folios:
41+
for idx, scheme in enumerate(folio.schemes):
42+
dates = [x.date for x in scheme.transactions]
43+
sorted_dates = list(sorted(dates))
44+
if dates != sorted_dates:
45+
sorted_transactions = []
46+
balance = scheme.open
47+
for transaction in sorted(scheme.transactions, key=lambda x: x.date):
48+
balance += transaction.units or 0
49+
transaction.balance = balance
50+
sorted_transactions.append(transaction)
51+
scheme.transactions = sorted_transactions
52+
folio.schemes[idx] = scheme
53+
54+
final_data = CASData(
55+
statement_period=processed_data.statement_period,
56+
folios=processed_data.folios,
57+
investor_info=partial_cas_data.investor_info,
58+
cas_type=processed_data.cas_type,
59+
file_type=partial_cas_data.file_type,
60+
)
61+
else:
62+
final_data = NSDLCASData(
63+
statement_period=processed_data.statement_period,
64+
accounts=processed_data.accounts,
65+
investor_info=partial_cas_data.investor_info,
66+
file_type=partial_cas_data.file_type,
67+
)
6068
if output == "dict":
6169
return final_data
6270
elif output == "csv":

0 commit comments

Comments
 (0)