2026-08-29 21:06:30 +03:30
|
|
|
|
<script setup lang="ts">
|
2026-09-05 17:53:50 +03:30
|
|
|
|
import { computed, onMounted, ref, watch } from 'vue';
|
2026-10-06 15:04:46 +03:30
|
|
|
|
import { matchesSearch } from '@/lib/search';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
import { api } from '@/lib/api';
|
2026-09-05 17:53:50 +03:30
|
|
|
|
import {
|
|
|
|
|
|
CURRENCY_OPTIONS,
|
|
|
|
|
|
currencySuffix,
|
|
|
|
|
|
formatMoney as formatMoneyLib,
|
|
|
|
|
|
} from '@/lib/money';
|
|
|
|
|
|
import { formatLocalDateInput, toFaDigits } from '@/lib/jalali';
|
2026-10-18 19:39:59 +03:30
|
|
|
|
import {
|
|
|
|
|
|
Affix as AAffix,
|
|
|
|
|
|
Alert as AAlert,
|
|
|
|
|
|
Button as AButton,
|
|
|
|
|
|
Card as ACard,
|
|
|
|
|
|
Col as ACol,
|
|
|
|
|
|
Collapse as ACollapse,
|
|
|
|
|
|
CollapsePanel as ACollapsePanel,
|
|
|
|
|
|
Descriptions as ADescriptions,
|
|
|
|
|
|
DescriptionsItem as ADescriptionsItem,
|
|
|
|
|
|
Divider as ADivider,
|
|
|
|
|
|
Flex as AFlex,
|
|
|
|
|
|
Form as AForm,
|
|
|
|
|
|
FormItem as AFormItem,
|
|
|
|
|
|
Input as AInput,
|
|
|
|
|
|
InputNumber as AInputNumber,
|
|
|
|
|
|
Row as ARow,
|
|
|
|
|
|
Segmented as ASegmented,
|
|
|
|
|
|
Select as ASelect,
|
|
|
|
|
|
Space as ASpace,
|
|
|
|
|
|
Spin as ASpin,
|
|
|
|
|
|
Textarea as ATextarea,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
} from 'ant-design-vue';
|
|
|
|
|
|
import {
|
|
|
|
|
|
ArrowLeftOutlined,
|
|
|
|
|
|
CheckOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
UserAddOutlined,
|
|
|
|
|
|
} from '@ant-design/icons-vue';
|
2026-09-02 21:29:00 +03:30
|
|
|
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
2026-09-05 02:13:39 +03:30
|
|
|
|
import MoneyInput from '@/components/MoneyInput.vue';
|
2026-10-18 19:39:59 +03:30
|
|
|
|
import InvoiceDocPreview from '@/components/InvoiceDocPreview.vue';
|
2026-09-11 01:09:59 +03:30
|
|
|
|
import { useAuthStore } from '@/stores/auth';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
import type {
|
|
|
|
|
|
CreateInvoiceRequest,
|
|
|
|
|
|
CreatePaymentRequest,
|
2026-09-05 17:53:50 +03:30
|
|
|
|
CreateQuoteRequest,
|
|
|
|
|
|
DealDto,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
DealLineItemInput,
|
2026-10-18 19:33:09 +03:30
|
|
|
|
FinancialAccountDto,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
InvoiceDto,
|
|
|
|
|
|
InvoiceInstallmentInput,
|
|
|
|
|
|
InvoiceStatus,
|
|
|
|
|
|
OrganizationDto,
|
2026-09-11 01:09:59 +03:30
|
|
|
|
OrgUserDto,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
PaginatedResponse,
|
|
|
|
|
|
PaymentMethod,
|
|
|
|
|
|
PersonDto,
|
|
|
|
|
|
ProductDto,
|
2026-09-05 17:53:50 +03:30
|
|
|
|
QuoteDto,
|
|
|
|
|
|
QuoteStatus,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
type DocMode = 'invoice' | 'quote' | 'credit';
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
type LineDraft = {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
productId: string;
|
|
|
|
|
|
name: string;
|
2026-09-05 17:53:50 +03:30
|
|
|
|
description: string;
|
|
|
|
|
|
sku: string;
|
|
|
|
|
|
kind: 'product' | 'service' | '';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
quantity: string;
|
|
|
|
|
|
unitPrice: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type InstallmentDraft = {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
dueDate: string;
|
|
|
|
|
|
amount: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-10 01:22:24 +03:30
|
|
|
|
const props = withDefaults(
|
|
|
|
|
|
defineProps<{
|
|
|
|
|
|
embedded?: boolean;
|
|
|
|
|
|
entityId?: string;
|
|
|
|
|
|
initialKind?: 'invoice' | 'credit' | 'quote';
|
|
|
|
|
|
initialFromDealId?: string;
|
|
|
|
|
|
}>(),
|
|
|
|
|
|
{ embedded: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
const emit = defineEmits<{ saved: []; cancel: [] }>();
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const route = useRoute();
|
|
|
|
|
|
const router = useRouter();
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const loadedInvoiceKind = ref<'invoice' | 'credit' | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const docMode = computed<DocMode>(() => {
|
2026-09-10 01:22:24 +03:30
|
|
|
|
if (props.embedded && props.initialKind) return props.initialKind;
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const name = String(route.name || '');
|
|
|
|
|
|
if (name.includes('quote')) return 'quote';
|
|
|
|
|
|
if (route.query.kind === 'credit' || route.query.doc === 'credit') return 'credit';
|
|
|
|
|
|
if (route.query.doc === 'quote') return 'quote';
|
|
|
|
|
|
if (loadedInvoiceKind.value === 'credit') return 'credit';
|
|
|
|
|
|
return 'invoice';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-10 01:22:24 +03:30
|
|
|
|
const id = computed(() => {
|
|
|
|
|
|
if (props.embedded) return props.entityId ?? '';
|
|
|
|
|
|
return typeof route.params.id === 'string' ? route.params.id : '';
|
|
|
|
|
|
});
|
|
|
|
|
|
const isNew = computed(() => {
|
|
|
|
|
|
if (props.embedded) return !props.entityId;
|
|
|
|
|
|
return route.name === 'invoice-new' || route.name === 'quote-new';
|
|
|
|
|
|
});
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const isInvoiceDoc = computed(() => docMode.value !== 'quote');
|
|
|
|
|
|
const listRouteName = 'invoices';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const saving = ref(false);
|
|
|
|
|
|
const paying = ref(false);
|
|
|
|
|
|
const error = ref('');
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const mobileStep = ref<1 | 2>(1);
|
|
|
|
|
|
const showPayments = ref(false);
|
|
|
|
|
|
const productSearch = ref('');
|
|
|
|
|
|
const validityMode = ref<'days' | 'date'>('days');
|
|
|
|
|
|
const validityDays = ref<number>(30);
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const number = ref('');
|
|
|
|
|
|
const title = ref('فاکتور');
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const status = ref<string>('draft');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const notes = ref('');
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const issuedAt = ref(formatLocalDateInput(new Date()));
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const dueDate = ref('');
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const validUntil = ref('');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const currency = ref('IRR');
|
|
|
|
|
|
const enabledCurrencies = ref<string[]>(['IRR']);
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const discountAmount = ref('0');
|
|
|
|
|
|
const taxPercent = ref('0');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const personId = ref('');
|
|
|
|
|
|
const companyId = ref('');
|
|
|
|
|
|
const dealId = ref('');
|
|
|
|
|
|
const quoteId = ref('');
|
|
|
|
|
|
const people = ref<PersonDto[]>([]);
|
|
|
|
|
|
const products = ref<ProductDto[]>([]);
|
|
|
|
|
|
const lines = ref<LineDraft[]>([]);
|
|
|
|
|
|
const installments = ref<InstallmentDraft[]>([]);
|
|
|
|
|
|
const invoice = ref<InvoiceDto | null>(null);
|
2026-10-18 19:39:59 +03:30
|
|
|
|
const orgName = ref('');
|
|
|
|
|
|
const previewOpen = ref(false);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-10-18 19:33:09 +03:30
|
|
|
|
const payAccountId = ref('');
|
|
|
|
|
|
const financeAccounts = ref<FinancialAccountDto[]>([]);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const payAmount = ref('');
|
|
|
|
|
|
const payMethod = ref<PaymentMethod>('transfer');
|
|
|
|
|
|
const payNotes = ref('');
|
|
|
|
|
|
const payInstallmentId = ref('');
|
2026-09-02 21:29:00 +03:30
|
|
|
|
const payDate = ref(formatLocalDateInput(new Date()));
|
2026-09-11 01:09:59 +03:30
|
|
|
|
const chequeNumber = ref('');
|
|
|
|
|
|
const chequeBank = ref('');
|
|
|
|
|
|
const chequeDue = ref('');
|
|
|
|
|
|
const chequeStatus = ref<'pending' | 'cleared' | 'bounced'>('pending');
|
|
|
|
|
|
const commissionMode = ref<'percent' | 'amount'>('percent');
|
|
|
|
|
|
const commissionPercent = ref('');
|
|
|
|
|
|
const commissionFixedAmount = ref('');
|
|
|
|
|
|
const defaultCommissionPercent = ref(0);
|
|
|
|
|
|
const auth = useAuthStore();
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const invoiceStatusLabel: Record<InvoiceStatus, string> = {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
draft: 'پیشنویس',
|
|
|
|
|
|
issued: 'صادرشده',
|
|
|
|
|
|
partially_paid: 'پرداخت جزئی',
|
2026-09-05 17:53:50 +03:30
|
|
|
|
paid: 'پرداخت شده',
|
|
|
|
|
|
canceled: 'لغو شده',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const quoteStatusLabel: Record<QuoteStatus, string> = {
|
|
|
|
|
|
draft: 'پیشنویس',
|
|
|
|
|
|
sent: 'ارسالشده',
|
|
|
|
|
|
accepted: 'تأییدشده',
|
|
|
|
|
|
rejected: 'ردشده',
|
|
|
|
|
|
expired: 'منقضی',
|
2026-08-29 21:06:30 +03:30
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const methodLabel: Record<PaymentMethod, string> = {
|
|
|
|
|
|
cash: 'نقد',
|
|
|
|
|
|
transfer: 'کارتبهکارت / حواله',
|
|
|
|
|
|
card: 'کارتخوان',
|
|
|
|
|
|
check: 'چک',
|
|
|
|
|
|
other: 'سایر',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const statusOptions = computed(() =>
|
|
|
|
|
|
docMode.value === 'quote' ? quoteStatusLabel : invoiceStatusLabel,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const pageTitle = computed(() => {
|
|
|
|
|
|
if (!isNew.value) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
number.value ||
|
|
|
|
|
|
(docMode.value === 'quote'
|
|
|
|
|
|
? 'پیش فاکتور'
|
|
|
|
|
|
: docMode.value === 'credit'
|
|
|
|
|
|
? 'فاکتور برگشتی'
|
|
|
|
|
|
: 'فاکتور')
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (docMode.value === 'quote') return 'افزودن پیش فاکتور';
|
|
|
|
|
|
if (docMode.value === 'credit') return 'افزودن فاکتور برگشتی';
|
|
|
|
|
|
return 'افزودن فاکتور';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const pageSubtitle = computed(() => {
|
|
|
|
|
|
if (mobileStep.value === 2) return 'محصولات و خدمات را اضافه کنید';
|
|
|
|
|
|
if (docMode.value === 'quote') return 'اطلاعات پیش فاکتور را وارد کنید';
|
|
|
|
|
|
if (docMode.value === 'credit') return 'اطلاعات فاکتور برگشتی را وارد کنید';
|
|
|
|
|
|
return 'اطلاعات فاکتور را وارد کنید';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const defaultTitle = computed(() => {
|
|
|
|
|
|
if (docMode.value === 'quote') return 'پیش فاکتور';
|
|
|
|
|
|
if (docMode.value === 'credit') return 'فاکتور برگشتی';
|
|
|
|
|
|
return 'فاکتور';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const saveLabel = computed(() => {
|
|
|
|
|
|
if (docMode.value === 'quote') return 'ذخیره پیش فاکتور';
|
|
|
|
|
|
if (docMode.value === 'credit') return 'ذخیره فاکتور برگشتی';
|
|
|
|
|
|
return 'ذخیره فاکتور';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const currencyLabel = computed(() => currencySuffix(currency.value));
|
|
|
|
|
|
|
|
|
|
|
|
const currencySelectOptions = computed(() => {
|
|
|
|
|
|
const codes = enabledCurrencies.value.length
|
|
|
|
|
|
? [...enabledCurrencies.value]
|
|
|
|
|
|
: [currency.value || 'IRR'];
|
|
|
|
|
|
if (currency.value && !codes.includes(currency.value)) {
|
|
|
|
|
|
codes.push(currency.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
return codes.map((code) => {
|
|
|
|
|
|
const opt = CURRENCY_OPTIONS.find((c) => c.code === code);
|
|
|
|
|
|
return {
|
|
|
|
|
|
code,
|
|
|
|
|
|
label: opt ? `${opt.suffix} - ${opt.code}` : code,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const filteredProducts = computed(() => {
|
2026-10-06 15:04:46 +03:30
|
|
|
|
const q = productSearch.value.trim();
|
2026-09-05 17:53:50 +03:30
|
|
|
|
if (!q) return products.value;
|
2026-10-06 15:04:46 +03:30
|
|
|
|
return products.value.filter((p) =>
|
|
|
|
|
|
matchesSearch(q, [p.name, p.sku, p.description, p.category]),
|
2026-09-05 17:53:50 +03:30
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function roundMoney(value: number): number {
|
|
|
|
|
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function lineTotal(line: LineDraft): number {
|
|
|
|
|
|
const qty = Number(line.quantity) || 0;
|
|
|
|
|
|
const price = Number(line.unitPrice) || 0;
|
|
|
|
|
|
return qty * price;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const subtotal = computed(() =>
|
|
|
|
|
|
lines.value.reduce((sum, line) => sum + lineTotal(line), 0),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const discountNum = computed(() => Number(discountAmount.value) || 0);
|
|
|
|
|
|
const taxPercentNum = computed(() => Number(taxPercent.value) || 0);
|
|
|
|
|
|
|
|
|
|
|
|
const taxAmount = computed(() =>
|
|
|
|
|
|
roundMoney(
|
|
|
|
|
|
Math.max(0, subtotal.value - discountNum.value) * (taxPercentNum.value / 100),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const payableSigned = computed(() =>
|
|
|
|
|
|
roundMoney(subtotal.value - discountNum.value + taxAmount.value),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const payableAmount = computed(() =>
|
|
|
|
|
|
docMode.value === 'credit' ? -payableSigned.value : payableSigned.value,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
);
|
|
|
|
|
|
|
2026-09-11 01:09:59 +03:30
|
|
|
|
const commissionPreview = computed(() => {
|
|
|
|
|
|
const base = Math.abs(payableAmount.value);
|
|
|
|
|
|
if (commissionMode.value === 'amount') {
|
|
|
|
|
|
const n = Number(commissionFixedAmount.value);
|
|
|
|
|
|
return Number.isFinite(n) ? n : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
const n = Number(commissionPercent.value || defaultCommissionPercent.value);
|
|
|
|
|
|
return Number.isFinite(n) ? Math.round((base * n) / 100) : 0;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
function formatMoney(amount: number) {
|
|
|
|
|
|
return formatMoneyLib(amount, currency.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function newLine(): LineDraft {
|
|
|
|
|
|
return {
|
|
|
|
|
|
key: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
|
|
|
|
productId: '',
|
|
|
|
|
|
name: '',
|
2026-09-05 17:53:50 +03:30
|
|
|
|
description: '',
|
|
|
|
|
|
sku: '',
|
|
|
|
|
|
kind: '',
|
2026-08-29 21:06:30 +03:30
|
|
|
|
quantity: '1',
|
|
|
|
|
|
unitPrice: '',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function newInstallment(): InstallmentDraft {
|
|
|
|
|
|
return {
|
|
|
|
|
|
key: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
|
|
|
|
dueDate: '',
|
|
|
|
|
|
amount: '',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addLine() {
|
|
|
|
|
|
lines.value.push(newLine());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeLine(key: string) {
|
|
|
|
|
|
lines.value = lines.value.filter((l) => l.key !== key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onPickProduct(line: LineDraft) {
|
|
|
|
|
|
const product = products.value.find((p) => p.id === line.productId);
|
|
|
|
|
|
if (!product) return;
|
|
|
|
|
|
line.name = product.name;
|
2026-09-05 17:53:50 +03:30
|
|
|
|
line.description = product.description ?? '';
|
|
|
|
|
|
line.sku = product.sku ?? '';
|
|
|
|
|
|
line.kind = product.kind;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
if (product.unitPrice != null) line.unitPrice = String(product.unitPrice);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
function onPersonChange() {
|
|
|
|
|
|
const person = people.value.find((p) => p.id === personId.value);
|
|
|
|
|
|
companyId.value = person?.company?.id ?? '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
function toLineInputs(): DealLineItemInput[] {
|
|
|
|
|
|
return lines.value
|
|
|
|
|
|
.filter((l) => l.name.trim() && l.quantity !== '' && l.unitPrice !== '')
|
|
|
|
|
|
.map((l) => ({
|
|
|
|
|
|
productId: l.productId || null,
|
|
|
|
|
|
name: l.name.trim(),
|
|
|
|
|
|
quantity: Number(l.quantity),
|
|
|
|
|
|
unitPrice: Number(l.unitPrice),
|
|
|
|
|
|
currency: currency.value || 'IRR',
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toInstallmentInputs(): InvoiceInstallmentInput[] {
|
|
|
|
|
|
return installments.value
|
|
|
|
|
|
.filter((i) => i.dueDate && i.amount !== '')
|
|
|
|
|
|
.map((i) => ({
|
|
|
|
|
|
dueDate: new Date(i.dueDate).toISOString(),
|
|
|
|
|
|
amount: Number(i.amount),
|
|
|
|
|
|
currency: currency.value || 'IRR',
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
function applyValidityDays(days: number) {
|
|
|
|
|
|
validityMode.value = 'days';
|
|
|
|
|
|
validityDays.value = days;
|
|
|
|
|
|
const base = issuedAt.value ? new Date(issuedAt.value) : new Date();
|
|
|
|
|
|
const d = new Date(base);
|
|
|
|
|
|
d.setDate(d.getDate() + days);
|
|
|
|
|
|
validUntil.value = formatLocalDateInput(d);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function keepPrefillQuery() {
|
|
|
|
|
|
const q: Record<string, string> = {};
|
|
|
|
|
|
for (const key of ['fromDealId', 'fromQuoteId', 'personId', 'dealId'] as const) {
|
|
|
|
|
|
const v = route.query[key];
|
|
|
|
|
|
if (typeof v === 'string' && v) q[key] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
return q;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function switchDocMode(mode: DocMode) {
|
|
|
|
|
|
if (!isNew.value || mode === docMode.value) return;
|
|
|
|
|
|
const q = keepPrefillQuery();
|
|
|
|
|
|
if (mode === 'quote') {
|
|
|
|
|
|
void router.push({ name: 'quote-new', query: q });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mode === 'credit') {
|
|
|
|
|
|
void router.push({ name: 'invoice-new', query: { ...q, kind: 'credit' } });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
void router.push({ name: 'invoice-new', query: q });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mapLinesFromItems(
|
|
|
|
|
|
items: Array<{
|
|
|
|
|
|
id?: string;
|
|
|
|
|
|
productId?: string | null;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
quantity: number;
|
|
|
|
|
|
unitPrice: number;
|
|
|
|
|
|
}>,
|
|
|
|
|
|
): LineDraft[] {
|
|
|
|
|
|
return items.map((item, index) => {
|
|
|
|
|
|
const product = products.value.find((p) => p.id === item.productId);
|
|
|
|
|
|
return {
|
|
|
|
|
|
key: item.id || `line-${index}-${Date.now()}`,
|
|
|
|
|
|
productId: item.productId ?? '',
|
|
|
|
|
|
name: item.name,
|
|
|
|
|
|
description: product?.description ?? '',
|
|
|
|
|
|
sku: product?.sku ?? '',
|
|
|
|
|
|
kind: product?.kind ?? '',
|
|
|
|
|
|
quantity: String(item.quantity),
|
|
|
|
|
|
unitPrice: String(item.unitPrice),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function prefillFromSources() {
|
|
|
|
|
|
const fromQuoteId =
|
|
|
|
|
|
typeof route.query.fromQuoteId === 'string' ? route.query.fromQuoteId : '';
|
|
|
|
|
|
const fromDealId =
|
2026-09-10 01:22:24 +03:30
|
|
|
|
props.initialFromDealId ||
|
|
|
|
|
|
(typeof route.query.fromDealId === 'string'
|
2026-09-05 17:53:50 +03:30
|
|
|
|
? route.query.fromDealId
|
|
|
|
|
|
: typeof route.query.dealId === 'string'
|
|
|
|
|
|
? route.query.dealId
|
2026-09-10 01:22:24 +03:30
|
|
|
|
: '');
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const qPersonId =
|
|
|
|
|
|
typeof route.query.personId === 'string' ? route.query.personId : '';
|
|
|
|
|
|
|
|
|
|
|
|
if (fromQuoteId) {
|
|
|
|
|
|
quoteId.value = fromQuoteId;
|
|
|
|
|
|
const { data } = await api.get<QuoteDto>(`/quotes/${fromQuoteId}`);
|
|
|
|
|
|
title.value =
|
|
|
|
|
|
docMode.value === 'credit'
|
|
|
|
|
|
? `فاکتور برگشتی — ${data.title}`
|
|
|
|
|
|
: `فاکتور — ${data.title}`;
|
|
|
|
|
|
notes.value = data.notes ?? '';
|
|
|
|
|
|
currency.value = data.currency || currency.value;
|
|
|
|
|
|
discountAmount.value = String(data.discountAmount ?? 0);
|
|
|
|
|
|
taxPercent.value = String(data.taxPercent ?? 0);
|
|
|
|
|
|
personId.value = data.person?.id ?? '';
|
|
|
|
|
|
companyId.value = data.company?.id ?? '';
|
|
|
|
|
|
dealId.value = data.deal?.id ?? '';
|
|
|
|
|
|
if (data.issuedAt) issuedAt.value = data.issuedAt.slice(0, 10);
|
|
|
|
|
|
lines.value = mapLinesFromItems(data.lineItems ?? []);
|
|
|
|
|
|
if (!lines.value.length) lines.value = [newLine()];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fromDealId) {
|
|
|
|
|
|
dealId.value = fromDealId;
|
|
|
|
|
|
const { data } = await api.get<DealDto>(`/deals/${fromDealId}`);
|
|
|
|
|
|
title.value = `${defaultTitle.value} — ${data.title}`;
|
|
|
|
|
|
notes.value = data.notes ?? '';
|
|
|
|
|
|
currency.value = data.currency || currency.value;
|
|
|
|
|
|
personId.value = data.person?.id ?? '';
|
|
|
|
|
|
companyId.value = data.company?.id ?? '';
|
|
|
|
|
|
lines.value = mapLinesFromItems(data.lineItems ?? []);
|
|
|
|
|
|
if (!lines.value.length) lines.value = [newLine()];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (qPersonId) {
|
|
|
|
|
|
personId.value = qPersonId;
|
|
|
|
|
|
onPersonChange();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!lines.value.length) lines.value = [newLine()];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
async function load() {
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
error.value = '';
|
2026-09-05 17:53:50 +03:30
|
|
|
|
mobileStep.value = 1;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
try {
|
2026-10-18 19:33:09 +03:30
|
|
|
|
const accounts = await api.get<FinancialAccountDto[]>('/treasury/accounts').catch(() => null);
|
|
|
|
|
|
financeAccounts.value = accounts?.data.filter((row) => row.isActive) ?? [];
|
2026-09-05 17:53:50 +03:30
|
|
|
|
title.value = defaultTitle.value;
|
|
|
|
|
|
status.value = 'draft';
|
|
|
|
|
|
notes.value = '';
|
|
|
|
|
|
issuedAt.value = formatLocalDateInput(new Date());
|
|
|
|
|
|
dueDate.value = '';
|
|
|
|
|
|
validUntil.value = '';
|
|
|
|
|
|
discountAmount.value = '0';
|
|
|
|
|
|
taxPercent.value = '0';
|
|
|
|
|
|
personId.value = '';
|
|
|
|
|
|
companyId.value = '';
|
|
|
|
|
|
dealId.value = '';
|
|
|
|
|
|
quoteId.value = '';
|
|
|
|
|
|
number.value = '';
|
|
|
|
|
|
invoice.value = null;
|
|
|
|
|
|
loadedInvoiceKind.value = null;
|
|
|
|
|
|
installments.value = [];
|
|
|
|
|
|
lines.value = [];
|
2026-09-11 01:09:59 +03:30
|
|
|
|
commissionMode.value = 'percent';
|
|
|
|
|
|
commissionPercent.value = '';
|
|
|
|
|
|
commissionFixedAmount.value = '';
|
2026-09-05 17:53:50 +03:30
|
|
|
|
validityMode.value = 'days';
|
|
|
|
|
|
validityDays.value = 30;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const [peopleRes, productsRes, orgRes] = await Promise.all([
|
2026-08-29 21:06:30 +03:30
|
|
|
|
api.get<PaginatedResponse<PersonDto>>('/people', {
|
|
|
|
|
|
params: { limit: 100 },
|
|
|
|
|
|
}),
|
|
|
|
|
|
api.get<ProductDto[]>('/products', { params: { activeOnly: true } }),
|
|
|
|
|
|
api.get<OrganizationDto>('/organizations/current'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
people.value = peopleRes.data.items;
|
|
|
|
|
|
products.value = productsRes.data;
|
2026-09-11 01:09:59 +03:30
|
|
|
|
try {
|
|
|
|
|
|
const { data: team } = await api.get<OrgUserDto[]>('/users');
|
|
|
|
|
|
const me = team.find((u) => u.id === auth.user?.id);
|
|
|
|
|
|
defaultCommissionPercent.value = Number(me?.commissionPercent ?? 0);
|
|
|
|
|
|
if (!commissionPercent.value && defaultCommissionPercent.value) {
|
|
|
|
|
|
commissionPercent.value = String(defaultCommissionPercent.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* optional */
|
|
|
|
|
|
}
|
2026-10-18 19:39:59 +03:30
|
|
|
|
orgName.value = orgRes.data.name || '';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
enabledCurrencies.value = orgRes.data.enabledCurrencies?.length
|
|
|
|
|
|
? orgRes.data.enabledCurrencies
|
|
|
|
|
|
: [orgRes.data.baseCurrency || 'IRR'];
|
|
|
|
|
|
currency.value = orgRes.data.baseCurrency || 'IRR';
|
|
|
|
|
|
|
|
|
|
|
|
if (!isNew.value && id.value) {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
if (docMode.value === 'quote') {
|
|
|
|
|
|
const { data } = await api.get<QuoteDto>(`/quotes/${id.value}`);
|
|
|
|
|
|
number.value = data.number;
|
|
|
|
|
|
title.value = data.title;
|
|
|
|
|
|
status.value = data.status;
|
|
|
|
|
|
notes.value = data.notes ?? '';
|
|
|
|
|
|
currency.value = data.currency || currency.value;
|
|
|
|
|
|
discountAmount.value = String(data.discountAmount ?? 0);
|
|
|
|
|
|
taxPercent.value = String(data.taxPercent ?? 0);
|
|
|
|
|
|
issuedAt.value = data.issuedAt
|
|
|
|
|
|
? data.issuedAt.slice(0, 10)
|
|
|
|
|
|
: formatLocalDateInput(new Date(data.createdAt));
|
|
|
|
|
|
validUntil.value = data.validUntil ? data.validUntil.slice(0, 10) : '';
|
|
|
|
|
|
if (validUntil.value) validityMode.value = 'date';
|
|
|
|
|
|
personId.value = data.person?.id ?? '';
|
|
|
|
|
|
companyId.value = data.company?.id ?? '';
|
|
|
|
|
|
dealId.value = data.deal?.id ?? '';
|
|
|
|
|
|
lines.value = mapLinesFromItems(data.lineItems ?? []);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const { data } = await api.get<InvoiceDto>(`/invoices/${id.value}`);
|
|
|
|
|
|
invoice.value = data;
|
|
|
|
|
|
loadedInvoiceKind.value = data.kind === 'credit' ? 'credit' : 'invoice';
|
|
|
|
|
|
number.value = data.number;
|
|
|
|
|
|
title.value = data.title;
|
|
|
|
|
|
status.value = data.status;
|
|
|
|
|
|
notes.value = data.notes ?? '';
|
|
|
|
|
|
currency.value = data.currency || currency.value;
|
|
|
|
|
|
discountAmount.value = String(data.discountAmount ?? 0);
|
|
|
|
|
|
taxPercent.value = String(data.taxPercent ?? 0);
|
|
|
|
|
|
issuedAt.value = data.issuedAt
|
|
|
|
|
|
? data.issuedAt.slice(0, 10)
|
|
|
|
|
|
: formatLocalDateInput(new Date(data.createdAt));
|
|
|
|
|
|
dueDate.value = data.dueDate ? data.dueDate.slice(0, 10) : '';
|
|
|
|
|
|
personId.value = data.person?.id ?? '';
|
|
|
|
|
|
companyId.value = data.company?.id ?? '';
|
|
|
|
|
|
dealId.value = data.deal?.id ?? '';
|
|
|
|
|
|
quoteId.value = data.quote?.id ?? '';
|
|
|
|
|
|
lines.value = mapLinesFromItems(data.lineItems ?? []);
|
|
|
|
|
|
installments.value = (data.installments ?? []).map((item) => ({
|
|
|
|
|
|
key: item.id,
|
|
|
|
|
|
dueDate: item.dueDate.slice(0, 10),
|
|
|
|
|
|
amount: String(item.amount),
|
|
|
|
|
|
}));
|
|
|
|
|
|
if (data.remainingAmount > 0) {
|
|
|
|
|
|
payAmount.value = String(data.remainingAmount);
|
|
|
|
|
|
}
|
2026-09-11 01:09:59 +03:30
|
|
|
|
if (data.commissionFixedAmount != null) {
|
|
|
|
|
|
commissionMode.value = 'amount';
|
|
|
|
|
|
commissionFixedAmount.value = String(data.commissionFixedAmount);
|
|
|
|
|
|
commissionPercent.value = String(
|
|
|
|
|
|
data.commissionPercent ?? defaultCommissionPercent.value,
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
commissionMode.value = 'percent';
|
|
|
|
|
|
commissionPercent.value = String(
|
|
|
|
|
|
data.commissionPercent ?? defaultCommissionPercent.value,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
2026-09-05 17:53:50 +03:30
|
|
|
|
if (!lines.value.length) lines.value = [newLine()];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await prefillFromSources();
|
|
|
|
|
|
if (docMode.value === 'quote') applyValidityDays(validityDays.value);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
error.value =
|
|
|
|
|
|
docMode.value === 'quote'
|
|
|
|
|
|
? 'بارگذاری فرم پیش فاکتور ناموفق بود.'
|
|
|
|
|
|
: 'بارگذاری فرم فاکتور ناموفق بود.';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
function invoicePayload(): CreateInvoiceRequest {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const fromDealId =
|
2026-09-10 01:22:24 +03:30
|
|
|
|
isNew.value &&
|
|
|
|
|
|
(props.initialFromDealId || typeof route.query.fromDealId === 'string')
|
|
|
|
|
|
? (props.initialFromDealId ||
|
|
|
|
|
|
(typeof route.query.fromDealId === 'string' ? route.query.fromDealId : ''))
|
2026-08-29 21:06:30 +03:30
|
|
|
|
: undefined;
|
|
|
|
|
|
const fromQuoteId =
|
|
|
|
|
|
isNew.value && typeof route.query.fromQuoteId === 'string'
|
|
|
|
|
|
? route.query.fromQuoteId
|
|
|
|
|
|
: undefined;
|
|
|
|
|
|
return {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
title: title.value.trim() || defaultTitle.value,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
status: status.value,
|
2026-09-05 17:53:50 +03:30
|
|
|
|
kind: docMode.value === 'credit' ? 'credit' : 'invoice',
|
2026-08-29 21:06:30 +03:30
|
|
|
|
notes: notes.value || undefined,
|
2026-09-05 17:53:50 +03:30
|
|
|
|
discountAmount: discountNum.value,
|
|
|
|
|
|
taxPercent: taxPercentNum.value,
|
|
|
|
|
|
issuedAt: issuedAt.value ? new Date(issuedAt.value).toISOString() : null,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
dueDate: dueDate.value ? new Date(dueDate.value).toISOString() : null,
|
|
|
|
|
|
personId: personId.value || null,
|
|
|
|
|
|
companyId: companyId.value || null,
|
|
|
|
|
|
dealId: dealId.value || null,
|
|
|
|
|
|
quoteId: quoteId.value || null,
|
|
|
|
|
|
fromDealId: fromDealId || null,
|
|
|
|
|
|
fromQuoteId: fromQuoteId || null,
|
|
|
|
|
|
lineItems: toLineInputs(),
|
|
|
|
|
|
installments: toInstallmentInputs(),
|
|
|
|
|
|
currency: currency.value || 'IRR',
|
2026-09-11 01:09:59 +03:30
|
|
|
|
commissionPercent:
|
|
|
|
|
|
commissionMode.value === 'percent'
|
|
|
|
|
|
? commissionPercent.value === ''
|
|
|
|
|
|
? null
|
|
|
|
|
|
: Number(commissionPercent.value)
|
|
|
|
|
|
: null,
|
|
|
|
|
|
commissionFixedAmount:
|
|
|
|
|
|
commissionMode.value === 'amount'
|
|
|
|
|
|
? commissionFixedAmount.value === ''
|
|
|
|
|
|
? null
|
|
|
|
|
|
: Number(commissionFixedAmount.value)
|
|
|
|
|
|
: null,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
function quotePayload(): CreateQuoteRequest {
|
|
|
|
|
|
const fromDealId =
|
2026-09-10 01:22:24 +03:30
|
|
|
|
isNew.value &&
|
|
|
|
|
|
(props.initialFromDealId || typeof route.query.fromDealId === 'string')
|
|
|
|
|
|
? (props.initialFromDealId ||
|
|
|
|
|
|
(typeof route.query.fromDealId === 'string' ? route.query.fromDealId : ''))
|
2026-09-05 17:53:50 +03:30
|
|
|
|
: undefined;
|
|
|
|
|
|
if (validityMode.value === 'days') {
|
|
|
|
|
|
applyValidityDays(validityDays.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
const body: CreateQuoteRequest = {
|
|
|
|
|
|
title: title.value.trim() || defaultTitle.value,
|
|
|
|
|
|
status: status.value,
|
|
|
|
|
|
notes: notes.value || undefined,
|
|
|
|
|
|
discountAmount: discountNum.value,
|
|
|
|
|
|
taxPercent: taxPercentNum.value,
|
|
|
|
|
|
issuedAt: issuedAt.value ? new Date(issuedAt.value).toISOString() : null,
|
|
|
|
|
|
personId: personId.value || null,
|
|
|
|
|
|
companyId: companyId.value || null,
|
|
|
|
|
|
dealId: dealId.value || null,
|
|
|
|
|
|
fromDealId: fromDealId || null,
|
|
|
|
|
|
lineItems: toLineInputs(),
|
|
|
|
|
|
currency: currency.value || 'IRR',
|
|
|
|
|
|
validUntil: validUntil.value
|
|
|
|
|
|
? new Date(validUntil.value).toISOString()
|
|
|
|
|
|
: null,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (isNew.value && validityMode.value === 'days') {
|
|
|
|
|
|
body.validityDays = validityDays.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
return body;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function axiosMessage(e: unknown, fallback: string) {
|
|
|
|
|
|
if (axios.isAxiosError(e)) {
|
|
|
|
|
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
|
|
|
|
|
return Array.isArray(msg) ? msg.join('، ') : (msg ?? fallback);
|
|
|
|
|
|
}
|
|
|
|
|
|
return fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
async function save() {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
if (!issuedAt.value) {
|
|
|
|
|
|
error.value = 'تاریخ را وارد کنید.';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!toLineInputs().length) {
|
|
|
|
|
|
error.value = 'حداقل یک ردیف محصول وارد کنید.';
|
|
|
|
|
|
mobileStep.value = 2;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
saving.value = true;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
if (docMode.value === 'quote') {
|
|
|
|
|
|
if (isNew.value) {
|
|
|
|
|
|
await api.post<QuoteDto>('/quotes', quotePayload());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await api.patch(`/quotes/${id.value}`, quotePayload());
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (isNew.value) {
|
|
|
|
|
|
await api.post<InvoiceDto>('/invoices', invoicePayload());
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} else {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
await api.patch(`/invoices/${id.value}`, invoicePayload());
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
2026-09-10 01:22:24 +03:30
|
|
|
|
if (props.embedded) {
|
|
|
|
|
|
emit('saved');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-05 17:53:50 +03:30
|
|
|
|
await router.push({ name: listRouteName });
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} catch (e) {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
error.value = axiosMessage(e, 'ذخیره ناموفق بود');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function remove() {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const label =
|
|
|
|
|
|
docMode.value === 'quote'
|
|
|
|
|
|
? 'این پیش فاکتور حذف شود؟'
|
|
|
|
|
|
: docMode.value === 'credit'
|
|
|
|
|
|
? 'این فاکتور برگشتی حذف شود؟'
|
|
|
|
|
|
: 'این فاکتور حذف شود؟';
|
|
|
|
|
|
if (!id.value || !confirm(label)) return;
|
|
|
|
|
|
if (docMode.value === 'quote') {
|
|
|
|
|
|
await api.delete(`/quotes/${id.value}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await api.delete(`/invoices/${id.value}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
await router.push({ name: listRouteName });
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitPayment() {
|
|
|
|
|
|
if (!id.value || !payAmount.value) {
|
|
|
|
|
|
error.value = 'مبلغ پرداخت را وارد کنید.';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-11 01:09:59 +03:30
|
|
|
|
if (payMethod.value === 'check' && (!chequeNumber.value.trim() || !chequeBank.value.trim() || !chequeDue.value)) {
|
|
|
|
|
|
error.value = 'شماره، بانک و سررسید چک را وارد کنید.';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
paying.value = true;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const body: CreatePaymentRequest = {
|
|
|
|
|
|
amount: Number(payAmount.value),
|
|
|
|
|
|
method: payMethod.value,
|
|
|
|
|
|
notes: payNotes.value || undefined,
|
2026-09-05 17:53:50 +03:30
|
|
|
|
paidAt: payDate.value ? new Date(payDate.value).toISOString() : undefined,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
installmentId: payInstallmentId.value || null,
|
2026-10-18 19:33:09 +03:30
|
|
|
|
financialAccountId: payAccountId.value || null,
|
2026-09-11 01:09:59 +03:30
|
|
|
|
cheque:
|
|
|
|
|
|
payMethod.value === 'check'
|
|
|
|
|
|
? {
|
|
|
|
|
|
number: chequeNumber.value.trim(),
|
|
|
|
|
|
bank: chequeBank.value.trim(),
|
|
|
|
|
|
dueDate: new Date(chequeDue.value).toISOString(),
|
|
|
|
|
|
status: chequeStatus.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
: undefined,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
};
|
|
|
|
|
|
const { data } = await api.post<InvoiceDto>(
|
|
|
|
|
|
`/invoices/${id.value}/payments`,
|
|
|
|
|
|
body,
|
|
|
|
|
|
);
|
|
|
|
|
|
invoice.value = data;
|
|
|
|
|
|
payNotes.value = '';
|
|
|
|
|
|
payInstallmentId.value = '';
|
2026-09-11 01:09:59 +03:30
|
|
|
|
chequeNumber.value = '';
|
|
|
|
|
|
chequeBank.value = '';
|
|
|
|
|
|
chequeDue.value = '';
|
|
|
|
|
|
chequeStatus.value = 'pending';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
await load();
|
|
|
|
|
|
} catch (e) {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
error.value = axiosMessage(e, 'ثبت پرداخت ناموفق بود');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
paying.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function removePayment(paymentId: string) {
|
|
|
|
|
|
if (!id.value || !confirm('این پرداخت حذف شود؟')) return;
|
|
|
|
|
|
await api.delete(`/invoices/${id.value}/payments/${paymentId}`);
|
|
|
|
|
|
await load();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
function addInstallment() {
|
|
|
|
|
|
installments.value.push(newInstallment());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeInstallment(key: string) {
|
|
|
|
|
|
installments.value = installments.value.filter((l) => l.key !== key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function splitInstallments(count: number) {
|
|
|
|
|
|
const total = Math.abs(payableAmount.value);
|
|
|
|
|
|
if (!total || count < 1) return;
|
|
|
|
|
|
const base = Math.floor(total / count);
|
|
|
|
|
|
const remainder = total - base * count;
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
installments.value = Array.from({ length: count }, (_, i) => {
|
|
|
|
|
|
const d = new Date(today);
|
|
|
|
|
|
d.setMonth(d.getMonth() + i + 1);
|
|
|
|
|
|
return {
|
|
|
|
|
|
key: `split-${i}-${Date.now()}`,
|
|
|
|
|
|
dueDate: formatLocalDateInput(d),
|
|
|
|
|
|
amount: String(base + (i === count - 1 ? remainder : 0)),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
function lineHasContent(line: LineDraft) {
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
line.name.trim() ||
|
|
|
|
|
|
line.productId ||
|
|
|
|
|
|
line.description.trim() ||
|
|
|
|
|
|
(line.unitPrice !== '' && Number.isFinite(Number(line.unitPrice))),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const previewLines = computed(() =>
|
|
|
|
|
|
lines.value.filter(lineHasContent).map((l) => {
|
|
|
|
|
|
const product = products.value.find((p) => p.id === l.productId);
|
|
|
|
|
|
const qty = Number(l.quantity);
|
|
|
|
|
|
const unit = Number(l.unitPrice);
|
|
|
|
|
|
const unitPrice = Number.isFinite(unit) && l.unitPrice !== ''
|
|
|
|
|
|
? unit
|
|
|
|
|
|
: Number(product?.unitPrice) || 0;
|
|
|
|
|
|
const quantity = Number.isFinite(qty) && l.quantity !== '' ? qty : 1;
|
|
|
|
|
|
return {
|
|
|
|
|
|
name:
|
|
|
|
|
|
l.name.trim() ||
|
|
|
|
|
|
product?.name ||
|
|
|
|
|
|
(l.productId ? 'محصول انتخابشده' : 'بدون نام'),
|
|
|
|
|
|
description: l.description?.trim() || product?.description || undefined,
|
|
|
|
|
|
sku: l.sku || product?.sku || undefined,
|
|
|
|
|
|
quantity,
|
|
|
|
|
|
unitPrice,
|
|
|
|
|
|
total: quantity * unitPrice,
|
|
|
|
|
|
};
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const previewSubtotal = computed(() =>
|
|
|
|
|
|
previewLines.value.reduce((sum, l) => sum + l.total, 0),
|
|
|
|
|
|
);
|
|
|
|
|
|
const previewTaxAmount = computed(() =>
|
|
|
|
|
|
Math.round(
|
|
|
|
|
|
(Math.max(0, previewSubtotal.value - discountNum.value) * (taxPercentNum.value / 100) +
|
|
|
|
|
|
Number.EPSILON) *
|
|
|
|
|
|
100,
|
|
|
|
|
|
) / 100,
|
|
|
|
|
|
);
|
|
|
|
|
|
const previewPayable = computed(() => {
|
|
|
|
|
|
const signed = Math.round(
|
|
|
|
|
|
(previewSubtotal.value - discountNum.value + previewTaxAmount.value + Number.EPSILON) * 100,
|
|
|
|
|
|
) / 100;
|
|
|
|
|
|
return docMode.value === 'credit' ? -signed : signed;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const previewCustomer = computed(() => {
|
|
|
|
|
|
const p = people.value.find((x) => x.id === personId.value);
|
|
|
|
|
|
if (!p) return { name: '—', sub: '' };
|
|
|
|
|
|
const full = p.fullName || `${p.firstName ?? ''} ${p.lastName ?? ''}`.trim();
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: p.company?.name || full || '—',
|
|
|
|
|
|
sub: p.company?.name ? full : '',
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const previewStatusLabel = computed(() => {
|
|
|
|
|
|
const map = statusOptions.value as Record<string, string>;
|
|
|
|
|
|
return map[status.value] || status.value || '—';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
function printDoc() {
|
2026-10-18 19:39:59 +03:30
|
|
|
|
previewOpen.value = true;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
function goBack() {
|
2026-09-10 01:22:24 +03:30
|
|
|
|
if (props.embedded) {
|
|
|
|
|
|
emit('cancel');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-05 17:53:50 +03:30
|
|
|
|
void router.push({ name: listRouteName });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onValidityDaysChange() {
|
|
|
|
|
|
applyValidityDays(validityDays.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function personLabel(p: PersonDto) {
|
|
|
|
|
|
return p.company?.name
|
|
|
|
|
|
? `${p.fullName} — ${p.company.name}`
|
|
|
|
|
|
: p.fullName;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const pendingInstallments = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
invoice.value?.installments.filter((i) => i.status === 'pending') ?? [],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const showPaymentBlock = computed(
|
|
|
|
|
|
() => !isNew.value && isInvoiceDoc.value && !!invoice.value,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => [route.name, route.params.id, route.query.kind, route.query.doc],
|
|
|
|
|
|
() => {
|
|
|
|
|
|
void load();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
|
|
|
|
|
|
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
const personSelectOptions = computed(() =>
|
|
|
|
|
|
people.value.map((p) => ({ value: p.id, label: personLabel(p) })),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const statusSelectOptions = computed(() =>
|
|
|
|
|
|
Object.entries(statusOptions.value).map(([value, label]) => ({ value, label })),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const currencyAntOptions = computed(() =>
|
|
|
|
|
|
currencySelectOptions.value.map((o) => ({ value: o.code, label: o.label })),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const productSelectOptions = computed(() =>
|
|
|
|
|
|
filteredProducts.value.map((p) => ({ value: p.id, label: p.name })),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const validityDayOptions = computed(() =>
|
|
|
|
|
|
[7, 15, 30, 60].map((d) => ({ value: d, label: `${toFaDigits(d)} روز` })),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const methodSelectOptions = computed(() =>
|
|
|
|
|
|
(Object.entries(methodLabel) as Array<[PaymentMethod, string]>).map(
|
|
|
|
|
|
([value, label]) => ({ value, label }),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const payInstallmentOptions = computed(() => [
|
|
|
|
|
|
{ value: '', label: 'بدون اتصال به قسط' },
|
|
|
|
|
|
...pendingInstallments.value.map((inst) => ({
|
|
|
|
|
|
value: inst.id,
|
|
|
|
|
|
label: `قسط ${toFaDigits(inst.position + 1)} — ${formatMoney(inst.amount)}`,
|
|
|
|
|
|
})),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const chequeStatusOptions = [
|
|
|
|
|
|
{ value: 'pending', label: 'در جریان' },
|
|
|
|
|
|
{ value: 'cleared', label: 'وصول شد' },
|
|
|
|
|
|
{ value: 'bounced', label: 'برگشت' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const docModeSegOptions = computed(() => {
|
|
|
|
|
|
const opts: Array<{ value: DocMode; label: string; disabled?: boolean }> = [
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'invoice',
|
|
|
|
|
|
label: 'فاکتور',
|
|
|
|
|
|
disabled: !isNew.value && docMode.value !== 'invoice',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'quote',
|
|
|
|
|
|
label: 'پیش فاکتور',
|
|
|
|
|
|
disabled: !isNew.value && docMode.value !== 'quote',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
if (docMode.value === 'credit') {
|
|
|
|
|
|
opts.push({ value: 'credit', label: 'برگشتی', disabled: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
return opts;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const sideCollapseKeys = ref<string[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
const paymentsCollapseKeys = computed({
|
|
|
|
|
|
get: () => (showPayments.value ? ['payments'] : []),
|
|
|
|
|
|
set: (keys: string[]) => {
|
|
|
|
|
|
showPayments.value = keys.includes('payments');
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function setLineQty(line: LineDraft, value: number | string | null) {
|
|
|
|
|
|
const n = Number(value);
|
|
|
|
|
|
line.quantity = String(Number.isFinite(n) && n > 0 ? n : 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setLineProduct(line: LineDraft, value: unknown) {
|
|
|
|
|
|
line.productId = value == null ? '' : String(value);
|
|
|
|
|
|
onPickProduct(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onDocModeSeg(value: string | number) {
|
|
|
|
|
|
switchDocMode(String(value) as DocMode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
onMounted(load);
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<div>
|
|
|
|
|
|
<a-flex
|
|
|
|
|
|
v-if="!embedded"
|
|
|
|
|
|
justify="space-between"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
wrap="wrap"
|
|
|
|
|
|
:gap="12"
|
|
|
|
|
|
class="no-print"
|
|
|
|
|
|
:style="{ marginBottom: '16px' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-space :size="12" align="start">
|
|
|
|
|
|
<FileTextOutlined />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<a-typography-title :level="3" :style="{ margin: 0 }">{{ pageTitle }}</a-typography-title>
|
|
|
|
|
|
<a-typography-text type="secondary">{{ pageSubtitle }}</a-typography-text>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
</div>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-space>
|
|
|
|
|
|
<a-button @click="goBack">
|
|
|
|
|
|
<template #icon><ArrowLeftOutlined /></template>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
بازگشت
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-flex>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
<a-form v-if="!loading" layout="vertical" @finish="save">
|
|
|
|
|
|
<a-alert
|
|
|
|
|
|
v-if="error"
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:message="error"
|
|
|
|
|
|
class="no-print"
|
|
|
|
|
|
:style="{ marginBottom: '16px' }"
|
|
|
|
|
|
/>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-row :gutter="[16, 16]">
|
|
|
|
|
|
<a-col :xs="24" :lg="embedded ? 24 : 17" :xl="embedded ? 24 : 18">
|
|
|
|
|
|
<a-card :bordered="false">
|
|
|
|
|
|
<a-segmented
|
|
|
|
|
|
class="no-print"
|
|
|
|
|
|
block
|
|
|
|
|
|
:value="docMode"
|
|
|
|
|
|
:options="docModeSegOptions"
|
|
|
|
|
|
:style="{ marginBottom: '16px' }"
|
|
|
|
|
|
@update:value="onDocModeSeg"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
/>
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-divider orientation="right">اطلاعات اصلی</a-divider>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="مشتری" required>
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="personId"
|
|
|
|
|
|
show-search
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
:options="personSelectOptions"
|
|
|
|
|
|
placeholder="انتخاب مشتری"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@change="onPersonChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<RouterLink :to="{ name: 'person-new' }">
|
|
|
|
|
|
<a-button type="link" style="padding-inline: 0">
|
|
|
|
|
|
<template #icon><UserAddOutlined /></template>
|
|
|
|
|
|
افزودن مشتری جدید
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</RouterLink>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="تاریخ" required>
|
|
|
|
|
|
<JalaliDateInput v-model="issuedAt" required />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="وضعیت">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="status"
|
|
|
|
|
|
:options="statusSelectOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="ارز">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="currency"
|
|
|
|
|
|
:options="currencyAntOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<template v-if="docMode === 'quote'">
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="اعتبار تا">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="validityDays"
|
|
|
|
|
|
:options="validityDayOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@change="onValidityDaysChange"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
/>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="validUntil" :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="تاریخ اعتبار">
|
|
|
|
|
|
<JalaliDateInput
|
|
|
|
|
|
v-model="validUntil"
|
|
|
|
|
|
@update:model-value="validityMode = 'date'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-col v-else :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="مهلت پرداخت">
|
|
|
|
|
|
<JalaliDateInput v-model="dueDate" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="یادداشت">
|
|
|
|
|
|
<a-textarea
|
|
|
|
|
|
v-model:value="notes"
|
|
|
|
|
|
:rows="3"
|
|
|
|
|
|
placeholder="یادداشت خود را وارد کنید..."
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
|
|
<a-collapse
|
|
|
|
|
|
v-model:activeKey="sideCollapseKeys"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
class="no-print"
|
|
|
|
|
|
:style="{ borderRadius: '8px', overflow: 'hidden', marginBottom: '16px' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-collapse-panel key="extra" header="اطلاعات تکمیلی">
|
|
|
|
|
|
<a-typography-text type="secondary">فیلدی برای تکمیل وجود ندارد.</a-typography-text>
|
|
|
|
|
|
</a-collapse-panel>
|
|
|
|
|
|
<a-collapse-panel key="shipping" header="آدرس و ارسال">
|
|
|
|
|
|
<a-typography-text type="secondary">فیلدی برای تکمیل وجود ندارد.</a-typography-text>
|
|
|
|
|
|
</a-collapse-panel>
|
|
|
|
|
|
</a-collapse>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="right">محصولات و خدمات</a-divider>
|
|
|
|
|
|
|
|
|
|
|
|
<a-flex
|
|
|
|
|
|
class="no-print"
|
|
|
|
|
|
justify="space-between"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
wrap="wrap"
|
|
|
|
|
|
:gap="8"
|
|
|
|
|
|
:style="{ marginBottom: '12px' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-space :size="8" wrap>
|
|
|
|
|
|
<RouterLink :to="{ name: 'product-new' }">
|
|
|
|
|
|
<a-button type="primary">
|
|
|
|
|
|
<template #icon><PlusOutlined /></template>
|
|
|
|
|
|
افزودن محصول
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</RouterLink>
|
|
|
|
|
|
<a-button @click="addLine">
|
|
|
|
|
|
<template #icon><PlusOutlined /></template>
|
|
|
|
|
|
افزودن ردیف
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="productSearch"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="جستجوی محصول..."
|
|
|
|
|
|
style="width: 220px; max-width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
|
|
|
|
|
|
<a-flex vertical :gap="12">
|
|
|
|
|
|
<a-card
|
|
|
|
|
|
v-for="(line, idx) in lines"
|
|
|
|
|
|
:key="line.key"
|
|
|
|
|
|
:bordered="true"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-flex justify="space-between" align="center" :style="{ marginBottom: '8px' }">
|
|
|
|
|
|
<a-typography-text type="secondary">
|
|
|
|
|
|
ردیف {{ toFaDigits(idx + 1) }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
danger
|
|
|
|
|
|
aria-label="حذف ردیف"
|
|
|
|
|
|
@click="removeLine(line.key)"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<template #icon><DeleteOutlined /></template>
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="محصول">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
:value="line.productId || undefined"
|
|
|
|
|
|
show-search
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
:options="productSelectOptions"
|
|
|
|
|
|
placeholder="انتخاب محصول…"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="(v) => setLineProduct(line, v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="نام ردیف" required>
|
|
|
|
|
|
<a-input v-model:value="line.name" placeholder="نام ردیف" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="توضیح">
|
|
|
|
|
|
<a-input v-model:value="line.description" placeholder="توضیح اختیاری" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item :label="`قیمت واحد (${currencyLabel})`">
|
|
|
|
|
|
<MoneyInput
|
|
|
|
|
|
v-model="line.unitPrice"
|
|
|
|
|
|
:decimals="0"
|
|
|
|
|
|
placeholder="قیمت"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="تعداد">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
:value="Number(line.quantity) || 1"
|
|
|
|
|
|
:min="0.01"
|
|
|
|
|
|
:step="1"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="(v) => setLineQty(line, v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item :label="`مبلغ کل (${currencyLabel})`">
|
|
|
|
|
|
<a-typography-text strong>{{ formatMoney(lineTotal(line)) }}</a-typography-text>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<a-typography-text type="secondary" dir="ltr">
|
|
|
|
|
|
{{ line.sku || '—' }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-flex>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-divider orientation="right">مبالغ</a-divider>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="تخفیف">
|
|
|
|
|
|
<MoneyInput v-model="discountAmount" :decimals="0" placeholder="۰" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="مالیات (%)">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
:value="Number(taxPercent) || 0"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
:step="0.01"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="(v) => (taxPercent = String(v ?? 0))"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="docMode === 'invoice'" :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="نوع پورسانت">
|
|
|
|
|
|
<a-segmented
|
|
|
|
|
|
v-model:value="commissionMode"
|
|
|
|
|
|
block
|
|
|
|
|
|
:options="[
|
|
|
|
|
|
{ value: 'percent', label: 'درصد' },
|
|
|
|
|
|
{ value: 'amount', label: 'مبلغ ثابت' },
|
|
|
|
|
|
]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="docMode === 'invoice' && commissionMode === 'percent'" :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="پورسانت کارشناس (%)">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
:value="commissionPercent === '' ? undefined : Number(commissionPercent)"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
:max="100"
|
|
|
|
|
|
:step="0.1"
|
|
|
|
|
|
:placeholder="String(defaultCommissionPercent || 0)"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="(v) => (commissionPercent = v == null ? '' : String(v))"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-else-if="docMode === 'invoice'" :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="مبلغ پورسانت">
|
|
|
|
|
|
<MoneyInput v-model="commissionFixedAmount" :decimals="0" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-collapse
|
|
|
|
|
|
v-if="showPaymentBlock"
|
|
|
|
|
|
v-model:activeKey="paymentsCollapseKeys"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
class="no-print"
|
|
|
|
|
|
:style="{ borderRadius: '8px', overflow: 'hidden', marginTop: '8px' }"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-collapse-panel key="payments" header="اقساط و پرداختها">
|
|
|
|
|
|
<a-space wrap :size="12" :style="{ marginBottom: '12px' }">
|
|
|
|
|
|
<a-typography-text>
|
|
|
|
|
|
جمع: {{ formatMoney(invoice?.payableAmount ?? invoice?.totalAmount ?? 0) }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
<a-typography-text>
|
|
|
|
|
|
وصول: {{ formatMoney(invoice?.paidAmount ?? 0) }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
<a-typography-text>
|
|
|
|
|
|
مانده: {{ formatMoney(invoice?.remainingAmount ?? 0) }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
</a-space>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
|
|
|
|
|
|
<a-typography-title :level="5" :style="{ margin: 0 }">اقساط</a-typography-title>
|
|
|
|
|
|
<a-space wrap :size="8">
|
|
|
|
|
|
<a-button @click="splitInstallments(2)">۲ قسط</a-button>
|
|
|
|
|
|
<a-button @click="splitInstallments(3)">۳ قسط</a-button>
|
|
|
|
|
|
<a-button @click="addInstallment">افزودن قسط</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
|
|
|
|
|
|
<a-row
|
|
|
|
|
|
v-for="item in installments"
|
|
|
|
|
|
:key="item.key"
|
|
|
|
|
|
:gutter="[16, 0]"
|
|
|
|
|
|
:style="{ marginTop: '8px' }"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-col :xs="24" :md="10">
|
|
|
|
|
|
<JalaliDateInput v-model="item.dueDate" />
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="10">
|
|
|
|
|
|
<MoneyInput
|
|
|
|
|
|
v-model="item.amount"
|
|
|
|
|
|
:decimals="0"
|
|
|
|
|
|
placeholder="مبلغ قسط"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="4">
|
|
|
|
|
|
<a-button danger block @click="removeInstallment(item.key)">
|
|
|
|
|
|
<template #icon><DeleteOutlined /></template>
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-typography-text v-if="!installments.length" type="secondary">
|
|
|
|
|
|
بدون برنامه اقساط.
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
|
|
|
|
|
|
<a-typography-title :level="5" :style="{ marginTop: '16px' }">
|
|
|
|
|
|
ثبت پرداخت
|
|
|
|
|
|
</a-typography-title>
|
|
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="مبلغ">
|
|
|
|
|
|
<MoneyInput v-model="payAmount" :decimals="0" placeholder="مبلغ" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="روش">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="payMethod"
|
|
|
|
|
|
:options="methodSelectOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="تاریخ پرداخت">
|
|
|
|
|
|
<JalaliDateInput v-model="payDate" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="قسط">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="payInstallmentId"
|
|
|
|
|
|
:options="payInstallmentOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="یادداشت پرداخت">
|
|
|
|
|
|
<a-input v-model:value="payNotes" placeholder="یادداشت پرداخت" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
2026-10-18 21:33:56 +03:30
|
|
|
|
<a-col v-if="financeAccounts.length" :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="حساب صندوق">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="payAccountId"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
:options="[
|
|
|
|
|
|
{ value: '', label: 'بدون اثر روی صندوق' },
|
|
|
|
|
|
...financeAccounts.map((account) => ({
|
|
|
|
|
|
value: account.id,
|
|
|
|
|
|
label: `واریز به ${account.name}`,
|
|
|
|
|
|
})),
|
|
|
|
|
|
]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<template v-if="payMethod === 'check'">
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="شماره چک">
|
|
|
|
|
|
<a-input v-model:value="chequeNumber" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="بانک">
|
|
|
|
|
|
<a-input v-model:value="chequeBank" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="سررسید چک">
|
|
|
|
|
|
<JalaliDateInput v-model="chequeDue" placeholder="سررسید چک" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="وضعیت چک">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="chequeStatus"
|
|
|
|
|
|
:options="chequeStatusOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-button type="primary" :loading="paying" @click="submitPayment">
|
|
|
|
|
|
ثبت پرداخت
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
|
|
<a-flex
|
|
|
|
|
|
v-for="p in invoice?.payments ?? []"
|
|
|
|
|
|
:key="p.id"
|
|
|
|
|
|
justify="space-between"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
:style="{ marginTop: '8px' }"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<div>
|
|
|
|
|
|
<a-typography-text strong>{{ formatMoney(p.amount) }}</a-typography-text>
|
|
|
|
|
|
<a-typography-text type="secondary">
|
|
|
|
|
|
· {{ methodLabel[p.method as PaymentMethod] || p.method }}
|
|
|
|
|
|
<template v-if="p.cheque">
|
|
|
|
|
|
· چک {{ p.cheque.number }} / {{ p.cheque.bank }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
· {{ p.paidAt.slice(0, 10) }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-button type="link" danger @click="removePayment(p.id)">حذف</a-button>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
<a-typography-text
|
|
|
|
|
|
v-if="!(invoice?.payments?.length)"
|
|
|
|
|
|
type="secondary"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
هنوز پرداختی ثبت نشده.
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
</a-collapse-panel>
|
|
|
|
|
|
</a-collapse>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-space
|
|
|
|
|
|
v-if="!embedded"
|
|
|
|
|
|
class="no-print"
|
|
|
|
|
|
wrap
|
|
|
|
|
|
:size="8"
|
|
|
|
|
|
:style="{ marginTop: '16px' }"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-button @click="printDoc">
|
|
|
|
|
|
<template #icon><EyeOutlined /></template>
|
|
|
|
|
|
پیشنمایش
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="!isNew"
|
|
|
|
|
|
danger
|
|
|
|
|
|
:disabled="saving"
|
|
|
|
|
|
@click="remove"
|
2026-09-05 17:53:50 +03:30
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<template #icon><DeleteOutlined /></template>
|
|
|
|
|
|
حذف
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button type="primary" html-type="submit" :loading="saving">
|
|
|
|
|
|
<template #icon><CheckOutlined /></template>
|
|
|
|
|
|
{{ saveLabel }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-space>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-space
|
|
|
|
|
|
v-if="embedded"
|
|
|
|
|
|
class="no-print"
|
|
|
|
|
|
wrap
|
|
|
|
|
|
:size="8"
|
|
|
|
|
|
:style="{ marginTop: '16px' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button type="primary" :loading="saving" @click="save">ذخیره</a-button>
|
|
|
|
|
|
<a-button :disabled="saving" @click="goBack">انصراف</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-col v-if="!embedded" :xs="24" :lg="7" :xl="6" class="no-print">
|
|
|
|
|
|
<a-affix :offset-top="24">
|
|
|
|
|
|
<a-card :bordered="false" title="خلاصه">
|
|
|
|
|
|
<a-descriptions :column="1" size="small" bordered>
|
|
|
|
|
|
<a-descriptions-item label="جمع کل">
|
|
|
|
|
|
{{ formatMoney(subtotal) }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="تخفیف">
|
|
|
|
|
|
{{ formatMoney(discountNum) }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="مالیات">
|
|
|
|
|
|
{{ formatMoney(taxAmount) }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="قابل پرداخت">
|
|
|
|
|
|
{{ formatMoney(payableAmount) }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item v-if="docMode === 'invoice'" label="برآورد پورسانت">
|
|
|
|
|
|
{{ formatMoney(commissionPreview) }}
|
|
|
|
|
|
<template v-if="invoice?.commission?.status === 'posted'">
|
|
|
|
|
|
· ثبتشده {{ formatMoney(invoice.commission.amount) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
</a-descriptions>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-affix>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-spin>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<InvoiceDocPreview
|
|
|
|
|
|
v-model:open="previewOpen"
|
|
|
|
|
|
:doc-mode="docMode"
|
|
|
|
|
|
:org-name="orgName"
|
|
|
|
|
|
:number="number"
|
|
|
|
|
|
:title="title"
|
|
|
|
|
|
:status-label="previewStatusLabel"
|
|
|
|
|
|
:issued-at="issuedAt"
|
|
|
|
|
|
:due-or-valid="docMode === 'quote' ? validUntil : dueDate"
|
|
|
|
|
|
:due-or-valid-label="docMode === 'quote' ? 'اعتبار تا' : 'مهلت پرداخت'"
|
|
|
|
|
|
:customer-name="previewCustomer.name"
|
|
|
|
|
|
:customer-sub="previewCustomer.sub"
|
|
|
|
|
|
:notes="notes"
|
|
|
|
|
|
:currency="currency"
|
|
|
|
|
|
:lines="previewLines"
|
|
|
|
|
|
:subtotal="previewSubtotal"
|
|
|
|
|
|
:discount="discountNum"
|
|
|
|
|
|
:tax-amount="previewTaxAmount"
|
|
|
|
|
|
:tax-percent="taxPercentNum"
|
|
|
|
|
|
:payable="previewPayable"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|