55from datetime import date
66import io
77import itertools
8- from typing import List
8+ from typing import List , Optional
99
1010from dateutil .parser import parse as dateparse
1111from dateutil .relativedelta import relativedelta
3333}
3434
3535
36+ @dataclass
37+ class GainEntry112A :
38+ """GainEntry for schedule 112A of ITR."""
39+
40+ acquired : str # AE, BE
41+ isin : str
42+ name : str
43+ units : Decimal
44+ sale_nav : Decimal
45+ sale_value : Decimal
46+ purchase_value : Decimal
47+ fmv_nav : Decimal
48+ fmv : Decimal
49+ stt : Decimal
50+ stamp_duty : Decimal
51+
52+ @property
53+ def consideration_value (self ):
54+ if self .acquired == "BE" :
55+ return min (self .fmv , self .sale_value )
56+ else :
57+ return Decimal ("0.00" ) # FMV not considered
58+
59+ @property
60+ def actual_coa (self ):
61+ return max (self .purchase_value , self .consideration_value )
62+
63+ @property
64+ def expenditure (self ):
65+ return self .stt + self .stamp_duty
66+
67+ @property
68+ def deductions (self ):
69+ return self .actual_coa + self .expenditure
70+
71+ @property
72+ def balance (self ):
73+ return self .sale_value - self .deductions
74+
75+
3676@dataclass
3777class MergedTransaction :
3878 """Represent net transaction on a given date"""
@@ -73,12 +113,17 @@ def add(self, txn: TransactionDataType):
73113class Fund :
74114 """Fund details"""
75115
76- name : str
116+ scheme : str
117+ folio : str
77118 isin : str
78119 type : str
79120
121+ @property
122+ def name (self ):
123+ return f"{ self .scheme } [{ self .folio } ]"
124+
80125 def __lt__ (self , other : "Fund" ):
81- return self .name < other .name
126+ return self .scheme < other .scheme
82127
83128
84129@dataclass
@@ -89,9 +134,11 @@ class GainEntry:
89134 fund : Fund
90135 type : str
91136 purchase_date : date
137+ purchase_nav : Decimal
92138 purchase_value : Decimal
93139 stamp_duty : Decimal
94140 sale_date : date
141+ sale_nav : Decimal
95142 sale_value : Decimal
96143 stt : Decimal
97144 units : Decimal
@@ -120,12 +167,16 @@ def gain(self) -> Decimal:
120167 return Decimal (round (self .sale_value - self .purchase_value , 2 ))
121168
122169 @property
123- def fmv (self ) -> Decimal :
170+ def fmv_nav (self ) -> Decimal :
124171 if self .fund .isin != self ._cached_isin :
125172 self .__update_nav ()
126- if self ._cached_nav is None :
173+ return self ._cached_nav
174+
175+ @property
176+ def fmv (self ) -> Decimal :
177+ if self .fmv_nav is None :
127178 return self .purchase_value
128- return self ._cached_nav * self .units
179+ return self .fmv_nav * self .units
129180
130181 @property
131182 def index_ratio (self ) -> Decimal :
@@ -270,9 +321,11 @@ def sell(self, sell_date: date, quantity: Decimal, nav: Decimal, tax: Decimal):
270321 fund = self ._fund ,
271322 type = self .fund_type .name ,
272323 purchase_date = purchase_date ,
324+ purchase_nav = purchase_nav ,
273325 purchase_value = purchase_value ,
274326 stamp_duty = stamp_duty ,
275327 sale_date = sell_date ,
328+ sale_nav = nav ,
276329 sale_value = sale_value ,
277330 stt = stt ,
278331 units = gain_units ,
@@ -306,30 +359,39 @@ def __init__(self, data: CASParserDataType):
306359 def gains (self ) -> List [GainEntry ]:
307360 return list (sorted (self ._gains , key = lambda x : (x .fy , x .fund , x .sale_date )))
308361
362+ def has_gains (self ) -> bool :
363+ return len (self .gains ) > 0
364+
309365 def has_error (self ) -> bool :
310366 return len (self .errors ) > 0
311367
368+ def get_fy_list (self ) -> List [str ]:
369+ return list (sorted (set ([f .fy for f in self .gains ]), reverse = True ))
370+
312371 def process_data (self ):
313372 self ._gains = []
314373 for folio in self ._data .get ("folios" , []):
315374 for scheme in folio .get ("schemes" , []):
316- name = f"{ scheme ['scheme' ]} [{ folio ['folio' ]} ]"
317375 transactions = scheme ["transactions" ]
376+ fund = Fund (
377+ scheme = scheme ["scheme" ],
378+ folio = folio ["folio" ],
379+ isin = scheme ["isin" ],
380+ type = scheme ["type" ],
381+ )
318382 if len (transactions ) > 0 :
319383 if scheme ["open" ] >= 0.01 :
320384 raise IncompleteCASError (
321385 "Incomplete CAS found. For gains computation, "
322386 "all folios should have zero opening balance"
323387 )
324388 try :
325- fifo = FIFOUnits (
326- Fund (name = name , isin = scheme ["isin" ], type = scheme ["type" ]), transactions
327- )
389+ fifo = FIFOUnits (fund , transactions )
328390 self .invested_amount += fifo .invested
329391 self .current_value += scheme ["valuation" ]["value" ]
330392 self ._gains .extend (fifo .gains )
331393 except GainsError as exc :
332- self .errors .append ((name , str (exc )))
394+ self .errors .append ((fund . name , str (exc )))
333395
334396 def get_summary (self ):
335397 """Calculate capital gains summary"""
@@ -400,3 +462,100 @@ def get_gains_csv_data(self) -> str:
400462 csv_fp .seek (0 )
401463 csv_data = csv_fp .read ()
402464 return csv_data
465+
466+ def generate_112a (self , fy ) -> List [GainEntry112A ]:
467+ fy_transactions = sorted (
468+ list (filter (lambda x : x .fy == fy and x .fund .type == "EQUITY" , self .gains )),
469+ key = lambda x : x .fund ,
470+ )
471+ rows : List [GainEntry112A ] = []
472+ for fund , txns in itertools .groupby (fy_transactions , key = lambda x : x .fund ):
473+ consolidated_entry : Optional [GainEntry112A ] = None
474+ entries = []
475+ for txn in txns :
476+ if txn .purchase_date <= date (2018 , 1 , 31 ):
477+ entries .append (
478+ GainEntry112A (
479+ "BE" ,
480+ fund .isin ,
481+ fund .scheme ,
482+ txn .units ,
483+ txn .sale_nav ,
484+ txn .sale_value ,
485+ txn .purchase_value ,
486+ txn .fmv_nav ,
487+ txn .fmv ,
488+ txn .stt ,
489+ txn .stamp_duty ,
490+ )
491+ )
492+ else :
493+ if consolidated_entry is None :
494+ consolidated_entry = GainEntry112A (
495+ "AE" ,
496+ fund .isin ,
497+ fund .scheme ,
498+ txn .units ,
499+ txn .sale_nav ,
500+ txn .sale_value ,
501+ txn .purchase_value ,
502+ Decimal (0.0 ),
503+ Decimal (0.0 ),
504+ txn .stt ,
505+ txn .stamp_duty ,
506+ )
507+ else :
508+ consolidated_entry .purchase_value += txn .purchase_value
509+ consolidated_entry .stt += txn .stt
510+ consolidated_entry .stamp_duty += txn .stamp_duty
511+ consolidated_entry .units += txn .units
512+ consolidated_entry .sale_value += txn .sale_value
513+ consolidated_entry .sale_nav = Decimal (round (txn .sale_value / txn .units , 3 ))
514+ rows .extend (entries )
515+ if consolidated_entry is not None :
516+ rows .append (consolidated_entry )
517+ return rows
518+
519+ def generate_112a_csv_data (self , fy ):
520+ headers = [
521+ "Share/Unit acquired(1a)" ,
522+ "ISIN Code(2)" ,
523+ "Name of the Share/Unit(3)" ,
524+ "No. of Shares/Units(4)" ,
525+ "Sale-price per Share/Unit(5)" ,
526+ "Full Value of Consideration(Total Sale Value)(6) = 4 * 5" ,
527+ "Cost of acquisition without indexation(7)" ,
528+ "Cost of acquisition(8)" ,
529+ "If the long term capital asset was acquired before 01.02.2018(9)" ,
530+ "Fair Market Value per share/unit as on 31st January 2018(10)" ,
531+ "Total Fair Market Value of capital asset as per section 55(2)(ac)(11) = 4 * 10" ,
532+ "Expenditure wholly and exclusively in connection with transfer(12)" ,
533+ "Total deductions(13) = 7 + 12" ,
534+ "Balance(14) = 6 - 13" ,
535+ ]
536+ with io .StringIO () as csv_fp :
537+ writer = csv .writer (csv_fp )
538+ writer .writerow (headers )
539+
540+ for row in self .generate_112a (fy ):
541+ writer .writerow (
542+ [
543+ row .acquired ,
544+ row .isin ,
545+ row .name ,
546+ str (row .units ),
547+ str (row .sale_nav ),
548+ str (row .sale_value ),
549+ str (row .actual_coa ),
550+ str (row .purchase_value ),
551+ str (row .consideration_value ),
552+ str (row .fmv_nav ),
553+ str (row .fmv ),
554+ str (row .expenditure ),
555+ str (row .deductions ),
556+ str (row .balance ),
557+ ]
558+ )
559+ csv_fp .seek (0 )
560+ csv_data = csv_fp .read ()
561+ return csv_data
0 commit comments