fix/refactor: quotation installment (#121)
* refactor/feat: i18n * chore: clean log * refactor: type * refactor: installment and product table state relation * refactor: handle split custom --------- Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
This commit is contained in:
parent
57aabf1deb
commit
1b4c06b182
10 changed files with 357 additions and 77 deletions
|
|
@ -6,7 +6,7 @@ import { storeToRefs } from 'pinia';
|
|||
import WorkerItem from './WorkerItem.vue';
|
||||
import DeleteButton from '../button/DeleteButton.vue';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import { QuotationPayload } from 'stores/quotations/types';
|
||||
import { ProductServiceList, QuotationPayload } from 'stores/quotations/types';
|
||||
import { formatNumberDecimal, commaInput } from 'stores/utils';
|
||||
import { useConfigStore } from 'stores/config';
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ const props = defineProps<{
|
|||
readonly?: boolean;
|
||||
agentPrice: boolean;
|
||||
installmentInput?: boolean;
|
||||
maxInstallment?: number | null;
|
||||
employeeRows?: {
|
||||
foreignRefNo: string;
|
||||
employeeName: string;
|
||||
|
|
@ -29,6 +30,13 @@ const props = defineProps<{
|
|||
|
||||
defineEmits<{
|
||||
(e: 'delete', index: number): void;
|
||||
(
|
||||
e: 'updateTable',
|
||||
data: QuotationPayload['productServiceList'][number],
|
||||
opt?: {
|
||||
newInstallmentNo: number;
|
||||
},
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
|
@ -224,6 +232,26 @@ watch(
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.maxInstallment,
|
||||
() => {
|
||||
if (!props.maxInstallment) return;
|
||||
let test: ProductServiceList[] = [];
|
||||
const items = groupByServiceId(
|
||||
rows.value.map((v, i) => Object.assign(v, { i })),
|
||||
) || [{ title: '', product: [] }];
|
||||
|
||||
items.forEach((p) => {
|
||||
test = test.concat(p.product.flatMap((item) => item));
|
||||
});
|
||||
test.forEach((p) => {
|
||||
if ((props.maxInstallment || 0) < (p.installmentNo || 0)) {
|
||||
p.installmentNo = Number(props.maxInstallment);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="column">
|
||||
|
|
@ -278,15 +306,24 @@ watch(
|
|||
<q-td class="text-center">{{ props.rowIndex + 1 }}</q-td>
|
||||
<q-td v-if="installmentInput">
|
||||
<q-input
|
||||
v-model="props.row.installmentNo"
|
||||
:readonly
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
dense
|
||||
min="0"
|
||||
min="1"
|
||||
:max="maxInstallment"
|
||||
outlined
|
||||
input-class="text-right"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
:model-value="props.row.installmentNo"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
$emit('updateTable', props.row, {
|
||||
newInstallmentNo: Number(v),
|
||||
});
|
||||
props.row.installmentNo = Number(v);
|
||||
}
|
||||
"
|
||||
></q-input>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.product.code }}</q-td>
|
||||
|
|
@ -309,12 +346,18 @@ watch(
|
|||
:type="readonly ? 'text' : 'number'"
|
||||
input-class="text-center"
|
||||
style="width: 70px"
|
||||
min="0"
|
||||
min="1"
|
||||
debounce="500"
|
||||
v-model="props.row.amount"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
$emit('updateTable', props.row);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td align="right">
|
||||
<!-- TODO: -->
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
props.row.pricePerUnit +
|
||||
|
|
@ -349,6 +392,7 @@ watch(
|
|||
: '',
|
||||
);
|
||||
props.row.discount = x;
|
||||
$emit('updateTable', props.row);
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ export default {
|
|||
customer: 'Customer',
|
||||
individual: 'Individual',
|
||||
unavailable: 'Unavailable',
|
||||
selected: 'Selected {number} {msg}',
|
||||
},
|
||||
|
||||
menu: {
|
||||
|
|
@ -715,6 +716,7 @@ export default {
|
|||
vatIncluded: 'Include VAT',
|
||||
vatExcluded: 'Exclude VAT',
|
||||
vat: 'VAT',
|
||||
product: 'Product',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ export default {
|
|||
customer: 'ลูกค้า',
|
||||
individual: 'รายบุคคล',
|
||||
unavailable: 'ไม่พร้อมใช้งาน',
|
||||
selected: '{number} {msg}ถูกเลือก',
|
||||
},
|
||||
|
||||
menu: {
|
||||
|
|
@ -707,6 +708,7 @@ export default {
|
|||
vatIncluded: 'รวม VAT',
|
||||
vatExcluded: 'ไม่รวม VAT',
|
||||
vat: 'คำนวณ VAT',
|
||||
product: 'สินค้า',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ import { runOcr, parseResultMRZ } from 'src/utils/ocr';
|
|||
|
||||
// NOTE Import Types
|
||||
import { View } from './types.ts';
|
||||
import { QuotationPayload } from 'src/stores/quotations/types';
|
||||
import {
|
||||
PayCondition,
|
||||
PaySplit,
|
||||
ProductServiceList,
|
||||
QuotationPayload,
|
||||
} from 'src/stores/quotations/types';
|
||||
import { EmployeeWorker } from 'src/stores/quotations/types';
|
||||
import { Employee } from 'src/stores/employee/types';
|
||||
import { Receipt } from 'src/stores/payment/types';
|
||||
|
|
@ -136,6 +141,7 @@ const templateFormOption = ref<{ label: string; value: string }[]>([]);
|
|||
const refSelectZoneEmployee = ref<InstanceType<typeof SelectZone>>();
|
||||
const mrz = ref<Awaited<ReturnType<typeof parseResultMRZ>>>();
|
||||
const toggleWorker = ref(true);
|
||||
const tempTableProduct = ref<ProductServiceList[]>([]);
|
||||
const tempPaySplitCount = ref(0);
|
||||
const tempPaySplit = ref<
|
||||
{ no: number; amount: number; name?: string; invoice?: boolean }[]
|
||||
|
|
@ -165,6 +171,7 @@ const workerList = ref<Employee[]>([]);
|
|||
|
||||
const selectedProductGroup = ref('');
|
||||
const selectedInstallmentNo = ref<number[]>([]);
|
||||
const installmentAmount = ref<number>(0);
|
||||
const selectedInstallment = ref();
|
||||
const agentPrice = ref(false);
|
||||
|
||||
|
|
@ -492,7 +499,10 @@ async function convertInvoiceToSubmit() {
|
|||
if (quotationFormData.value.id) {
|
||||
invoiceFormData.value = {
|
||||
installmentNo: selectedInstallmentNo.value,
|
||||
amount: summaryPrice.value.finalPrice,
|
||||
amount:
|
||||
quotationFormData.value.payCondition === 'SplitCustom'
|
||||
? installmentAmount.value
|
||||
: summaryPrice.value.finalPrice,
|
||||
quotationId: quotationFormData.value.id,
|
||||
};
|
||||
|
||||
|
|
@ -668,9 +678,164 @@ function triggerProductServiceDialog() {
|
|||
pageState.productServiceModal = true;
|
||||
}
|
||||
|
||||
function handlePaySplitDiscount() {
|
||||
if (readonly.value) return;
|
||||
if (quotationFormData.value.payCondition === 'Split') {
|
||||
// Calculate average discount
|
||||
const paySplitArray = JSON.parse(JSON.stringify(tempPaySplit.value));
|
||||
const numSplits = paySplitArray.length;
|
||||
let totalDiscount = quotationFormData.value.discount || 0;
|
||||
const averageDiscount = totalDiscount / numSplits;
|
||||
|
||||
// Adjust each amount
|
||||
paySplitArray.forEach((split: PaySplit) => {
|
||||
if (split.amount < averageDiscount) {
|
||||
totalDiscount -= split.amount; // Subtract the full amount from totalDiscount
|
||||
split.amount = 0; // Set the amount to 0
|
||||
} else {
|
||||
split.amount -= averageDiscount; // Subtract the average discount
|
||||
totalDiscount -= averageDiscount; // Adjust remaining totalDiscount
|
||||
}
|
||||
});
|
||||
|
||||
// Distribute any remaining discount
|
||||
if (totalDiscount > 0) {
|
||||
paySplitArray.forEach((split: PaySplit) => {
|
||||
if (totalDiscount === 0) return;
|
||||
const deduction = Math.min(split.amount, totalDiscount);
|
||||
split.amount -= deduction;
|
||||
totalDiscount -= deduction;
|
||||
});
|
||||
}
|
||||
|
||||
// Update paySplit in quotationFormData
|
||||
quotationFormData.value.paySplit = paySplitArray;
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePayType(type: PayCondition) {
|
||||
if (
|
||||
type === 'Split' &&
|
||||
tempPaySplitCount.value &&
|
||||
tempPaySplit.value &&
|
||||
tempTableProduct.value
|
||||
) {
|
||||
quotationFormData.value.paySplitCount = tempPaySplitCount.value;
|
||||
quotationFormData.value.paySplit = JSON.parse(
|
||||
JSON.stringify(tempPaySplit.value),
|
||||
);
|
||||
productServiceList.value = productServiceList.value.map((item, index) => ({
|
||||
...item,
|
||||
installmentNo: tempTableProduct.value[index].installmentNo || 0,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function handleUpdateProductTable(
|
||||
data: QuotationPayload['productServiceList'][number],
|
||||
opt?: {
|
||||
newInstallmentNo: number;
|
||||
},
|
||||
) {
|
||||
// calc price
|
||||
const calc = (c: QuotationPayload['productServiceList'][number]) => {
|
||||
const pricePerUnit = c.pricePerUnit || 0;
|
||||
const discount = c.discount || 0;
|
||||
|
||||
return precisionRound(
|
||||
pricePerUnit * c.amount -
|
||||
discount +
|
||||
(c.product.calcVat
|
||||
? (pricePerUnit * c.amount - discount) * (config.value?.vat || 0.07)
|
||||
: 0),
|
||||
);
|
||||
};
|
||||
|
||||
// installment
|
||||
if (opt && opt.newInstallmentNo) {
|
||||
const oldInstallmentNo = JSON.parse(JSON.stringify(data.installmentNo));
|
||||
const oldPaySplit = quotationFormData.value.paySplit.find(
|
||||
(v) => v.no === oldInstallmentNo,
|
||||
);
|
||||
const newPaySplit = quotationFormData.value.paySplit.find(
|
||||
(v) => v.no === opt.newInstallmentNo,
|
||||
);
|
||||
|
||||
if (oldPaySplit) oldPaySplit.amount = oldPaySplit.amount - calc(data);
|
||||
if (newPaySplit) newPaySplit.amount = newPaySplit.amount + calc(data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// re calc price
|
||||
const currTempPaySplit = tempPaySplit.value.find(
|
||||
(v) => v.no === data.installmentNo,
|
||||
);
|
||||
const currPaySplit = quotationFormData.value.paySplit.find(
|
||||
(v) => v.no === data.installmentNo,
|
||||
);
|
||||
const targetPaySplit = productService.value.filter(
|
||||
(v) => v.installmentNo === data.installmentNo,
|
||||
);
|
||||
let targetPaySplitAmount: number[] = [];
|
||||
|
||||
targetPaySplitAmount = targetPaySplit.map((c) => {
|
||||
return calc(c);
|
||||
});
|
||||
|
||||
const totalSum = targetPaySplitAmount.reduce((sum, value) => sum + value, 0);
|
||||
if (currTempPaySplit && currPaySplit) {
|
||||
currTempPaySplit.amount = totalSum;
|
||||
currPaySplit.amount = totalSum;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDeleteProduct(index: number) {
|
||||
const currProduct = productServiceList.value[index];
|
||||
let currPaySplit = quotationFormData.value.paySplit.find(
|
||||
(v) => v.no === currProduct.installmentNo,
|
||||
);
|
||||
let currTempPaySplit = tempPaySplit.value.find(
|
||||
(v) => v.no === currProduct.installmentNo,
|
||||
);
|
||||
|
||||
// cal curr amount
|
||||
if (currPaySplit && currTempPaySplit) {
|
||||
const price = agentPrice.value
|
||||
? currProduct.product.agentPrice
|
||||
: currProduct.product.price;
|
||||
const pricePerUnit = currProduct.product.vatIncluded
|
||||
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
|
||||
: price;
|
||||
const vat = precisionRound(
|
||||
(pricePerUnit * currProduct.amount - currProduct.discount) *
|
||||
(config.value?.vat || 0.07),
|
||||
);
|
||||
const finalPrice = precisionRound(
|
||||
pricePerUnit * currProduct.amount +
|
||||
vat -
|
||||
Number(currProduct.discount || 0),
|
||||
);
|
||||
|
||||
currTempPaySplit.amount = currPaySplit.amount - finalPrice;
|
||||
currPaySplit.amount = currTempPaySplit.amount;
|
||||
}
|
||||
|
||||
// product display
|
||||
productServiceList.value.splice(index, 1);
|
||||
productServiceList.value = [...productServiceList.value]; // สร้างอาร์เรย์ใหม่
|
||||
productServiceList.value = [...productServiceList.value];
|
||||
|
||||
// find split count
|
||||
tempPaySplitCount.value = Math.max(
|
||||
...productServiceList.value.map((v) => v.installmentNo || 0),
|
||||
);
|
||||
quotationFormData.value.paySplitCount = tempPaySplitCount.value;
|
||||
|
||||
// make split.length === split count
|
||||
tempPaySplit.value.splice(quotationFormData.value.paySplitCount);
|
||||
quotationFormData.value.paySplit.splice(
|
||||
quotationFormData.value.paySplitCount,
|
||||
);
|
||||
}
|
||||
|
||||
async function assignWorkerToSelectedWorker() {
|
||||
|
|
@ -678,6 +843,7 @@ async function assignWorkerToSelectedWorker() {
|
|||
}
|
||||
|
||||
function convertToTable(nodes: Node[]) {
|
||||
// TODO:
|
||||
const _recursive = (v: Node): Node | Node[] => {
|
||||
if (v.checked && v.children) return v.children.flatMap(_recursive);
|
||||
if (v.checked) return v;
|
||||
|
|
@ -703,6 +869,7 @@ function convertToTable(nodes: Node[]) {
|
|||
});
|
||||
|
||||
productServiceList.value = list;
|
||||
tempTableProduct.value = JSON.parse(JSON.stringify(list));
|
||||
|
||||
quotationFormData.value.paySplit = Array.apply(
|
||||
null,
|
||||
|
|
@ -907,7 +1074,18 @@ const productServiceNodes = ref<ProductTree>([]);
|
|||
watch(
|
||||
() => productServiceList.value,
|
||||
() => {
|
||||
productServiceNodes.value = quotationProductTree(productServiceList.value);
|
||||
productServiceNodes.value = quotationProductTree(
|
||||
productServiceList.value,
|
||||
agentPrice.value,
|
||||
config.value?.vat,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => quotationFormData.value.discount,
|
||||
() => {
|
||||
handlePaySplitDiscount();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1041,7 +1219,6 @@ async function getWorkerFromCriteria(
|
|||
|
||||
if (!ret) return false; // error, do not close dialog
|
||||
|
||||
// TODO: Merge employee into worker list
|
||||
const deduplicate = ret.result.filter(
|
||||
(a) => !selectedWorker.value.find((b) => a.id === b.id),
|
||||
);
|
||||
|
|
@ -1358,6 +1535,7 @@ async function getWorkerFromCriteria(
|
|||
:installment-input="
|
||||
quotationFormData.payCondition === 'SplitCustom'
|
||||
"
|
||||
:max-installment="quotationFormData.paySplitCount"
|
||||
:readonly="readonly"
|
||||
:agent-price="agentPrice"
|
||||
:employee-rows="
|
||||
|
|
@ -1386,8 +1564,11 @@ async function getWorkerFromCriteria(
|
|||
@delete="toggleDeleteProduct"
|
||||
:rows="productService"
|
||||
@update:rows="
|
||||
(v) => view === View.Quotation && (productServiceList = v)
|
||||
(v) => {
|
||||
view === View.Quotation && (productServiceList = v);
|
||||
}
|
||||
"
|
||||
@update-table="handleUpdateProductTable"
|
||||
/>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
|
@ -1420,9 +1601,8 @@ async function getWorkerFromCriteria(
|
|||
<template v-if="true">
|
||||
<QuotationFormInfo
|
||||
:view="view"
|
||||
:installment-amount="installmentAmount"
|
||||
:installment-no="selectedInstallmentNo"
|
||||
:pay-split-count-fixed="tempPaySplitCount"
|
||||
:pay-split-fixed="tempPaySplit"
|
||||
v-model:pay-type="quotationFormData.payCondition"
|
||||
v-model:pay-bank="payBank"
|
||||
v-model:pay-split-count="quotationFormData.paySplitCount"
|
||||
|
|
@ -1432,6 +1612,7 @@ async function getWorkerFromCriteria(
|
|||
v-model:pay-bill-date="quotationFormData.payBillDate"
|
||||
v-model:summary-price="summaryPrice"
|
||||
class="q-mb-md"
|
||||
@change-pay-type="handleChangePayType"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -1642,9 +1823,24 @@ async function getWorkerFromCriteria(
|
|||
selectedInstallmentNo = selectedInstallment.map(
|
||||
(value: any) => value.no,
|
||||
);
|
||||
|
||||
installmentAmount = selectedInstallment.reduce(
|
||||
(total: number, value: any) => total + value.amount,
|
||||
0,
|
||||
);
|
||||
}
|
||||
"
|
||||
row-key="no"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-pagination
|
||||
:selected-rows-label="
|
||||
(n) =>
|
||||
$t('general.selected', {
|
||||
number: n,
|
||||
msg: $t('productService.product.product'),
|
||||
})
|
||||
"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
|
|
@ -1670,7 +1866,6 @@ async function getWorkerFromCriteria(
|
|||
@click="
|
||||
async () => {
|
||||
if (props.row.invoiceId) {
|
||||
// TODO: get invoice code
|
||||
await getInvoiceCode(props.row.invoiceId);
|
||||
|
||||
selectedInstallmentNo =
|
||||
|
|
@ -1680,6 +1875,8 @@ async function getWorkerFromCriteria(
|
|||
v.invoiceId === props.row.invoiceId,
|
||||
)
|
||||
.map((v) => v.no) || [];
|
||||
|
||||
installmentAmount = props.row.amount;
|
||||
view = View.Invoice;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,18 +14,18 @@ import SelectInput from 'src/components/shared/SelectInput.vue';
|
|||
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import { PayCondition } from 'src/stores/quotations/types.ts';
|
||||
|
||||
defineEmits<{
|
||||
(e: 'changePayType', type: PayCondition): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
readonly?: boolean;
|
||||
quotationNo?: string;
|
||||
installmentNo?: number[];
|
||||
paySplitCountFixed?: number;
|
||||
paySplitFixed?: {
|
||||
no: number;
|
||||
amount: number;
|
||||
name?: string;
|
||||
invoice?: boolean;
|
||||
}[];
|
||||
installmentAmount?: number;
|
||||
|
||||
view?: View;
|
||||
data?: {
|
||||
total: number;
|
||||
|
|
@ -42,14 +42,7 @@ const { data: config } = storeToRefs(configStore);
|
|||
const payBillDate = defineModel<Date | null | undefined>('payBillDate', {
|
||||
required: false,
|
||||
});
|
||||
const payType = defineModel<
|
||||
| 'Full'
|
||||
| 'Split'
|
||||
| 'SplitCustom'
|
||||
| 'BillFull'
|
||||
| 'BillSplit'
|
||||
| 'BillSplitCustom'
|
||||
>('payType', { required: true });
|
||||
const payType = defineModel<PayCondition>('payType', { required: true });
|
||||
const paySplitCount = defineModel<number | null>('paySplitCount', {
|
||||
default: 1,
|
||||
});
|
||||
|
|
@ -92,6 +85,17 @@ const payTypeOption = computed(() => [
|
|||
]);
|
||||
const amount4Show = ref<string[]>([]);
|
||||
|
||||
function handleSplitCount(count: number) {
|
||||
if (count > paySplit.value.length) {
|
||||
paySplit.value.push({ no: Number(count), amount: 0 });
|
||||
} else {
|
||||
paySplit.value[paySplit.value.length - 2].amount =
|
||||
paySplit.value[paySplit.value.length - 1].amount +
|
||||
paySplit.value[paySplit.value.length - 2].amount;
|
||||
paySplit.value.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function calculateInstallments(param: {
|
||||
customIndex?: number;
|
||||
customAmount?: number;
|
||||
|
|
@ -159,31 +163,21 @@ function calculateInstallments(param: {
|
|||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => payType.value,
|
||||
(v) => {
|
||||
if (v === 'Split' && props.paySplitCountFixed && props.paySplitFixed) {
|
||||
paySplitCount.value = props.paySplitCountFixed;
|
||||
paySplit.value = JSON.parse(JSON.stringify(props.paySplitFixed));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [paySplitCount.value, summaryPrice.value.finalPrice],
|
||||
([newCount, _newF], [oldCount, _oldF]) => {
|
||||
if (
|
||||
paySplitCount.value === 0 ||
|
||||
!paySplitCount.value ||
|
||||
!summaryPrice.value.finalPrice ||
|
||||
props.readonly
|
||||
) {
|
||||
return;
|
||||
}
|
||||
calculateInstallments({ newCount: newCount || 0, oldCount: oldCount || 0 });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
// watch(
|
||||
// () => [paySplitCount.value, summaryPrice.value.finalPrice],
|
||||
// ([newCount, _newF], [oldCount, _oldF]) => {
|
||||
// if (
|
||||
// paySplitCount.value === 0 ||
|
||||
// !paySplitCount.value ||
|
||||
// !summaryPrice.value.finalPrice ||
|
||||
// props.readonly
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
// calculateInstallments({ newCount: newCount || 0, oldCount: oldCount || 0 });
|
||||
// },
|
||||
// { deep: true },
|
||||
// );
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -214,7 +208,13 @@ watch(
|
|||
:option="payTypeOption"
|
||||
:readonly
|
||||
id="pay-type"
|
||||
v-model="payType"
|
||||
:model-value="payType"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
payType = v as PayCondition;
|
||||
$emit('changePayType', v as PayCondition);
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
@ -246,7 +246,12 @@ watch(
|
|||
dense
|
||||
outlined
|
||||
min="1"
|
||||
@update:model-value="(i) => (paySplitCount = Number(i))"
|
||||
@update:model-value="
|
||||
(i) => {
|
||||
paySplitCount = Number(i);
|
||||
handleSplitCount(Number(i));
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -347,7 +352,10 @@ watch(
|
|||
{{ $t('quotation.summary') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="q-pa-sm price-container">
|
||||
<div
|
||||
class="q-pa-sm price-container"
|
||||
v-if="payType !== 'SplitCustom' || view !== View.Invoice"
|
||||
>
|
||||
<div class="row">
|
||||
{{ $t('general.total') }}
|
||||
<span class="q-ml-auto">
|
||||
|
|
@ -415,6 +423,7 @@ watch(
|
|||
input-class="text-right"
|
||||
debounce="500"
|
||||
:model-value="commaInput(finalDiscount.toString() || '0')"
|
||||
@focus="(e) => (e.target as HTMLInputElement).select()"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') finalDiscount4Show = commaInput(v);
|
||||
|
|
@ -435,7 +444,10 @@ watch(
|
|||
|
||||
<span class="q-ml-auto" style="color: var(--brand-1)">
|
||||
{{
|
||||
formatNumberDecimal(Math.max(summaryPrice.finalPrice, 0), 2) || 0
|
||||
payType === 'SplitCustom' && view === View.Invoice
|
||||
? formatNumberDecimal(Math.max(installmentAmount || 0, 0), 2) || 0
|
||||
: formatNumberDecimal(Math.max(summaryPrice.finalPrice, 0), 2) ||
|
||||
0
|
||||
}}
|
||||
฿
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ function mapCard() {
|
|||
}
|
||||
|
||||
const productCount = ref(0);
|
||||
|
||||
function mapNode() {
|
||||
refSelectZone.value?.assignSelect(
|
||||
selectedItems.value,
|
||||
|
|
@ -361,7 +362,7 @@ function mapNode() {
|
|||
}
|
||||
});
|
||||
|
||||
nodes.value = node;
|
||||
nodes.value = JSON.parse(JSON.stringify(node));
|
||||
pageState.addModal = false;
|
||||
}
|
||||
watch(
|
||||
|
|
@ -404,7 +405,11 @@ watch(
|
|||
:title="$t('general.list', { msg: $t('productService.title') })"
|
||||
:submit-label="$t('general.select', { msg: $t('productService.title') })"
|
||||
submit-icon="mdi-check"
|
||||
:submit="() => $emit('submit', nodes)"
|
||||
:submit="
|
||||
() => {
|
||||
$emit('submit', nodes);
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-splitter
|
||||
v-model="splitterModel"
|
||||
|
|
|
|||
|
|
@ -145,8 +145,6 @@ export const useQuotationForm = defineStore('form-quotation', () => {
|
|||
|
||||
currentFormData.value = structuredClone(resetFormData);
|
||||
|
||||
console.log(currentFormData.value);
|
||||
|
||||
currentFormState.value.createdBy = (locale) =>
|
||||
locale === 'eng'
|
||||
? data.createdBy.firstNameEN + ' ' + data.createdBy.lastNameEN
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { QuotationFull } from 'src/stores/quotations/types';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
|
||||
export type ProductTree = {
|
||||
type?: string;
|
||||
|
|
@ -40,6 +41,8 @@ export function quotationProductTree(
|
|||
work?: QuotationFull['productServiceList'][number]['work'];
|
||||
product: QuotationFull['productServiceList'][number]['product'];
|
||||
}[],
|
||||
agentPrice?: boolean,
|
||||
vat?: number,
|
||||
): ProductTree {
|
||||
const ret: ProductTree = [];
|
||||
|
||||
|
|
@ -64,6 +67,13 @@ export function quotationProductTree(
|
|||
const mapper = (
|
||||
relation: (typeof work)['productOnWork'][number],
|
||||
): ProductTree[number] => {
|
||||
const price = agentPrice
|
||||
? relation.product.agentPrice
|
||||
: relation.product.price;
|
||||
const pricePerUnit = relation.product.vatIncluded
|
||||
? precisionRound(price / (1 + (vat || 0.07)))
|
||||
: price;
|
||||
|
||||
return {
|
||||
type: 'product',
|
||||
id: relation.product.id,
|
||||
|
|
@ -80,7 +90,7 @@ export function quotationProductTree(
|
|||
),
|
||||
value: {
|
||||
vat: current.vat,
|
||||
pricePerUnit: current.pricePerUnit,
|
||||
pricePerUnit: pricePerUnit,
|
||||
discount: current.discount,
|
||||
amount: current.amount,
|
||||
serviceId: service.id,
|
||||
|
|
|
|||
|
|
@ -342,17 +342,7 @@ export type QuotationFull = {
|
|||
};
|
||||
|
||||
export type QuotationPayload = {
|
||||
productServiceList: {
|
||||
workerIndex: number[];
|
||||
vat?: number;
|
||||
pricePerUnit?: number;
|
||||
discount?: number;
|
||||
amount: number;
|
||||
product: ProductRelation;
|
||||
installmentNo?: number;
|
||||
work?: WorkRelation | null;
|
||||
service?: ServiceRelation | null;
|
||||
}[];
|
||||
productServiceList: ProductServiceList[];
|
||||
customerBranchId: string;
|
||||
registeredBranchId: string;
|
||||
urgent: boolean;
|
||||
|
|
@ -437,3 +427,23 @@ export type PaymentPayload = {
|
|||
date: Date;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export type ProductServiceList = {
|
||||
workerIndex: number[];
|
||||
vat?: number;
|
||||
pricePerUnit?: number;
|
||||
discount?: number;
|
||||
amount: number;
|
||||
product: ProductRelation;
|
||||
installmentNo?: number;
|
||||
work?: WorkRelation | null;
|
||||
service?: ServiceRelation | null;
|
||||
};
|
||||
|
||||
export type PaySplit = {
|
||||
no: number;
|
||||
amount: number;
|
||||
name?: string;
|
||||
invoice?: boolean;
|
||||
invoiceId?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ const templates = {
|
|||
}) => {
|
||||
if (context?.paymentType === 'Full') {
|
||||
return [
|
||||
`**** เงื่อนไขเพิ่มเติม`,
|
||||
`- เงื่อนไขการชำระเงิน แบบเต็มจำนวน`,
|
||||
'**** เงื่อนไขเพิ่มเติม',
|
||||
'- เงื่อนไขการชำระเงิน แบบเต็มจำนวน',
|
||||
` จำนวน ${formatNumberDecimal(context?.amount || 0, 2)}`,
|
||||
].join('<br/>');
|
||||
} else {
|
||||
return [
|
||||
`**** เงื่อนไขเพิ่มเติม`,
|
||||
'**** เงื่อนไขเพิ่มเติม',
|
||||
`- เงื่อนไขการชำระเงิน แบบแบ่งจ่าย${context?.paymentType === 'SplitCustom' ? ' กำหนดเอง ' : ' '}${context?.installments?.length} งวด`,
|
||||
...(context?.installments?.map(
|
||||
(v) =>
|
||||
|
|
@ -63,7 +63,7 @@ export function convertTemplate(
|
|||
? template.converter(context?.[name as TemplateName] as any)
|
||||
: template.converter,
|
||||
);
|
||||
console.log(ret);
|
||||
// console.log(ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue