from __future__ import annotations from io import BytesIO import csv import zipfile from decimal import Decimal from sqlalchemy import or_ from flask import Blueprint, Response, abort, flash, redirect, render_template, request, send_file, url_for from flask_login import login_required from app.extensions import db from app.forms.invoices import InvoiceFilterForm, InvoiceMetaForm from app.forms.issued import IssuedInvoiceForm from app.models.catalog import Customer, InvoiceLine, Product from app.models.invoice import Invoice, InvoiceStatus, InvoiceType from app.repositories.invoice_repository import InvoiceRepository from app.services.audit_service import AuditService from app.services.ceidg_service import CeidgService from app.services.company_service import CompanyService from app.services.invoice_service import InvoiceService from app.services.ksef_service import KSeFService from app.services.mail_service import MailService from app.services.pdf_service import PdfService from app.services.settings_service import SettingsService bp = Blueprint('invoices', __name__, url_prefix='/invoices') def _company(): return CompanyService.get_current_company() def _require_company(redirect_endpoint='dashboard.index'): company = _company() if company: return company flash('Najpierw wybierz lub utwórz firmę, aby korzystać z tej sekcji.', 'warning') return redirect(url_for(redirect_endpoint)) def _invoice_or_404(invoice_id): company = _company() invoice = db.session.get(Invoice, invoice_id) if not invoice or (company and invoice.company_id != company.id): abort(404) return invoice def _ensure_full_access(company_id): if SettingsService.read_only_enabled(company_id=company_id): flash('Tryb tylko do odczytu jest aktywny dla tej firmy.', 'warning') return False return True def _redirect_with_prefill(endpoint, *, customer_id=None, product_id=None, **kwargs): if customer_id: kwargs['created_customer_id'] = customer_id if product_id: kwargs['created_product_id'] = product_id return redirect(url_for(endpoint, **kwargs)) def _customer_from_invoice(invoice, company_id): existing = None if invoice.customer_id: existing = db.session.get(Customer, invoice.customer_id) if existing and existing.company_id == company_id: return existing, False query = Customer.query.filter_by(company_id=company_id) if invoice.contractor_nip: existing = query.filter(Customer.tax_id == invoice.contractor_nip).first() if not existing: existing = query.filter(Customer.name == invoice.contractor_name).first() created = existing is None customer = existing or Customer(company_id=company_id) customer.name = customer.name or (invoice.contractor_name or '').strip() customer.tax_id = customer.tax_id or (invoice.contractor_nip or '').strip() customer.address = customer.address or (invoice.contractor_address or '').strip() if customer.tax_id and (not customer.address or not customer.regon): lookup = CeidgService().fetch_company(customer.tax_id) if lookup.get('ok'): customer.name = customer.name or (lookup.get('name') or '').strip() customer.tax_id = customer.tax_id or (lookup.get('tax_id') or '').strip() customer.address = customer.address or (lookup.get('address') or '').strip() customer.regon = customer.regon or (lookup.get('regon') or '').strip() db.session.add(customer) db.session.flush() if invoice.customer_id != customer.id: invoice.customer_id = customer.id return customer, created @bp.route('/') @login_required def index(): form = InvoiceFilterForm(request.args) company = _company() query = InvoiceRepository().query_filtered(request.args, company_id=company.id if company else None) page = request.args.get('page', 1, type=int) pagination = query.paginate(page=page, per_page=15, error_out=False) payment_details_map = {invoice.id: InvoiceService().resolve_payment_details(invoice) for invoice in pagination.items} return render_template('invoices/index.html', form=form, pagination=pagination, company=company, payment_details_map=payment_details_map) @bp.route('/monthly') @login_required def monthly(): company = _company() period = request.args.get('period', 'month') if period not in {'year', 'quarter', 'month'}: period = 'month' search = (request.args.get('q') or '').strip() service = InvoiceService() groups = service.grouped_summary(company_id=company.id if company else None, period=period, search=search) comparisons = service.comparative_stats(company_id=company.id if company else None, search=search) return render_template( 'invoices/monthly.html', groups=groups, comparisons=comparisons, company=company, period=period, period_title=service.period_title(period), search=search ) @bp.route('/issued') @login_required def issued_list(): company = _company() search = (request.args.get('q') or '').strip() page = request.args.get('page', 1, type=int) query = Invoice.query.filter(Invoice.company_id == (company.id if company else None), Invoice.source.in_(['issued', 'nfz'])) if search: like = f'%{search}%' query = query.filter(or_( Invoice.invoice_number.ilike(like), Invoice.ksef_number.ilike(like), Invoice.contractor_name.ilike(like), Invoice.contractor_nip.ilike(like) )) pagination = query.order_by(Invoice.created_at.desc()).paginate(page=page, per_page=15, error_out=False) payment_details_map = {invoice.id: InvoiceService().resolve_payment_details(invoice) for invoice in pagination.items} return render_template('invoices/issued_list.html', pagination=pagination, invoices=pagination.items, search=search, payment_details_map=payment_details_map) @bp.route('/issued/new', methods=['GET', 'POST']) @login_required def issued_new(): company = _company() if not company: return _require_company() form = IssuedInvoiceForm(numbering_template='monthly') customers = Customer.query.filter_by(company_id=company.id, is_active=True).order_by(Customer.name).all() products = Product.query.filter_by(company_id=company.id, is_active=True).order_by(Product.name).all() form.customer_id.choices = [(c.id, f'{c.name} ({c.tax_id})' if c.tax_id else c.name) for c in customers] form.product_id.choices = [(p.id, f'{p.name} - {p.net_price} PLN') for p in products] if request.method == 'GET': duplicate_id = request.args.get('duplicate_id', type=int) if duplicate_id: src = _invoice_or_404(duplicate_id) form.invoice_number.data = f'{src.invoice_number}/COPY' form.numbering_template.data = 'custom' if src.customer_id: form.customer_id.data = src.customer_id first_line = src.lines.first() if first_line and first_line.product_id: form.product_id.data = first_line.product_id form.quantity.data = first_line.quantity form.unit_net.data = first_line.unit_net form.split_payment.data = bool(getattr(src, 'split_payment', False)) else: created_customer_id = request.args.get('created_customer_id', type=int) created_product_id = request.args.get('created_product_id', type=int) if created_customer_id and any(c.id == created_customer_id for c in customers): form.customer_id.data = created_customer_id elif customers: form.customer_id.data = customers[0].id if created_product_id and any(p.id == created_product_id for p in products): form.product_id.data = created_product_id elif products: form.product_id.data = products[0].id if products and form.product_id.data and not form.unit_net.data: selected = next((p for p in products if p.id == form.product_id.data), None) if selected: form.unit_net.data = selected.net_price form.split_payment.data = bool(selected.split_payment_default) if customers and products and not form.invoice_number.data: form.invoice_number.data = InvoiceService().next_sale_number(company.id, form.numbering_template.data or 'monthly') if form.validate_on_submit(): if not _ensure_full_access(company.id): return redirect(url_for('invoices.issued_list')) customer = db.session.get(Customer, form.customer_id.data) product = db.session.get(Product, form.product_id.data) qty = Decimal(str(form.quantity.data or 1)) unit_net = Decimal(str(form.unit_net.data or product.net_price)) net = (qty * unit_net).quantize(Decimal('0.01')) vat = (net * (Decimal(str(product.vat_rate)) / Decimal('100'))).quantize(Decimal('0.01')) gross = net + vat split_payment = bool(form.split_payment.data or product.split_payment_default or gross > Decimal('15000')) number = form.invoice_number.data or InvoiceService().next_sale_number(company.id, form.numbering_template.data) send_to_ksef_now = bool(form.submit.data) invoice = Invoice( company_id=company.id, customer_id=customer.id, ksef_number=f'PENDING/{number}', invoice_number=number, contractor_name=customer.name, contractor_nip=customer.tax_id, issue_date=InvoiceService().today_date(), received_date=InvoiceService().today_date(), net_amount=net, vat_amount=vat, gross_amount=gross, seller_bank_account=(company.bank_account or '').strip(), split_payment=split_payment, invoice_type=InvoiceType.SALE, status=InvoiceStatus.NEW, source='issued', issued_status='draft' if not send_to_ksef_now else 'pending', html_preview='', external_metadata={'split_payment': split_payment, 'seller_bank_account': (company.bank_account or '').strip()}, ) db.session.add(invoice) db.session.flush() db.session.add(InvoiceLine( invoice_id=invoice.id, product_id=product.id, description=product.name, quantity=qty, unit=product.unit, unit_net=unit_net, vat_rate=product.vat_rate, net_amount=net, vat_amount=vat, gross_amount=gross, )) InvoiceService().persist_issued_assets(invoice) if send_to_ksef_now: payload = { 'invoiceNumber': number, 'customer': {'name': customer.name, 'taxId': customer.tax_id}, 'lines': [{'name': product.name, 'qty': float(qty), 'unitNet': float(unit_net), 'vatRate': float(product.vat_rate)}], 'metadata': {'split_payment': bool(invoice.split_payment)}, } result = KSeFService(company_id=company.id).issue_invoice(payload) invoice.ksef_number = result.get('ksef_number', invoice.ksef_number) invoice.issued_status = result.get('status', 'issued') invoice.issued_to_ksef_at = InvoiceService().utcnow() invoice.external_metadata = dict(invoice.external_metadata or {}, ksef_send=result) flash(result.get('message', 'Wysłano fakturę do KSeF.'), 'success') AuditService().log('send_invoice_to_ksef', 'invoice', invoice.id, invoice.ksef_number) else: flash('Wygenerowano fakturę roboczą. Możesz ją jeszcze poprawić przed wysyłką do KSeF.', 'success') AuditService().log('draft_invoice', 'invoice', invoice.id, invoice.invoice_number) db.session.commit() return redirect(url_for('invoices.detail', invoice_id=invoice.id)) preview_number = form.invoice_number.data or InvoiceService().next_sale_number(company.id, form.numbering_template.data or 'monthly') if customers and products else '' return render_template('invoices/issued_form.html', form=form, customers=customers, products=products, preview_number=preview_number) @bp.post('/issued//send-to-ksef') @login_required def send_to_ksef(invoice_id): invoice = _invoice_or_404(invoice_id) if invoice.source != 'issued': abort(400) if not _ensure_full_access(invoice.company_id): return redirect(url_for('invoices.detail', invoice_id=invoice.id)) if InvoiceService.invoice_locked(invoice): flash('Ta faktura została już wysłana do KSeF i jest zablokowana do edycji.', 'warning') return redirect(url_for('invoices.detail', invoice_id=invoice.id)) if invoice.gross_amount > Decimal('15000'): invoice.split_payment = True invoice.external_metadata = dict(invoice.external_metadata or {}, split_payment=True) first_line = invoice.lines.first() payload = { 'invoiceNumber': invoice.invoice_number, 'customer': {'name': invoice.contractor_name, 'taxId': invoice.contractor_nip}, 'metadata': {'split_payment': bool(invoice.split_payment)}, 'lines': [{ 'name': first_line.description if first_line else invoice.invoice_number, 'qty': float(first_line.quantity if first_line else 1), 'unitNet': float(first_line.unit_net if first_line else invoice.net_amount), 'vatRate': float(first_line.vat_rate if first_line else 23), }], } result = KSeFService(company_id=invoice.company_id).issue_invoice(payload) invoice.ksef_number = result.get('ksef_number', invoice.ksef_number) invoice.issued_status = result.get('status', 'issued') invoice.issued_to_ksef_at = InvoiceService().utcnow() invoice.external_metadata = dict(invoice.external_metadata or {}, ksef_send=result) InvoiceService().persist_issued_assets(invoice) db.session.commit() flash(result.get('message', 'Wysłano fakturę do KSeF.'), 'success') return redirect(url_for('invoices.detail', invoice_id=invoice.id)) @bp.route('/customers', methods=['GET', 'POST']) @login_required def customers(): company = _company() if not company: return _require_company() customer_id = request.args.get('customer_id', type=int) editing = db.session.get(Customer, customer_id) if customer_id else None if editing and editing.company_id != company.id: editing = None if request.method == 'POST' and request.form.get('fetch_ceidg') and _ensure_full_access(company.id): lookup = CeidgService().fetch_company(request.form.get('tax_id')) if lookup.get('ok'): editing = editing or Customer(company_id=company.id) editing.name = lookup.get('name') or editing.name editing.tax_id = lookup.get('tax_id') or editing.tax_id editing.address = lookup.get('address') or editing.address editing.regon = lookup.get('regon') or editing.regon flash('Pobrano dane klienta z rejestru przedsiębiorców.', 'success') else: flash(lookup.get('message', 'Nie udało się pobrać danych z CEIDG.'), 'warning') elif request.method == 'POST' and _ensure_full_access(company.id): target = editing or Customer(company_id=company.id) target.name = request.form.get('name', '').strip() target.tax_id = request.form.get('tax_id', '').strip() target.email = request.form.get('email', '').strip() target.address = request.form.get('address', '').strip() target.regon = request.form.get('regon', '').strip() db.session.add(target) db.session.commit() flash('Zapisano klienta.', 'success') return redirect(url_for('invoices.customers')) search = (request.args.get('q') or '').strip() page = request.args.get('page', 1, type=int) sort = (request.args.get('sort') or 'name_asc').strip() query = Customer.query.filter_by(company_id=company.id) if search: like = f'%{search}%' query = query.filter(or_( Customer.name.ilike(like), Customer.tax_id.ilike(like), Customer.regon.ilike(like), Customer.email.ilike(like), Customer.address.ilike(like) )) sort_map = { 'name_asc': Customer.name.asc(), 'name_desc': Customer.name.desc(), 'tax_id_asc': Customer.tax_id.asc(), 'tax_id_desc': Customer.tax_id.desc(), } pagination = query.order_by(sort_map.get(sort, Customer.name.asc()), Customer.id.asc()).paginate(page=page, per_page=15, error_out=False) return render_template('invoices/customers.html', items=pagination.items, pagination=pagination, editing=editing, search=search, sort=sort) @bp.route('/products', methods=['GET', 'POST']) @login_required def products(): company = _company() if not company: return _require_company() product_id = request.args.get('product_id', type=int) editing = db.session.get(Product, product_id) if product_id else None if editing and editing.company_id != company.id: editing = None if request.method == 'POST' and _ensure_full_access(company.id): target = editing or Product(company_id=company.id) target.name = request.form.get('name', '').strip() target.sku = request.form.get('sku', '').strip() target.unit = request.form.get('unit', 'szt.').strip() target.net_price = Decimal(request.form.get('net_price', '0') or '0') target.vat_rate = Decimal(request.form.get('vat_rate', '23') or '23') target.split_payment_default = bool(request.form.get('split_payment_default')) db.session.add(target) db.session.commit() flash('Zapisano towar/usługę.', 'success') return redirect(url_for('invoices.products')) search = (request.args.get('q') or '').strip() page = request.args.get('page', 1, type=int) sort = (request.args.get('sort') or 'name_asc').strip() query = Product.query.filter_by(company_id=company.id) if search: like = f'%{search}%' query = query.filter(or_(Product.name.ilike(like), Product.sku.ilike(like), Product.unit.ilike(like))) sort_map = { 'name_asc': Product.name.asc(), 'name_desc': Product.name.desc(), 'price_asc': Product.net_price.asc(), 'price_desc': Product.net_price.desc(), 'sku_asc': Product.sku.asc(), 'sku_desc': Product.sku.desc(), } pagination = query.order_by(sort_map.get(sort, Product.name.asc()), Product.id.asc()).paginate(page=page, per_page=15, error_out=False) return render_template('invoices/products.html', items=pagination.items, pagination=pagination, editing=editing, search=search, sort=sort) @bp.post('/customers/quick-create') @login_required def quick_create_customer(): company = _company() if not company: return _require_company() if not _ensure_full_access(company.id): return redirect(request.form.get('return_to') or url_for('invoices.issued_new')) name = (request.form.get('name') or '').strip() if not name: flash('Podaj nazwę klienta.', 'warning') return redirect(request.form.get('return_to') or url_for('invoices.issued_new')) customer = Customer( company_id=company.id, name=name, tax_id=(request.form.get('tax_id') or '').strip(), email=(request.form.get('email') or '').strip(), address=(request.form.get('address') or '').strip(), regon=(request.form.get('regon') or '').strip() ) db.session.add(customer) db.session.commit() flash('Dodano klienta i ustawiono go w formularzu.', 'success') target = request.form.get('return_endpoint') or 'invoices.issued_new' target_kwargs = {} invoice_id = request.form.get('invoice_id', type=int) if invoice_id and target in {'invoices.issued_edit', 'nfz.edit'}: target_kwargs['invoice_id'] = invoice_id return _redirect_with_prefill(target, customer_id=customer.id, **target_kwargs) @bp.post('/products/quick-create') @login_required def quick_create_product(): company = _company() if not company: return _require_company() if not _ensure_full_access(company.id): return redirect(request.form.get('return_to') or url_for('invoices.issued_new')) name = (request.form.get('name') or '').strip() if not name: flash('Podaj nazwę towaru lub usługi.', 'warning') return redirect(request.form.get('return_to') or url_for('invoices.issued_new')) product = Product( company_id=company.id, name=name, sku=(request.form.get('sku') or '').strip(), unit=(request.form.get('unit') or 'szt.').strip(), net_price=Decimal(request.form.get('net_price', '0') or '0'), vat_rate=Decimal(request.form.get('vat_rate', '23') or '23'), split_payment_default=bool(request.form.get('split_payment_default')) ) db.session.add(product) db.session.commit() flash('Dodano towar/usługę i ustawiono w formularzu.', 'success') target = request.form.get('return_endpoint') or 'invoices.issued_new' target_kwargs = {} invoice_id = request.form.get('invoice_id', type=int) if invoice_id and target in {'invoices.issued_edit', 'nfz.edit'}: target_kwargs['invoice_id'] = invoice_id return _redirect_with_prefill(target, product_id=product.id, **target_kwargs) @bp.route('/issued//edit', methods=['GET', 'POST']) @login_required def issued_edit(invoice_id): invoice = _invoice_or_404(invoice_id) if invoice.source != 'issued': abort(400) if InvoiceService.invoice_locked(invoice): flash('Ta faktura została już wysłana do KSeF i nie można jej edytować.', 'warning') return redirect(url_for('invoices.detail', invoice_id=invoice.id)) company = invoice.company if not company or not _ensure_full_access(company.id): return redirect(url_for('invoices.detail', invoice_id=invoice.id)) form = IssuedInvoiceForm(numbering_template='custom') customers = Customer.query.filter_by(company_id=company.id, is_active=True).order_by(Customer.name).all() products = Product.query.filter_by(company_id=company.id, is_active=True).order_by(Product.name).all() form.customer_id.choices = [(c.id, f'{c.name} ({c.tax_id})' if c.tax_id else c.name) for c in customers] form.product_id.choices = [(p.id, f'{p.name} - {p.net_price} PLN') for p in products] line = invoice.lines.first() if request.method == 'GET': created_customer_id = request.args.get('created_customer_id', type=int) created_product_id = request.args.get('created_product_id', type=int) form.customer_id.data = created_customer_id if created_customer_id and any(c.id == created_customer_id for c in customers) else (invoice.customer_id or (customers[0].id if customers else None)) form.invoice_number.data = invoice.invoice_number form.product_id.data = created_product_id if created_product_id and any(p.id == created_product_id for p in products) else (line.product_id if line and line.product_id else (products[0].id if products else None)) form.quantity.data = line.quantity if line else 1 form.unit_net.data = line.unit_net if line else invoice.net_amount form.split_payment.data = bool(invoice.split_payment or (form.product_id.data and next((p.split_payment_default for p in products if p.id == form.product_id.data), False))) if created_product_id: selected = next((p for p in products if p.id == created_product_id), None) if selected: form.unit_net.data = selected.net_price if form.validate_on_submit(): customer = db.session.get(Customer, form.customer_id.data) product = db.session.get(Product, form.product_id.data) qty = Decimal(str(form.quantity.data or 1)) unit_net = Decimal(str(form.unit_net.data or product.net_price)) net = (qty * unit_net).quantize(Decimal('0.01')) vat = (net * (Decimal(str(product.vat_rate)) / Decimal('100'))).quantize(Decimal('0.01')) gross = net + vat invoice.split_payment = bool(form.split_payment.data or product.split_payment_default or gross > Decimal('15000')) invoice.customer_id = customer.id invoice.invoice_number = form.invoice_number.data or invoice.invoice_number invoice.contractor_name = customer.name invoice.contractor_nip = customer.tax_id invoice.net_amount = net invoice.vat_amount = vat invoice.gross_amount = gross invoice.seller_bank_account = (company.bank_account or '').strip() invoice.ksef_number = f'PENDING/{invoice.invoice_number}' if not line: line = InvoiceLine(invoice_id=invoice.id) db.session.add(line) line.product_id = product.id line.description = product.name line.quantity = qty line.unit = product.unit line.unit_net = unit_net line.vat_rate = product.vat_rate line.net_amount = net line.vat_amount = vat line.gross_amount = gross InvoiceService().persist_issued_assets(invoice) invoice.external_metadata = dict(invoice.external_metadata or {}, split_payment=invoice.split_payment, seller_bank_account=invoice.seller_bank_account) invoice.issued_status = 'draft' db.session.commit() flash('Zapisano zmiany w fakturze roboczej.', 'success') return redirect(url_for('invoices.detail', invoice_id=invoice.id)) preview_number = form.invoice_number.data or invoice.invoice_number return render_template( 'invoices/issued_form.html', form=form, customers=customers, products=products, preview_number=preview_number, editing_invoice=invoice ) @bp.route('/', methods=['GET', 'POST']) @login_required def detail(invoice_id): invoice = _invoice_or_404(invoice_id) service = InvoiceService() service.mark_read(invoice) locked = read_only = SettingsService.read_only_enabled(company_id=invoice.company_id) or InvoiceService.invoice_locked(invoice) form = InvoiceMetaForm( status=invoice.status.value, tags=', '.join([t.name for t in invoice.tags]), internal_note=invoice.internal_note, queue_accounting=invoice.queue_accounting, pinned=invoice.pinned ) if form.validate_on_submit() and not locked: service.update_metadata(invoice, form) AuditService().log('update', 'invoice', invoice.id, 'Updated metadata') flash('Zapisano zmiany.', 'success') return redirect(url_for('invoices.detail', invoice_id=invoice.id)) xml_content = '' if invoice.xml_path: from pathlib import Path as _Path path = _Path(invoice.xml_path) if path.exists(): xml_content = path.read_text(encoding='utf-8') invoice.html_preview = service.render_invoice_html(invoice) linked_customer = db.session.get(Customer, invoice.customer_id) if invoice.customer_id else None can_add_seller_customer = bool(invoice.contractor_name) and invoice.source not in ['issued', 'nfz'] if invoice.source in ['issued', 'nfz']: if not xml_content or (invoice.source == 'issued' and not invoice.xml_path): xml_content = service.persist_issued_assets(invoice) else: PdfService().render_invoice_pdf(invoice) else: if invoice.xml_path: PdfService().render_invoice_pdf(invoice) db.session.commit() payment_details = service.resolve_payment_details(invoice) return render_template( 'invoices/detail.html', invoice=invoice, form=form, xml_content=xml_content, edit_locked=locked, linked_customer=linked_customer, can_add_seller_customer=can_add_seller_customer, payment_details=payment_details, ) @bp.route('//duplicate') @login_required def duplicate(invoice_id): invoice = _invoice_or_404(invoice_id) if invoice.source == 'nfz': return redirect(url_for('nfz.index', duplicate_id=invoice_id)) return redirect(url_for('invoices.issued_new', duplicate_id=invoice_id)) @bp.route('//pdf') @login_required def pdf(invoice_id): invoice = _invoice_or_404(invoice_id) pdf_bytes, _ = PdfService().render_invoice_pdf(invoice) return send_file(BytesIO(pdf_bytes), download_name=f'{invoice.invoice_number}.pdf', mimetype='application/pdf') @bp.route('//send', methods=['POST']) @login_required def send(invoice_id): invoice = _invoice_or_404(invoice_id) if not _ensure_full_access(invoice.company_id): return redirect(url_for('invoices.detail', invoice_id=invoice.id)) recipient = (request.form.get('recipient') or '').strip() if not recipient: flash('Brak adresu e-mail odbiorcy.', 'warning') return redirect(url_for('invoices.detail', invoice_id=invoice.id)) delivery = MailService(company_id=invoice.company_id).send_invoice(invoice, recipient) if delivery.status == 'sent': flash(f'Fakturę wysłano na adres: {delivery.recipient}.', 'success') else: error_message = (delivery.error_message or 'Nieznany błąd SMTP').strip() flash( f'Nie udało się wysłać faktury na adres: {delivery.recipient}. Błąd: {error_message}', 'danger' ) return redirect(url_for('invoices.detail', invoice_id=invoice.id)) @bp.route('/export/csv') @login_required def export_csv(): company = _company() rows = InvoiceRepository().query_filtered(request.args, company_id=company.id if company else None).all() import io sio = io.StringIO() writer = csv.writer(sio) writer.writerow(['Firma', 'Numer', 'KSeF', 'Kontrahent', 'NIP', 'Data', 'Netto', 'VAT', 'Brutto', 'Typ', 'Status']) for invoice in rows: writer.writerow([ invoice.company.name if invoice.company else '', invoice.invoice_number, invoice.ksef_number, invoice.contractor_name, invoice.contractor_nip, invoice.issue_date.isoformat(), str(invoice.net_amount), str(invoice.vat_amount), str(invoice.gross_amount), invoice.invoice_type.value, invoice.status.value ]) return Response(sio.getvalue(), mimetype='text/csv', headers={'Content-Disposition': 'attachment; filename=invoices.csv'}) @bp.route('/export/zip') @login_required def export_zip(): company = _company() rows = InvoiceRepository().query_filtered(request.args, company_id=company.id if company else None).all() mem = BytesIO() with zipfile.ZipFile(mem, mode='w', compression=zipfile.ZIP_DEFLATED) as zf: for invoice in rows: if invoice.xml_path: from pathlib import Path path = Path(invoice.xml_path) if path.exists(): zf.write(path, arcname=path.name) mem.seek(0) return send_file(mem, download_name='invoices.zip', mimetype='application/zip') @bp.route('/monthly//pdf') @login_required def month_pdf(period): company = _company() groups = [g for g in InvoiceService().grouped_summary(company_id=company.id if company else None, period='month') if g['key'] == period] if not groups: return redirect(url_for('invoices.monthly')) pdf_bytes = PdfService().month_pdf(groups[0]['entries'], f'Miesiąc {period}') return send_file(BytesIO(pdf_bytes), download_name=f'{period}.pdf', mimetype='application/pdf') @bp.route('/bulk', methods=['POST']) @login_required def bulk_action(): company = _company() if SettingsService.read_only_enabled(company_id=company.id if company else None): flash('Tryb read only jest aktywny.', 'warning') return redirect(url_for('invoices.index')) ids = request.form.getlist('invoice_ids') action = request.form.get('action') invoices = Invoice.query.filter(Invoice.company_id == (company.id if company else None), Invoice.id.in_(ids)).all() for invoice in invoices: if action == 'mark_accounted': invoice.status = invoice.status.__class__.ACCOUNTED elif action == 'queue_accounting': invoice.queue_accounting = True db.session.commit() flash('Wykonano akcję masową.', 'success') return redirect(url_for('invoices.index')) @bp.post('//add-seller-customer') @login_required def add_seller_customer(invoice_id): invoice = _invoice_or_404(invoice_id) if invoice.source in ['issued', 'nfz']: flash('Dla faktur sprzedażowych kontrahent jest już obsługiwany w kartotece odbiorców.', 'info') return redirect(url_for('invoices.detail', invoice_id=invoice.id)) if not _ensure_full_access(invoice.company_id): return redirect(url_for('invoices.detail', invoice_id=invoice.id)) customer, created = _customer_from_invoice(invoice, invoice.company_id) db.session.commit() flash('Dodano kontrahenta do kartoteki klientów.' if created else 'Powiązano kontrahenta z istniejącą kartoteką klientów.', 'success') return redirect(url_for('invoices.customers', customer_id=customer.id))