Skip to content

Instantly share code, notes, and snippets.

@solesensei
Last active April 30, 2021 22:10
Show Gist options
  • Save solesensei/eb3112196c87179e6e09bd86c11771c3 to your computer and use it in GitHub Desktop.
Save solesensei/eb3112196c87179e6e09bd86c11771c3 to your computer and use it in GitHub Desktop.
Tinkoff.Invest Dividends Tax Calculator | Налог с дивидендов в Тинькофф.Инвестициях
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()
@solesensei
Copy link
Author

requirements

Python 3.6+

pip install pdfreader pycbrf -U

pdf report

https://www.tinkoff.ru/invest/broker_account/about/

Личный кабинет → Инвестиции → Портфель → О счете → Справка о доходах за пределами РФ за прошлый год

usage

python dividends.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment