Last active
April 30, 2021 22:10
-
-
Save solesensei/eb3112196c87179e6e09bd86c11771c3 to your computer and use it in GitHub Desktop.
Tinkoff.Invest Dividends Tax Calculator | Налог с дивидендов в Тинькофф.Инвестициях
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from datetime import datetime | |
from decimal import Decimal, InvalidOperation | |
import typing as tp | |
try: | |
from pdfreader import SimplePDFViewer, PageDoesNotExist | |
except ImportError as e: | |
raise ImportError("Install pdfreader: pip install pdfreader") from e | |
try: | |
from pycbrf import rates | |
except ImportError as e: | |
raise ImportError("Install pycbrf: pip install pycbrf") from e | |
# ---------- CHANGE ME ---------- | |
REPORT = 'out-inc-state-2020.pdf' # your report | |
ONE_DAY_TAX = "24.12.2020" # calculate rates and taxes for specific date | |
# ------------------------------- | |
TAX_RATE = 13 # 13% | |
class DivIncome: | |
def __init__( | |
self, | |
company: str = None, | |
fixday: datetime = None, | |
payday: datetime = None, | |
country: str = None, | |
papers_num: int = None, | |
pay_per_paper: Decimal = None, | |
tax_held: Decimal = None, | |
total_pay: Decimal = None, | |
currency: str = None, | |
fixrate: Decimal = None, | |
payrate: Decimal = None, | |
) -> None: | |
""" Дивидендный доход | |
:param company: Название компании, выплатившей дивиденды | |
:param fixday: Дата фиксации реестра | |
:param payday: Дата выплаты | |
:param country: Страна эмитента | |
:param papers_num: Количество ценных бумаг | |
:param pay_per_paper: Выплата на одну бумагу | |
:param tax_held: Удержано налога | |
:param total_pay: Итоговая сумма выплаты | |
:param currency: Валюта | |
:param fixrate: Курс цб на дату `fixday` фиксации | |
:param payrate: Курс цб на дату `payday` выплаты | |
""" | |
self.company = company | |
self.fixday = fixday | |
self.payday = payday | |
self.country = country | |
self.papers_num = papers_num | |
self.pay_per_paper = pay_per_paper | |
self.tax_held = tax_held | |
self.total_pay = total_pay | |
self.currency = currency | |
self.fixrate = fixrate | |
self.payrate = payrate | |
def get_total_income(self) -> Decimal: | |
""" Совокупный доход в долларах (без вычета налогов) """ | |
return self.total_pay + self.tax_held | |
def get_total_income_rubles(self) -> Decimal: | |
""" Совокупный доход в рублях (без вычета налогов) по курсу ЦБ на дату выплаты """ | |
return (self.total_pay + self.tax_held) * self.payrate | |
def get_income_after_taxes(self) -> Decimal: | |
""" Доход зачисленный на счет в рублях (после вычета налогов) по курсу ЦБ на дату выплаты """ | |
return self.total_pay * self.payrate | |
def get_tax_payed(self) -> Decimal: | |
""" Уплаченный налог в рублях по курсу ЦБ на дату фиксации реестра """ | |
return self.tax_held * self.fixrate | |
def get_tax_payed_rate(self) -> Decimal: | |
""" Процент уплаченного налога зарубежом """ | |
return self.tax_held * 100 / (self.total_pay + self.tax_held) | |
def get_taxes_to_pay(self, tax_rate: int) -> Decimal: | |
return self.get_total_income_rubles() * tax_rate / 100 | |
def fetch_rate(self): | |
if self.fixday is None: | |
raise ValueError("fixday is empty") | |
if self.payday is None: | |
raise ValueError("payday is empty") | |
if self.currency is None: | |
raise ValueError("currency is empty") | |
self.fixrate = rates.ExchangeRates(on_date=self.fixday)[self.currency].rate | |
self.payrate = rates.ExchangeRates(on_date=self.payday)[self.currency].rate | |
def validate(self): | |
if self.company is None: | |
raise ValueError("company is empty") | |
if self.fixday is None: | |
raise ValueError("fixday is empty") | |
if self.payday is None: | |
raise ValueError("payday is empty") | |
if self.country is None: | |
raise ValueError("country is empty") | |
if self.papers_num is None: | |
raise ValueError("papers_num is empty") | |
if self.pay_per_paper is None: | |
raise ValueError("pay_per_paper is empty") | |
if self.tax_held is None: | |
raise ValueError("tax_held is empty") | |
if self.total_pay is None: | |
raise ValueError("total_pay is empty") | |
if self.currency is None: | |
raise ValueError("currency is empty") | |
if self.payrate is None: | |
raise ValueError("payrate is empty") | |
if self.fixrate is None: | |
raise ValueError("fixrate is empty") | |
def get_date_from_line(date): | |
try: | |
return datetime.strptime(date, r"%d.%m.%Y") | |
except ValueError: | |
return None | |
def get_decimal(f): | |
try: | |
return Decimal(f.replace(",", '.')) | |
except InvalidOperation: | |
return None | |
def parse_report() -> tp.List[DivIncome]: | |
print("Load report") | |
f = open(REPORT, 'rb') | |
viewer = SimplePDFViewer(f) | |
viewer.navigate(1) | |
viewer.render() | |
report: tp.List[DivIncome] = [] | |
try: | |
companies = set() | |
pay_seq = [ | |
"per_paper", "commision", "total_no_tax", "tax", "total" | |
] | |
while True: | |
prev_line = None | |
count_pays = 0 | |
d = DivIncome() | |
for line in viewer.canvas.strings: | |
if d.fixday is None or d.payday is None: | |
date = get_date_from_line(line) | |
if date and d.fixday is None: | |
d.fixday = date | |
elif date and d.payday is None: | |
d.payday = date | |
if "ORD" in line: | |
d.company = line.split("ORD")[0].strip().strip("_") | |
if d.company not in companies: | |
print(f"Income dividents from {d.company}") | |
companies.add(d.company) | |
elif d.company and line.isdigit(): | |
d.country = prev_line.strip() | |
d.papers_num = int(line) | |
elif d.papers_num and count_pays < len(pay_seq): | |
val = get_decimal(line) | |
if val is None: | |
raise ValueError(f"Parsing error, pay value in company: {d.company} is not float[{count_pays}]: {line}", "is is empty") | |
if pay_seq[count_pays] == "per_paper": | |
d.pay_per_paper = val | |
if pay_seq[count_pays] == "tax": | |
d.tax_held = val | |
if pay_seq[count_pays] == "total": | |
d.total_pay = val | |
count_pays += 1 | |
elif count_pays == len(pay_seq): | |
d.currency = line.strip() | |
d.fetch_rate() | |
d.validate() | |
report.append(d) | |
count_pays = 0 | |
d = DivIncome() | |
prev_line = line | |
viewer.next() | |
viewer.render() | |
except PageDoesNotExist: | |
pass | |
return report | |
def print_table(report: tp.List[DivIncome]) -> None: | |
print("-" * 110) | |
fixday = "Fixday" | |
payday = "Payday" | |
company = "Company" | |
total = "Income" | |
payrate = "Rate" | |
fixrate = "Rate" | |
total_r = "Income,₽" | |
tax_held = "Held" | |
percent = "%" | |
tax = f"Tax,₽,{TAX_RATE}%" | |
company_len = max(len(r.company) for r in report) | |
print(f"{fixday:<10} {fixrate:<7} {payday:<10} {payrate:<7}\t{company:<{company_len}}\t{total:<6}\t{total_r:<8} {tax_held:<5} {percent:<3} {tax:<5}") | |
print("-" * 110) | |
for r in sorted(report, key=lambda x: x.payday): | |
fixday = r.fixday.strftime(r"%d.%m.%Y") | |
payday = r.payday.strftime(r"%d.%m.%Y") | |
company = r.company | |
total = f"{r.get_total_income():.2f}" | |
payrate = f"{r.payrate:.4f}" | |
fixrate = f"{r.fixrate:.4f}" | |
total_r = f"{r.get_total_income_rubles():.2f}₽" | |
tax_held = f"{r.tax_held:.2f}" | |
percent = f"{r.get_tax_payed_rate():.0f}" | |
tax = f"{r.get_taxes_to_pay(tax_rate=TAX_RATE) - r.get_tax_payed():.2f}" | |
print(f"{fixday:<10} {fixrate:<7} {payday:<10} {payrate:<7}\t{company:<{company_len}}\t{total:<6}\t{total_r:<8} {tax_held:<5} {percent:<3} {tax:<5}") | |
def main(): | |
one_day_tax = get_date_from_line(ONE_DAY_TAX) | |
if one_day_tax is None: | |
ValueError(f"Invalid date format: '{ONE_DAY_TAX}'") | |
report = parse_report() | |
print_table(report) | |
total_income_rubles, total_income_dollars = Decimal(0), Decimal(0) | |
pay_ammount_rubles, pay_ammount_dollars = Decimal(0), Decimal(0) | |
taxes_held_rubles, taxes_held_dollars = Decimal(0), Decimal(0) | |
taxes = Decimal(0) | |
for r in report: | |
pay_ammount_dollars += r.total_pay | |
pay_ammount_rubles += r.get_income_after_taxes() | |
total_income_dollars += r.get_total_income() | |
total_income_rubles += r.get_total_income_rubles() | |
taxes_held_dollars += r.tax_held | |
taxes_held_rubles += r.get_tax_payed() | |
taxes += r.get_taxes_to_pay(tax_rate=TAX_RATE) - r.get_tax_payed() | |
print("-" * 60) | |
print(f"Total income: {total_income_rubles:.2f} ₽ | {total_income_dollars:.2f} $") | |
print(f"Your income: {pay_ammount_rubles:.2f} ₽ | {pay_ammount_dollars:.2f} $") | |
print(f"Income after taxes: {pay_ammount_rubles-taxes:.2f} ₽") | |
print(f"Taxes payed: {taxes_held_rubles:.2f} ₽ | {taxes_held_dollars:.2f} $") | |
print(f"Taxes to pay: {taxes:.2f} ₽") | |
print(f"Tax Rate: {TAX_RATE}%") | |
print("-" * 60) | |
rate = rates.ExchangeRates(on_date=one_day_tax)["USD"].rate | |
total_income_rubles = Decimal(0) | |
taxes = Decimal(0) | |
taxes_payed = Decimal(0) | |
for r in report: | |
total_income_rubles += r.get_total_income() * rate | |
taxes_payed += r.tax_held * rate | |
taxes += r.get_total_income() * rate * TAX_RATE / 100 - r.tax_held * rate | |
print(f"Single income for date {ONE_DAY_TAX}") | |
print(f"Total income: {total_income_rubles:.2f} ₽") | |
print(f"Taxes payed: {taxes_payed:.2f} ₽") | |
print(f"Taxes to pay: {taxes:.2f} ₽") | |
print(f"Tax Rate: {TAX_RATE}%") | |
print(f"USD Rate: {rate:.4f}") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
requirements
Python 3.6+
pdf report
https://www.tinkoff.ru/invest/broker_account/about/
Личный кабинет → Инвестиции → Портфель → О счете → Справка о доходах за пределами РФ за прошлый год
usage