lariv-rs 0.1.0

Compile-time plugin web application framework built on Axum, SeaORM, Maud, and HTMX
Documentation
//! Patches invoice presentation + GL preferences onto `/finance/preferences`.

use crate::components::{
    CodeEditorInput,
    attrs::escape_attr,
    code_editor_input,
    htmx::{HTMX_SWAP_BODY_MODAL, HTMX_TARGET_BODY_MODAL},
    label_hint,
};
use crate::html_form::FormFieldKey;
use crate::plugins::finance_accounts::{
    account_select_route_url,
    accounting_preferences_patch::{AccountingPreferencesAddon, str_to_opt_i64, str_to_opt_string},
    logic::journal::{credit_balance_type, debit_balance_type},
    scope::{load_account_parent_label, load_journal_display_label},
};
use crate::plugins::finance_products::preferences::optional_i64;
use chrono::Utc;
use maud::{Markup, PreEscaped, html};
use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait};

use crate::plugins::filesystem::entities::filesystem_node::Entity as VNodeEntity;
use crate::plugins::finance_invoices::{
    entities::{
        payment_preferences::{self},
        preferences::{self},
    },
    forms::{
        InvoiceCompanyPreferencesForm, InvoiceCompanyPreferencesFormField,
        InvoicePdfAssetPreferencesForm, InvoicePdfAssetPreferencesFormField,
        InvoicePreferencesForm, InvoicePreferencesFormField, InvoicePresentationPreferencesForm,
        InvoicePresentationPreferencesFormField, PaymentPreferencesForm,
        PaymentPreferencesFormField,
    },
    invoice_pdf_template::DEFAULT_INVOICE_PDF_TEMPLATE,
    logic::preferences::{load_invoice_preferences, load_payment_preferences},
    preferences_hints::{
        INVOICE_DATE_FORMAT_HINT, INVOICE_DATETIME_FORMAT_HINT, INVOICE_NUMBER_FORMAT_HINT,
        INVOICE_PDF_TEMPLATE_HINT,
    },
};

async fn load_vnode_display(db: &DatabaseConnection, id: Option<i64>) -> String {
    let Some(id) = id.filter(|&id| id > 0) else {
        return String::new();
    };
    crate::web::opt_or_log(VNodeEntity::find_by_id(id).one(db).await, "find by id")
        .map(|n| n.name)
        .unwrap_or_default()
}

fn fk_value(id: Option<i64>) -> String {
    optional_i64(id).to_string()
}

pub(crate) struct InvoicesAccountingPreferencesAddon;

