206 lines
7.9 KiB
Python
206 lines
7.9 KiB
Python
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
|
|
from flask import Blueprint, current_app, jsonify, redirect, render_template, request, url_for
|
|
from flask_login import current_user, login_required
|
|
from sqlalchemy import extract
|
|
|
|
from app.extensions import csrf
|
|
from app.models.invoice import Invoice
|
|
from app.models.sync_log import SyncLog
|
|
from app.services.company_service import CompanyService
|
|
from app.services.health_service import HealthService
|
|
from app.services.redis_service import RedisService
|
|
from app.services.settings_service import SettingsService
|
|
from app.services.sync_service import SyncService
|
|
|
|
bp = Blueprint('dashboard', __name__)
|
|
|
|
|
|
def _load_dashboard_summary(company_id: int):
|
|
cache_key = f'dashboard.summary.company.{company_id}'
|
|
cached = RedisService.get_json(cache_key) or {}
|
|
base = Invoice.query.filter_by(company_id=company_id)
|
|
today = date.today()
|
|
|
|
if not cached:
|
|
month_invoices = base.filter(
|
|
extract('month', Invoice.issue_date) == today.month,
|
|
extract('year', Invoice.issue_date) == today.year,
|
|
).order_by(Invoice.issue_date.desc(), Invoice.id.desc()).all()
|
|
cached = {
|
|
'month_invoice_ids': [invoice.id for invoice in month_invoices],
|
|
'unread': base.filter_by(is_unread=True).count(),
|
|
'totals': {
|
|
'net': str(sum(Decimal(invoice.net_amount) for invoice in month_invoices)),
|
|
'vat': str(sum(Decimal(invoice.vat_amount) for invoice in month_invoices)),
|
|
'gross': str(sum(Decimal(invoice.gross_amount) for invoice in month_invoices)),
|
|
},
|
|
'recent_invoice_ids': [invoice.id for invoice in base.order_by(Invoice.created_at.desc(), Invoice.id.desc()).limit(200).all()],
|
|
}
|
|
RedisService.set_json(cache_key, cached, ttl=300)
|
|
|
|
month_ids = cached.get('month_invoice_ids', [])
|
|
month_invoices = Invoice.query.filter(Invoice.id.in_(month_ids)).all() if month_ids else []
|
|
month_invoices.sort(key=lambda item: month_ids.index(item.id) if item.id in month_ids else 9999)
|
|
|
|
totals = {
|
|
'net': Decimal(str(cached.get('totals', {}).get('net', '0'))),
|
|
'vat': Decimal(str(cached.get('totals', {}).get('vat', '0'))),
|
|
'gross': Decimal(str(cached.get('totals', {}).get('gross', '0'))),
|
|
}
|
|
return cached, month_invoices, totals
|
|
|
|
|
|
@bp.route('/')
|
|
@login_required
|
|
def index():
|
|
company = CompanyService.get_current_company()
|
|
health_service = HealthService()
|
|
health = health_service.get_cached_status(company.id if company else None) or health_service.get_status(company_id=company.id if company else None)
|
|
|
|
if not company:
|
|
return render_template(
|
|
'dashboard/index.html',
|
|
company=None,
|
|
month_invoices=[],
|
|
unread=0,
|
|
totals={'net': Decimal('0'), 'vat': Decimal('0'), 'gross': Decimal('0')},
|
|
recent_invoices=[],
|
|
last_sync_display='brak',
|
|
sync_status='inactive',
|
|
health=health,
|
|
current_user=current_user,
|
|
recent_pagination={'page': 1, 'pages': 1, 'has_prev': False, 'has_next': False, 'prev_num': 1, 'next_num': 1},
|
|
payment_details_map={},
|
|
redis_fallback=(health.get('redis') == 'fallback'),
|
|
)
|
|
|
|
read_only = SettingsService.read_only_enabled(company_id=company.id)
|
|
base = Invoice.query.filter_by(company_id=company.id)
|
|
last_sync_raw = SettingsService.get('ksef.last_sync_at', None, company_id=company.id)
|
|
last_sync = None
|
|
if isinstance(last_sync_raw, str) and last_sync_raw.strip():
|
|
try:
|
|
last_sync = datetime.fromisoformat(last_sync_raw.replace('Z', '+00:00'))
|
|
except Exception:
|
|
last_sync = last_sync_raw
|
|
elif last_sync_raw:
|
|
last_sync = last_sync_raw
|
|
if not last_sync:
|
|
latest_log = SyncLog.query.filter_by(company_id=company.id, status='finished').order_by(SyncLog.finished_at.desc()).first()
|
|
last_sync = latest_log.finished_at if latest_log and latest_log.finished_at else None
|
|
|
|
cached, month_invoices, totals = _load_dashboard_summary(company.id)
|
|
unread = cached.get('unread', 0)
|
|
recent_ids = cached.get('recent_invoice_ids', [])
|
|
per_page = 10
|
|
total_recent = len(recent_ids)
|
|
total_pages = max((total_recent + per_page - 1) // per_page, 1)
|
|
dashboard_page = min(max(request.args.get('dashboard_page', 1, type=int), 1), total_pages)
|
|
start = (dashboard_page - 1) * per_page
|
|
end = start + per_page
|
|
current_ids = recent_ids[start:end]
|
|
recent_invoices = Invoice.query.filter(Invoice.id.in_(current_ids)).all() if current_ids else []
|
|
recent_invoices.sort(key=lambda item: current_ids.index(item.id) if item.id in current_ids else 9999)
|
|
recent_pagination = {
|
|
'page': dashboard_page,
|
|
'pages': total_pages,
|
|
'has_prev': dashboard_page > 1,
|
|
'has_next': end < total_recent,
|
|
'prev_num': dashboard_page - 1,
|
|
'next_num': dashboard_page + 1,
|
|
}
|
|
|
|
from app.services.invoice_service import InvoiceService
|
|
payment_details_map = {invoice.id: InvoiceService().resolve_payment_details(invoice) for invoice in recent_invoices}
|
|
last_sync_display = last_sync.strftime('%Y-%m-%d %H:%M:%S') if hasattr(last_sync, 'strftime') else (last_sync or 'brak')
|
|
|
|
return render_template(
|
|
'dashboard/index.html',
|
|
company=company,
|
|
month_invoices=month_invoices,
|
|
unread=unread,
|
|
totals=totals,
|
|
recent_invoices=recent_invoices,
|
|
recent_pagination=recent_pagination,
|
|
payment_details_map=payment_details_map,
|
|
last_sync_display=last_sync_display,
|
|
last_sync_raw=last_sync,
|
|
sync_status=SettingsService.get('ksef.status', 'inactive', company_id=company.id),
|
|
health=health,
|
|
read_only=read_only,
|
|
redis_fallback=(health.get('redis') == 'fallback'),
|
|
)
|
|
|
|
|
|
@bp.route('/switch-company/<int:company_id>')
|
|
@login_required
|
|
def switch_company(company_id):
|
|
if not CompanyService.switch_company(company_id):
|
|
abort(403)
|
|
return redirect(url_for('dashboard.index'))
|
|
|
|
|
|
@bp.post('/sync/manual')
|
|
@login_required
|
|
def manual_sync():
|
|
company = CompanyService.get_current_company()
|
|
if not company:
|
|
return redirect(url_for('dashboard.index'))
|
|
app = current_app._get_current_object()
|
|
log_id = SyncService.start_manual_sync_async(app, company.id)
|
|
return redirect(url_for('dashboard.index', started=log_id))
|
|
|
|
|
|
@bp.route('/sync/status')
|
|
@login_required
|
|
@csrf.exempt
|
|
def sync_status():
|
|
company = CompanyService.get_current_company()
|
|
if not company:
|
|
return jsonify({'status': 'no_company'})
|
|
log = SyncLog.query.filter_by(company_id=company.id).order_by(SyncLog.started_at.desc()).first()
|
|
if not log:
|
|
return jsonify({'status': 'idle'})
|
|
return jsonify(
|
|
{
|
|
'status': log.status,
|
|
'message': log.message,
|
|
'processed': log.processed,
|
|
'created': log.created,
|
|
'updated': log.updated,
|
|
'errors': log.errors,
|
|
'total': log.total,
|
|
}
|
|
)
|
|
|
|
|
|
@bp.post('/sync/start')
|
|
@login_required
|
|
def sync_start():
|
|
company = CompanyService.get_current_company()
|
|
if not company:
|
|
return jsonify({'error': 'no_company'}), 400
|
|
app = current_app._get_current_object()
|
|
log_id = SyncService.start_manual_sync_async(app, company.id)
|
|
return jsonify({'log_id': log_id})
|
|
|
|
|
|
@bp.get('/sync/status/<int:log_id>')
|
|
@login_required
|
|
def sync_status_by_id(log_id):
|
|
log = SyncLog.query.get_or_404(log_id)
|
|
total = log.total or 0
|
|
progress = int((log.processed / total) * 100) if total else (100 if log.status == 'finished' else 0)
|
|
return jsonify({
|
|
'status': log.status,
|
|
'message': log.message,
|
|
'processed': log.processed,
|
|
'created': log.created,
|
|
'updated': log.updated,
|
|
'errors': log.errors,
|
|
'total': total,
|
|
'progress': progress,
|
|
})
|