"""Ручные операции (D21): купон, амортизация, комиссия за андеррайтинг, корректировка. Купля и продажа — только загрузкой выгрузки QUIK.""" from __future__ import annotations import logging from datetime import date import streamlit as st from app.db.base import run_db from app.db.models import OperationCode from app.services import manual_operations from app.services.manual_operations import ( MANUAL_TYPES, UWFEE_PORTFOLIO, FormOptions, ManualOperationError, ManualOperationInput, parse_decimal, ) from app.ui.common import as_table, call_db, current_user logger = logging.getLogger(__name__) _SECURITY_CURRENCY = "валюта бумаги" _HINTS = { OperationCode.CPN: "Сумма купона больше нуля. Количество — по желанию.", OperationCode.NOM: "Сумма погашения номинала больше нуля. Количество — по " "желанию. Номинал бумаги здесь не меняется.", OperationCode.UWFEE: f"Только портфель {UWFEE_PORTFOLIO}. Сумма больше нуля, " "без количества. В сводном отчёте добавляется к P/L (Net).", OperationCode.ADJ: "Сумма и/или количество со знаком. Комментарий обязателен. " "В расчётах пока не участвует — смысл корректировки уточняется.", } _COLUMNS = { "trade_date": "Дата", "operation": "Операция", "portfolio": "Портфель", "name": "Бумага", "isin": "ISIN", "quantity": "Кол-во", "amount": "Сумма", "currency": "Валюта", "comment": "Комментарий", "created_by": "Кто ввёл", } def render() -> None: st.header("Ручные операции") st.caption( "Здесь — только то, чего нет в выгрузке QUIK. Купля и продажа вводятся " "загрузкой файла на странице «Загрузка сделок»." ) options = call_db( manual_operations.load_form_options(), "Не удалось загрузить справочники" ) if options is None: return if options.securities: _render_form(options) else: st.info("Бумаг пока нет — добавьте их на странице «Бумаги».") st.subheader("Последние ручные операции") rows = call_db( manual_operations.list_manual_operations(), "Не удалось загрузить ручные операции", ) if rows: st.dataframe(as_table(rows, _COLUMNS), hide_index=True) elif rows is not None: st.caption("Ручных операций пока нет.") def _render_form(options: FormOptions) -> None: # Тип — вне формы: от него зависят поля формы. code = st.selectbox( "Тип операции", list(MANUAL_TYPES), format_func=MANUAL_TYPES.__getitem__ ) st.caption(_HINTS[code]) securities = {s.isin: s for s in options.securities} portfolios = ( [UWFEE_PORTFOLIO] if code == OperationCode.UWFEE else list(options.portfolios) ) with st.form("manual_operation", clear_on_submit=True): portfolio = st.selectbox( "Портфель", portfolios, format_func=lambda c: f"{c} — {options.portfolios.get(c, c)}", ) isin = st.selectbox( "Бумага", list(securities), format_func=lambda i: ( f"{securities[i].name} ({i}, {securities[i].currency})" ), ) trade_date = st.date_input("Дата", value=date.today(), format="DD.MM.YYYY") amount_text = st.text_input("Сумма", placeholder="например 125 000,50") quantity_text = ( "" if code == OperationCode.UWFEE else st.text_input("Количество, шт", placeholder="необязательно") ) currency = st.selectbox("Валюта", [_SECURITY_CURRENCY, *options.currencies]) comment = st.text_area("Комментарий") submitted = st.form_submit_button("Сохранить", type="primary") if submitted: _save( code=code, portfolio=portfolio, isin=isin, trade_date=trade_date, amount_text=amount_text, quantity_text=quantity_text, currency=currency, comment=comment, ) def _save( *, code: str, portfolio: str, isin: str, trade_date: date, amount_text: str, quantity_text: str, currency: str, comment: str, ) -> None: try: operation = ManualOperationInput( operation_code=code, portfolio_code=portfolio, isin=isin, trade_date=trade_date, amount=parse_decimal(amount_text, "Сумма"), quantity=parse_decimal(quantity_text, "Количество"), comment=comment, currency_code=None if currency == _SECURITY_CURRENCY else currency, ) except ManualOperationError as exc: st.error(str(exc)) return errors = manual_operations.validate(operation) if errors: for error in errors: st.error(error) return try: run_db(manual_operations.add_manual_operation(operation, current_user())) except ManualOperationError as exc: st.error(str(exc)) return except Exception as exc: # noqa: BLE001 logger.exception("Ошибка сохранения ручной операции %s по %s", code, isin) st.error(f"Не удалось сохранить операцию: {exc}") return st.success( f"Сохранено: {MANUAL_TYPES[code].lower()}, {isin}, {portfolio}, " f"{trade_date:%d.%m.%Y}." )