#[async_trait::async_trait]
impl AccountingPreferencesAddon for InvoicesAccountingPreferencesAddon {
    fn id(&self) -> &'static str {
        "finance-invoices"
    }

    async fn render_inputs(&self, db: &DatabaseConnection) -> Markup {
        use crate::html_form::{FormCtx, HtmlForm};

        let inv = load_invoice_preferences(db).await;
        let pay = load_payment_preferences(db).await;

        let ar_display = load_account_parent_label(db, inv.account_receivable_id).await;
        let revenue_display = load_account_parent_label(db, inv.account_revenue_id).await;
        let tax_display = load_account_parent_label(db, inv.account_tax_payable_id).await;
        let journal_display = load_journal_display_label(db, inv.journal_id).await;
        let payment_display = load_account_parent_label(db, pay.payment_account_id).await;

        let debit_url = account_select_route_url(debit_balance_type().as_str());
        let credit_url = account_select_route_url(credit_balance_type().as_str());

        let number_format = inv.invoice_number_format.unwrap_or_default();
        let date_format = inv.invoice_date_format.unwrap_or_default();
        let datetime_format = inv.invoice_datetime_format.unwrap_or_default();
        let pdf_template = inv.invoice_pdf_template.unwrap_or_default();
        let logo_display = load_vnode_display(db, inv.invoice_logo_vnode_id).await;
        let signature_display = load_vnode_display(db, inv.invoice_signature_vnode_id).await;

        html! {
            (label_hint(
                "Invoice number format",
                Some(INVOICE_NUMBER_FORMAT_HINT),
                html! {
                    input type="text"
                        name=(InvoicePresentationPreferencesFormField::InvoiceNumberFormat.html_name())
                        class="input input-bordered w-full"
                        value=(number_format) {}
                },
            ))
            (label_hint(
                "Invoice date format",
                Some(INVOICE_DATE_FORMAT_HINT),
                html! {
                    input type="text"
                        name=(InvoicePresentationPreferencesFormField::InvoiceDateFormat.html_name())
                        class="input input-bordered w-full"
                        value=(date_format)
                        placeholder="%d/%m/%Y" {}
                },
            ))
            (label_hint(
                "Invoice datetime format",
                Some(INVOICE_DATETIME_FORMAT_HINT),
                html! {
                    input type="text"
                        name=(InvoicePresentationPreferencesFormField::InvoiceDatetimeFormat.html_name())
                        class="input input-bordered w-full"
                        value=(datetime_format)
                        placeholder="%d/%m/%Y" {}
                },
            ))
            (InvoicePdfAssetPreferencesForm::render_inputs(
                &FormCtx::form::<InvoicePdfAssetPreferencesForm>()
                    .value(
                        InvoicePdfAssetPreferencesFormField::InvoiceLogoVnodeId,
                        fk_value(inv.invoice_logo_vnode_id),
                    )
                    .display(
                        InvoicePdfAssetPreferencesFormField::InvoiceLogoVnodeId,
                        &logo_display,
                    )
                    .value(
                        InvoicePdfAssetPreferencesFormField::InvoiceSignatureVnodeId,
                        fk_value(inv.invoice_signature_vnode_id),
                    )
                    .display(
                        InvoicePdfAssetPreferencesFormField::InvoiceSignatureVnodeId,
                        &signature_display,
                    ),
            ))
            (InvoiceCompanyPreferencesForm::render_inputs(
                &FormCtx::form::<InvoiceCompanyPreferencesForm>()
                    .value(
                        InvoiceCompanyPreferencesFormField::CompanyName,
                        inv.company_name.as_deref().unwrap_or_default(),
                    )
                    .value(
                        InvoiceCompanyPreferencesFormField::CompanyAddress,
                        inv.company_address.as_deref().unwrap_or_default(),
                    )
                    .value(
                        InvoiceCompanyPreferencesFormField::CompanyPhone,
                        inv.company_phone.as_deref().unwrap_or_default(),
                    )
                    .value(
                        InvoiceCompanyPreferencesFormField::CompanyGstin,
                        inv.company_gstin.as_deref().unwrap_or_default(),
                    )
                    .value(
                        InvoiceCompanyPreferencesFormField::PlaceOfSupply,
                        inv.place_of_supply.as_deref().unwrap_or_default(),
                    ),
            ))
            (label_hint(
                "Invoice PDF template (Typst + Minijinja)",
                Some(INVOICE_PDF_TEMPLATE_HINT),
                html! {
                    (code_editor_input(CodeEditorInput {
                        label: "",
                        name: InvoicePresentationPreferencesFormField::InvoicePdfTemplate.html_name(),
                        value: &pdf_template,
                        id: "invoice-pdf-template-field",
                        language: "plaintext",
                        rows: 16,
                        max_height: "24rem",
                        ..Default::default()
                    }))
                    textarea id="default-invoice-pdf-template" hidden readonly {
                        (DEFAULT_INVOICE_PDF_TEMPLATE)
                    }
                    div class="flex justify-end gap-2 mt-2" {
                        button type="button" class="btn btn-ghost btn-sm"
                            onclick="if (confirm('This will overwrite the template in the field with the default example template. Continue?')) { const ta = document.getElementById('invoice-pdf-template-field'); const def = document.getElementById('default-invoice-pdf-template'); if (!ta || !def) return; ta.value = def.value; const root = ta.closest('[data-code-editor-root]'); if (root) { root.dispatchEvent(new CustomEvent('code-editor:set', { detail: { value: def.value } })); } else { ta.dispatchEvent(new Event('change', { bubbles: true })); } }" {
                            "Use default template"
                        }
                        div class="fk-modal-host" {
                            (PreEscaped(format!(
                                r#"<button type="button" class="btn btn-outline btn-sm" hx-post="/finance-invoices/invoice-pdf-preview" hx-target="{}" hx-swap="{}" hx-include="closest form" hx-push-url="false">Preview sample PDF</button>"#,
                                escape_attr(HTMX_TARGET_BODY_MODAL),
                                escape_attr(HTMX_SWAP_BODY_MODAL),
                            )))
                        }
                    }
                },
            ))
            (InvoicePreferencesForm::render_inputs(
                &FormCtx::form::<InvoicePreferencesForm>()
                    .value(
                        InvoicePreferencesFormField::AccountReceivableId,
                        fk_value(inv.account_receivable_id),
                    )
                    .display(InvoicePreferencesFormField::AccountReceivableId, &ar_display)
                    .url(InvoicePreferencesFormField::AccountReceivableId, &debit_url)
                    .value(
                        InvoicePreferencesFormField::AccountRevenueId,
                        fk_value(inv.account_revenue_id),
                    )
                    .display(InvoicePreferencesFormField::AccountRevenueId, &revenue_display)
                    .url(InvoicePreferencesFormField::AccountRevenueId, &credit_url)
                    .value(
                        InvoicePreferencesFormField::AccountTaxPayableId,
                        fk_value(inv.account_tax_payable_id),
                    )
                    .display(
                        InvoicePreferencesFormField::AccountTaxPayableId,
                        &tax_display,
                    )
                    .url(InvoicePreferencesFormField::AccountTaxPayableId, &credit_url)
                    .value(
                        InvoicePreferencesFormField::JournalId,
                        fk_value(inv.journal_id),
                    )
                    .display(InvoicePreferencesFormField::JournalId, &journal_display),
            ))
            (PaymentPreferencesForm::render_inputs(
                &FormCtx::form::<PaymentPreferencesForm>()
                    .value(
                        PaymentPreferencesFormField::PaymentAccountId,
                        fk_value(pay.payment_account_id),
                    )
                    .display(PaymentPreferencesFormField::PaymentAccountId, &payment_display)
                    .url(PaymentPreferencesFormField::PaymentAccountId, &debit_url),
            ))
        }
    }

    async fn save_from_form(
        &self,
        db: &DatabaseConnection,
        post: &crate::plugins::finance_accounts::accounting_preferences_patch::AccountingPreferencesPost,
    ) -> Result<(), String> {
        let inv_form = post
            .deserialize::<InvoicePreferencesForm>()
            .map_err(|e| e.to_string())?;
        let presentation = post
            .deserialize::<InvoicePresentationPreferencesForm>()
            .map_err(|e| e.to_string())?;
        let assets = post
            .deserialize::<InvoicePdfAssetPreferencesForm>()
            .map_err(|e| e.to_string())?;
        let company = post
            .deserialize::<InvoiceCompanyPreferencesForm>()
            .map_err(|e| e.to_string())?;
        let payment = post
            .deserialize::<PaymentPreferencesForm>()
            .map_err(|e| e.to_string())?;

        let now = Utc::now();

        let inv_prefs = load_invoice_preferences(db).await;
        let mut inv_am: preferences::ActiveModel = inv_prefs.into();
        inv_am.account_receivable_id = Set(str_to_opt_i64(&inv_form.account_receivable_id));
        inv_am.account_revenue_id = Set(str_to_opt_i64(&inv_form.account_revenue_id));
        inv_am.account_tax_payable_id = Set(str_to_opt_i64(&inv_form.account_tax_payable_id));
        inv_am.journal_id = Set(str_to_opt_i64(&inv_form.journal_id));
        inv_am.invoice_number_format = Set(str_to_opt_string(&presentation.invoice_number_format));
        inv_am.invoice_date_format = Set(str_to_opt_string(&presentation.invoice_date_format));
        inv_am.invoice_datetime_format =
            Set(str_to_opt_string(&presentation.invoice_datetime_format));
        inv_am.invoice_logo_vnode_id = Set(str_to_opt_i64(&assets.invoice_logo_vnode_id));
        inv_am.invoice_signature_vnode_id = Set(str_to_opt_i64(&assets.invoice_signature_vnode_id));
        inv_am.company_name = Set(str_to_opt_string(&company.company_name));
        inv_am.company_address = Set(str_to_opt_string(&company.company_address));
        inv_am.company_phone = Set(str_to_opt_string(&company.company_phone));
        inv_am.company_gstin = Set(str_to_opt_string(&company.company_gstin));
        inv_am.place_of_supply = Set(str_to_opt_string(&company.place_of_supply));
        inv_am.invoice_pdf_template = Set(str_to_opt_string(&presentation.invoice_pdf_template));
        inv_am.updated_at = Set(Some(now));
        inv_am.update(db).await.map_err(|e| e.to_string())?;

        let pay_prefs = load_payment_preferences(db).await;
        let mut pay_am: payment_preferences::ActiveModel = pay_prefs.into();
        pay_am.payment_account_id = Set(str_to_opt_i64(&payment.payment_account_id));
        pay_am.updated_at = Set(Some(now));
        pay_am.update(db).await.map_err(|e| e.to_string())?;

        Ok(())
    }
}

pub(crate) static INVOICES_ADDON: InvoicesAccountingPreferencesAddon =
    InvoicesAccountingPreferencesAddon;