Goods receipts and warehouse receipts now capture party vs warehouse separately; purchase returns link to invoices and reduce AP. Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
8.4 KiB
TypeScript
188 lines
8.4 KiB
TypeScript
"use client";
|
||
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { toast } from "sonner";
|
||
import { accountingApi } from "@/lib/accounting-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import {
|
||
PageHeader,
|
||
LoadingState,
|
||
ErrorState,
|
||
Card,
|
||
CardContent,
|
||
CardHeader,
|
||
CardTitle,
|
||
Button,
|
||
Checkbox,
|
||
Select,
|
||
Badge,
|
||
} from "@/components/ds";
|
||
|
||
const AUTO_KEYS: { key: string; label: string; hint: string }[] = [
|
||
{ key: "sales_invoice", label: "فاکتور فروش", hint: "با تأیید فاکتور فروش، سند GL خودکار صادر شود" },
|
||
{ key: "sales_return", label: "مرجوعی فروش", hint: "ثبت خودکار برگشت فروش در دفتر" },
|
||
{ key: "purchase_goods_receipt", label: "رسید کالا", hint: "Dr موجودی / Cr GRNI هنگام تأیید رسید" },
|
||
{ key: "purchase_invoice", label: "فاکتور خرید", hint: "ثبت بدهی تأمینکننده هنگام تأیید فاکتور" },
|
||
{ key: "purchase_return", label: "مرجوعی خرید", hint: "وصل به فاکتور خرید — کاهش بدهی تأمینکننده (Dr AP / Cr موجودی)" },
|
||
{ key: "treasury_receipt", label: "دریافت خزانه", hint: "دریافت نقدی/بانکی همیشه به Posting Engine" },
|
||
{ key: "treasury_payment", label: "پرداخت خزانه", hint: "پرداخت نقدی/بانکی همیشه به Posting Engine" },
|
||
{ key: "treasury_transfer", label: "انتقال بین حسابها", hint: "از هر حساب به هر حساب — سند اتومات Dr مقصد / Cr مبدأ" },
|
||
{ key: "inventory_issue", label: "خروج / هزینهکرد کالا", hint: "خیرات، مصرف، هزینه، تهاتر با سند اتومات" },
|
||
{ key: "payroll", label: "حقوق و دستمزد", hint: "پس از محاسبه/تأیید حقوق" },
|
||
{ key: "assets_depreciation", label: "استهلاک دارایی", hint: "ثبت خودکار استهلاک دورهای" },
|
||
];
|
||
|
||
const POLICY_KEYS: { key: string; label: string; hint: string }[] = [
|
||
{
|
||
key: "require_balanced",
|
||
label: "اجبار تراز بدهکار/بستانکار",
|
||
hint: "سند نامتوازن هرگز ذخیره یا قطعی نشود (استاندارد دوبلانتری)",
|
||
},
|
||
{
|
||
key: "require_source_reference",
|
||
label: "الزام مرجع سند مبدأ",
|
||
hint: "اسناد اتومات باید source_module و reference داشته باشند",
|
||
},
|
||
{
|
||
key: "lock_after_post",
|
||
label: "قفل پس از ثبت قطعی",
|
||
hint: "ویرایش مستقیم ممنوع؛ فقط برگشت از طریق Posting Engine",
|
||
},
|
||
{
|
||
key: "sequential_numbers",
|
||
label: "شمارهگذاری متوالی",
|
||
hint: "استفاده از Document Number Sequence بهجای شناسه تصادفی",
|
||
},
|
||
{
|
||
key: "idempotent_source",
|
||
label: "جلوگیری از ثبت تکراری",
|
||
hint: "یک سند مبدأ بیش از یک بار به GL نرود",
|
||
},
|
||
];
|
||
|
||
export default function AutoPostingSettingsPage() {
|
||
const { tenantId } = useTenantId();
|
||
const qc = useQueryClient();
|
||
|
||
const policyQ = useQuery({
|
||
queryKey: ["accounting", tenantId, "posting-policy"],
|
||
queryFn: () => accountingApi.setup.getPostingPolicy(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
const saveM = useMutation({
|
||
mutationFn: (body: Parameters<typeof accountingApi.setup.updatePostingPolicy>[1]) =>
|
||
accountingApi.setup.updatePostingPolicy(tenantId!, body),
|
||
onSuccess: async () => {
|
||
toast.success("سیاست اسناد اتومات ذخیره شد");
|
||
await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "posting-policy"] });
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
if (!tenantId || policyQ.isLoading) return <LoadingState />;
|
||
if (policyQ.error) {
|
||
return <ErrorState message={policyQ.error.message} onRetry={() => policyQ.refetch()} />;
|
||
}
|
||
|
||
const policy = policyQ.data!;
|
||
|
||
const toggleAuto = (key: string, value: boolean) => {
|
||
saveM.mutate({
|
||
auto_post: { ...policy.auto_post, [key]: value },
|
||
});
|
||
};
|
||
|
||
const togglePolicy = (key: string, value: boolean) => {
|
||
saveM.mutate({ [key]: value } as Parameters<typeof accountingApi.setup.updatePostingPolicy>[1]);
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
title="اسناد اتوماتیک حسابداری"
|
||
description="کنترل ثبت خودکار در دفتر کل مطابق استانداردهای بینالمللی دوبلانتری: تراز، قفل پس از ثبت، شماره متوالی، مرجع مبدأ و جلوگیری از ثبت تکراری."
|
||
/>
|
||
|
||
<Card className="mb-6">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
حالت ثبت
|
||
<Badge tone="primary">{policy.posting_mode === "direct_post" ? "ثبت مستقیم" : "پیشنویس سپس قطعی"}</Badge>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="max-w-md space-y-2">
|
||
<Select
|
||
value={policy.posting_mode}
|
||
disabled={saveM.isPending}
|
||
onChange={(e) => saveM.mutate({ posting_mode: e.target.value })}
|
||
>
|
||
<option value="direct_post">ثبت مستقیم در دفتر (پس از تأیید سند عملیاتی)</option>
|
||
<option value="draft_then_post">ایجاد پیشنویس و قطعیسازی جداگانه</option>
|
||
</Select>
|
||
<p className="text-xs text-[var(--muted)]">
|
||
در حالت مستقیم، با تأیید سند عملیاتی (اگر ماژول روشن باشد) سند GL بلافاصله از طریق Posting Engine صادر میشود.
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="mb-6 grid gap-4 lg:grid-cols-2">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>قواعد الزامی (Compliance)</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{POLICY_KEYS.map((item) => (
|
||
<div key={item.key} className="flex items-start gap-3 rounded-xl border border-[var(--border)] p-3">
|
||
<Checkbox
|
||
checked={Boolean((policy as Record<string, unknown>)[item.key])}
|
||
disabled={saveM.isPending}
|
||
onChange={(e) => togglePolicy(item.key, e.target.checked)}
|
||
aria-label={item.label}
|
||
/>
|
||
<span>
|
||
<span className="block text-sm font-medium text-secondary">{item.label}</span>
|
||
<span className="mt-0.5 block text-xs text-[var(--muted)]">{item.hint}</span>
|
||
</span>
|
||
</div>
|
||
))}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>ثبت خودکار به تفکیک ماژول</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{AUTO_KEYS.map((item) => (
|
||
<div key={item.key} className="flex items-start gap-3 rounded-xl border border-[var(--border)] p-3">
|
||
<Checkbox
|
||
checked={Boolean(policy.auto_post?.[item.key])}
|
||
disabled={saveM.isPending}
|
||
onChange={(e) => toggleAuto(item.key, e.target.checked)}
|
||
aria-label={item.label}
|
||
/>
|
||
<span>
|
||
<span className="block text-sm font-medium text-secondary">{item.label}</span>
|
||
<span className="mt-0.5 block text-xs text-[var(--muted)]">{item.hint}</span>
|
||
</span>
|
||
</div>
|
||
))}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardContent className="flex flex-wrap items-center justify-between gap-3 pt-6">
|
||
<p className="text-sm text-[var(--muted)]">
|
||
قلب حسابداری فقط از مسیر Posting Engine میگذرد (ADR-010). تنظیمات بالا فقط زمان و شرط صدور را کنترل میکنند.
|
||
</p>
|
||
<Button type="button" variant="outline" disabled={saveM.isPending} onClick={() => policyQ.refetch()}>
|
||
بازخوانی
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|