Merge branch 'feat/separate-price-calc' into develop

This commit is contained in:
Methapon2001 2025-01-30 16:54:42 +07:00
commit 84282ff5ce
18 changed files with 744 additions and 332 deletions

View file

@ -1014,6 +1014,10 @@ const currentNoAction = ref(false);
const prevProduct = ref<ProductCreate>({
expenseType: '',
vatIncluded: true,
agentPriceVatIncluded: true,
agentPriceCalcVat: true,
serviceChargeVatIncluded: true,
serviceChargeCalcVat: true,
productGroupId: '',
remark: '',
serviceCharge: 0,
@ -1050,6 +1054,10 @@ async function assignFormDataProduct(data: Product) {
status: data.status,
expenseType: data.expenseType,
vatIncluded: data.vatIncluded,
serviceChargeCalcVat: data.serviceChargeCalcVat,
serviceChargeVatIncluded: data.serviceChargeVatIncluded,
agentPriceCalcVat: data.agentPriceCalcVat,
agentPriceVatIncluded: data.agentPriceVatIncluded,
selectedImage: data.selectedImage,
document: data.document,
shared: data.shared,
@ -1086,6 +1094,10 @@ function clearFormProduct() {
expenseType: '',
calcVat: true,
vatIncluded: true,
agentPriceCalcVat: true,
agentPriceVatIncluded: true,
serviceChargeCalcVat: true,
serviceChargeVatIncluded: true,
shared: false,
};
imageProduct.value = undefined;
@ -3900,7 +3912,12 @@ watch(
v-model:service-charge="formProduct.serviceCharge"
v-model:vat-included="formProduct.vatIncluded"
v-model:calc-vat="formProduct.calcVat"
dense
v-model:agent-price-vat-included="formProduct.agentPriceVatIncluded"
v-model:agent-price-calc-vat="formProduct.agentPriceCalcVat"
v-model:service-charge-vat-included="
formProduct.serviceChargeVatIncluded
"
v-model:service-charge-calc-vat="formProduct.serviceChargeCalcVat"
/>
<FormDocument
v-if="productTab === 3"
@ -4109,7 +4126,12 @@ watch(
v-model:service-charge="formProduct.serviceCharge"
v-model:vat-included="formProduct.vatIncluded"
v-model:calc-vat="formProduct.calcVat"
dense
v-model:agent-price-vat-included="formProduct.agentPriceVatIncluded"
v-model:agent-price-calc-vat="formProduct.agentPriceCalcVat"
v-model:service-charge-vat-included="
formProduct.serviceChargeVatIncluded
"
v-model:service-charge-calc-vat="formProduct.serviceChargeCalcVat"
:priceDisplay="priceDisplay"
/>
<FormDocument

View file

@ -251,10 +251,13 @@ function getPrice(
const vat =
(finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
a.vatExcluded = c.product.calcVat
a.vat = calcVat ? precisionRound(a.vat + vat) : a.vat;
a.vatExcluded = calcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + price);
a.finalPrice = precisionRound(
@ -424,6 +427,7 @@ async function fetchQuotation() {
const id = currentQuotationId.value || quotationFormData.value.id || '';
await quotationForm.assignFormData(id, quotationFormState.value.mode);
tempPaySplitCount.value = quotationFormData.value.paySplitCount || 0;
tempPaySplit.value = JSON.parse(
JSON.stringify(quotationFormData.value.paySplit),
@ -606,6 +610,7 @@ async function convertDataToFormSubmit() {
status: quotationFormData.value.status,
discount: quotationFormData.value.discount,
remark: quotationFormData.value.remark || '',
agentPrice: agentPrice.value,
};
newWorkerList.value = [];
@ -1119,6 +1124,7 @@ function storeDataLocal() {
},
selectedWorker: selectedWorker.value,
createdBy: quotationFormState.value.createdBy('tha'),
agentPrice: agentPrice.value,
},
}),
);

View file

@ -260,7 +260,11 @@ function mapNode() {
const price = prop.agentPrice
? p.product.agentPrice
: p.product.price;
const pricePerUnit = p.product.vatIncluded
const pricePerUnit = (
prop.agentPrice
? p.product.agentPriceVatIncluded
: p.product.vatIncluded
)
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
: price;
productCount.value++;
@ -303,7 +307,11 @@ function mapNode() {
const price = prop.agentPrice
? p.product.agentPrice
: p.product.price;
const pricePerUnit = p.product.vatIncluded
const pricePerUnit = (
prop.agentPrice
? p.product.agentPriceVatIncluded
: p.product.vatIncluded
)
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
: price;
productCount.value++;
@ -334,7 +342,9 @@ function mapNode() {
};
} else {
const price = prop.agentPrice ? v.raw.agentPrice : v.raw.price;
const pricePerUnit = v.raw.vatIncluded
const pricePerUnit = (
prop.agentPrice ? v.raw.agentPriceVatIncluded : v.raw.vatIncluded
)
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
: price;
productCount.value++;

View file

@ -39,6 +39,7 @@ export const DEFAULT_DATA: QuotationPayload = {
_count: { worker: 0 },
status: 'CREATED',
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
agentPrice: false,
};
const DEFAULT_DATA_INVOICE: InvoicePayload = {

View file

@ -19,6 +19,7 @@ import {
QuotationPayload,
Details,
QuotationFull,
ProductRelation,
} from 'src/stores/quotations/types';
// NOTE: Import Components
@ -40,11 +41,12 @@ type Product = {
code: string;
detail: string;
amount: number;
priceUnit: number;
pricePerUnit: number;
discount: number;
vat: number;
value: number;
calcVat: boolean;
product: ProductRelation;
};
type SummaryPrice = {
@ -60,6 +62,8 @@ const branch = ref<Branch>();
const productList = ref<Product[]>([]);
const bankList = ref<BankBook[]>([]);
const agentPrice = ref(false);
const elements = ref<HTMLElement[]>([]);
const chunks = ref<Product[][]>([[]]);
const attachmentList = ref<
@ -194,6 +198,8 @@ onMounted(async () => {
customer.value = resCustomerBranch;
}
agentPrice.value = data.value.agentPrice || parsed?.meta?.agentPrice;
details.value = {
code: parsed?.meta?.source?.code ?? data.value?.code,
createdAt:
@ -247,11 +253,12 @@ onMounted(async () => {
code: v.product.code,
detail: v.product.name,
amount: v.amount || 0,
priceUnit: v.pricePerUnit || 0,
pricePerUnit: v.pricePerUnit || 0,
discount: v.discount || 0,
vat: v.vat || 0,
value: precisionRound(price + (v.product.calcVat ? vat : 0)),
calcVat: v.product.calcVat,
product: v.product,
};
},
) || [];
@ -273,10 +280,13 @@ onMounted(async () => {
const vat =
(finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
a.vatExcluded = c.product.calcVat
a.vat = calcVat ? precisionRound(a.vat + vat) : a.vat;
a.vatExcluded = calcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + price);
a.finalPrice = precisionRound(
@ -285,7 +295,6 @@ onMounted(async () => {
a.vat -
Number(data.value?.discount || 0),
);
return a;
},
{
@ -300,6 +309,20 @@ onMounted(async () => {
assignData();
});
function calcPrice(c: Product) {
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice + originalPrice * (config.value?.vat || 0.07),
);
const finalPriceNoVat = finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * c.amount - c.discount;
const vat = c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat']
? (finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07)
: 0;
return precisionRound(price + vat);
}
watch(elements, () => {});
function print() {
@ -359,7 +382,15 @@ function print() {
<td>{{ v.detail }}</td>
<td style="text-align: right">{{ v.amount }}</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.priceUnit, 2) }}
{{
formatNumberDecimal(
v.pricePerUnit +
(v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? v.pricePerUnit * (config?.vat || 0.07)
: 0),
2,
)
}}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.discount, 2) }}
@ -367,9 +398,9 @@ function print() {
<td style="text-align: right">
{{
formatNumberDecimal(
v.calcVat
v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? precisionRound(
(v.priceUnit * v.amount - v.discount) *
(v.pricePerUnit * v.amount - v.discount) *
(config?.vat || 0.07),
)
: 0,
@ -378,7 +409,7 @@ function print() {
}}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.value, 2) }}
{{ formatNumberDecimal(calcPrice(v), 2) }}
</td>
</tr>
</tbody>

View file

@ -199,7 +199,7 @@ onMounted(async () => {
})
.reduce(
(a, c) => {
const priceNoVat = c.product.vatIncluded
const priceNoVat = c.product.serviceChargeVatIncluded
? c.pricePerUnit / (1 + (config.value?.vat || 0.07))
: c.pricePerUnit;
const adjustedPriceWithVat = precisionRound(
@ -219,9 +219,10 @@ onMounted(async () => {
priceUnit: precisionRound(priceNoVat),
amount: c.amount,
discount: c.discount,
vat: c.product.calcVat ? precisionRound(rawVat) : 0,
vat: c.product.serviceChargeCalcVat ? precisionRound(rawVat) : 0,
value: precisionRound(
priceNoVat * c.amount + (c.product.calcVat ? rawVatTotal : 0),
priceNoVat * c.amount +
(c.product.serviceChargeCalcVat ? rawVatTotal : 0),
),
});

View file

@ -56,18 +56,25 @@ function openList(index: number) {
}
function calcPricePerUnit(product: RequestWork['productService']['product']) {
return product.vatIncluded
? (props.creditNote
const val = props.creditNote
? props.agentPrice
? product.agentPrice
: product.price
: product.serviceCharge;
if (
product[
props.creditNote
? props.agentPrice
? product.agentPrice
: product.price
: product.serviceCharge) /
(1 + (config.value?.vat || 0.07))
: props.creditNote
? props.agentPrice
? product.agentPrice
: product.price
: product.serviceCharge;
? 'agentPriceCalcVat'
: 'calcVat'
: 'serviceChargeCalcVat'
]
) {
return val / (1 + (config.value?.vat || 0.07));
} else {
return val;
}
}
function calcPrice(
@ -81,12 +88,24 @@ function calcPrice(
: product.serviceCharge;
const discount =
taskProduct.value.find((v) => v.productId === product.id)?.discount || 0;
const priceNoVat = product.vatIncluded
const priceNoVat = product[
props.creditNote
? props.agentPrice
? 'agentPriceVatIncluded'
: 'vatIncluded'
: 'serviceChargeVatIncluded'
]
? pricePerUnit / (1 + (config.value?.vat || 0.07))
: pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount - discount;
const rawVatTotal = product.calcVat
const rawVatTotal = product[
props.creditNote
? props.agentPrice
? 'agentPriceCalcVat'
: 'calcVat'
: 'serviceChargeCalcVat'
]
? priceDiscountNoVat * (config.value?.vat || 0.07)
: 0;

View file

@ -135,7 +135,7 @@ function getPrice(
const discount =
taskProduct.value.find((v) => v.productId === c.product.id)?.discount ||
0;
const priceNoVat = c.product.vatIncluded
const priceNoVat = c.product.serviceChargeVatIncluded
? pricePerUnit / (1 + (config.value?.vat || 0.07))
: pricePerUnit;
const adjustedPriceWithVat = precisionRound(
@ -149,8 +149,8 @@ function getPrice(
a.totalPrice = a.totalPrice + priceDiscountNoVat;
a.totalDiscount = a.totalDiscount + Number(discount);
a.vat = c.product.calcVat ? a.vat + rawVatTotal : a.vat;
a.vatExcluded = c.product.calcVat
a.vat = c.product.serviceChargeCalcVat ? a.vat + rawVatTotal : a.vat;
a.vatExcluded = c.product.serviceChargeCalcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + priceDiscountNoVat);
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;

View file

@ -19,7 +19,7 @@ import AdditionalFileExpansion from '../09_task-order/expansion/AdditionalFileEx
import PaymentExpansion from './expansion/PaymentExpansion.vue';
import CreditNoteExpansion from './expansion/CreditNoteExpansion.vue';
import StateButton from 'src/components/button/StateButton.vue';
import ProductExpansion from '../09_task-order/expansion/ProductExpansion.vue';
import ProductExpansion from './expansion/ProductExpansion.vue';
import SelectReadyRequestWork from '../09_task-order/SelectReadyRequestWork.vue';
import RefundInformation from './RefundInformation.vue';
import QuotationFormReceipt from '../05_quotation/QuotationFormReceipt.vue';
@ -111,7 +111,7 @@ const formTaskList = ref<
let taskListGroup = computed(() => {
const cacheData = formTaskList.value.reduce<
{
product: RequestWork['productService']['product'];
product: RequestWork['productService'];
list: RequestWork[];
}[]
>((acc, curr) => {
@ -129,7 +129,7 @@ let taskListGroup = computed(() => {
exist.list.push(task.requestWork);
} else {
acc.push({
product: task.requestWork.productService.product,
product: task.requestWork.productService,
list: [record],
});
}
@ -189,37 +189,28 @@ async function initStatus() {
function getPrice(
list: {
product: RequestWork['productService']['product'];
product: RequestWork['productService'];
list: RequestWork[];
}[],
) {
return list.reduce(
(a, c) => {
const pricePerUnit = quotationData.value?.agentPrice
? c.product.agentPrice
: c.product.price;
const pricePerUnit =
c.product.pricePerUnit - c.product.discount / c.product.amount;
const amount = c.list.length;
const discount = 0;
const priceNoVat = c.product.vatIncluded
? pricePerUnit / (1 + (config.value?.vat || 0.07))
: pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount - discount;
const priceNoVat = pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount;
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
// const rawVat = rawVatTotal / amount;
a.totalPrice = a.totalPrice + priceDiscountNoVat;
a.totalDiscount = a.totalDiscount + Number(discount);
a.vat = c.product.calcVat ? a.vat + rawVatTotal : a.vat;
a.vatExcluded = c.product.calcVat ? a.vatExcluded : a.vat + rawVatTotal;
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
a.vat = c.product.vat !== 0 ? a.vat + rawVatTotal : a.vat;
a.finalPrice = a.totalPrice + a.vat;
return a;
},
{
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatExcluded: 0,
finalPrice: 0,
},
);
@ -749,12 +740,12 @@ onMounted(async () => {
"
/>
<!-- TODO: bind remark -->
<RemarkExpansion
v-if="view !== CreditNoteStatus.Success"
:readonly="readonly"
v-model:remark="formData.remark"
:default-remark="defaultRemark"
:items="[]"
:readonly
>
<template #hint>
{{ $t('general.hintRemark') }}

View file

@ -76,3 +76,60 @@ export const hslaColors: Record<string, string> = {
Pending: '--orange-5-hsl',
Success: '--blue-6-hsl',
};
export const productColumn = [
{
name: 'order',
align: 'center',
label: 'general.order',
field: 'no',
},
{
name: 'code',
align: 'center',
label: 'productService.product.code',
field: 'code',
},
{
name: 'productList',
align: 'center',
label: 'taskOrder.productList',
field: 'productList',
},
{
name: 'amountOfEmployee',
align: 'center',
label: 'taskOrder.amountOfEmployee',
field: 'amountOfEmployee',
},
{
name: 'pricePerUnit',
align: 'center',
label: 'quotation.pricePerUnit',
field: 'pricePerUnit',
},
{
name: 'discount',
align: 'center',
label: 'general.discount',
field: 'discount',
},
{
name: 'priceBeforeVat',
align: 'center',
label: 'quotation.priceBeforeVat',
field: 'priceBeforeVat',
},
{
name: 'vat',
align: 'center',
label: 'general.vat',
field: 'vat',
},
{
name: 'totalPriceBaht',
align: 'center',
label: 'quotation.totalPriceBaht',
field: 'totalPriceBaht',
},
] as const satisfies QTableProps['columns'];

View file

@ -0,0 +1,232 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { QTableSlots } from 'quasar';
import { storeToRefs } from 'pinia';
import { AddButton } from 'src/components/button';
import TableEmployee from '../../09_task-order/TableEmployee.vue';
import { useConfigStore } from 'stores/config';
import { RequestWork } from 'src/stores/request-list';
import { baseUrl, formatNumberDecimal } from 'src/stores/utils';
import { precisionRound } from 'src/utils/arithmetic';
import { productColumn } from '../constants';
const configStore = useConfigStore();
const { data: config } = storeToRefs(configStore);
const currentExpanded = ref<boolean[]>([]);
defineProps<{
readonly?: boolean;
taskList: {
product: RequestWork['productService'];
list: RequestWork[];
}[];
}>();
defineEmits<{
(e: 'addProduct'): void;
}>();
defineExpose({ calcPricePerUnit });
function openList(index: number) {
if (!currentExpanded.value[index]) {
currentExpanded.value.map((_, i) => {
if (i !== index) currentExpanded.value[i] = false;
});
}
currentExpanded.value[index] = !currentExpanded.value[index];
}
function calcPricePerUnit(product: RequestWork['productService']) {
return product.pricePerUnit - product.discount / product.amount;
}
function calcPrice(c: RequestWork['productService'], amount: number) {
const pricePerUnit = c.pricePerUnit - c.discount / c.amount;
const priceNoVat = pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount;
const rawVatTotal =
c.vat === 0 ? 0 : priceDiscountNoVat * (config.value?.vat || 0.07);
return precisionRound(priceNoVat * amount + rawVatTotal);
}
</script>
<template>
<q-expansion-item
dense
class="overflow-hidden bordered full-width"
switch-toggle-side
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="surface-1 q-py-sm text-medium text-body1"
default-opened
>
<template #header>
<span
class="row items-center justify-between full-width"
style="min-height: 31.01px"
>
{{ $t('general.information', { msg: $t('taskOrder.productList') }) }}
<AddButton
icon-only
@click.stop="$emit('addProduct')"
v-if="!readonly"
/>
</span>
</template>
<main class="q-px-md q-py-sm surface-1">
<q-table
:columns="
productColumn.filter(
(v) =>
v.name !== 'discount' &&
v.name !== 'priceBeforeVat' &&
v.name !== 'vat',
)
"
:rows="taskList"
bordered
flat
hide-pagination
card-container-class="q-col-gutter-sm"
class="full-width"
:rows-per-page-options="[0]"
:no-data-label="$t('general.noDataTable')"
>
<template v-slot:header="props">
<q-tr
style="background-color: hsla(var(--info-bg) / 0.07)"
:props="props"
>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label && $t(col.label) }}
{{ col.label === 'quotation.vat' ? '%' : '' }}
</q-th>
<q-th></q-th>
</q-tr>
</template>
<template
v-slot:body="props: {
row: {
product: RequestWork['productService'];
list: RequestWork[];
};
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
>
<q-tr class="text-center">
<q-td>
{{ props.rowIndex + 1 }}
</q-td>
<q-td>
{{ props.row.product.product.code }}
</q-td>
<q-td style="width: 100%" class="text-left">
<q-avatar class="q-mr-sm" size="md">
<q-img
class="text-center"
:ratio="1"
:src="`${baseUrl}/product/${props.row.product.id}/image/${props.row.product.product.selectedImage}`"
>
<template #error>
<q-icon
class="full-width full-height"
name="mdi-shopping-outline"
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
/>
</template>
</q-img>
</q-avatar>
{{ props.row.product.product.name }}
</q-td>
<q-td>
{{ props.row.list.length }}
</q-td>
<q-td class="text-right">
{{
formatNumberDecimal(
calcPricePerUnit(props.row.product) +
(props.row.product.vat > 0
? calcPricePerUnit(props.row.product) *
(config?.vat || 0.07)
: 0),
2,
)
}}
</q-td>
<!-- total -->
<q-td class="text-right">
{{
formatNumberDecimal(
calcPrice(props.row.product, props.row.list.length),
2,
)
}}
</q-td>
<q-td>
<q-btn
dense
flat
class="rounded"
@click.stop="openList(props.rowIndex)"
>
<div class="row items-center no-wrap">
<q-icon name="mdi-account-group-outline" />
<q-icon
class="btn-arrow-right"
:class="{
active: currentExpanded[props.rowIndex],
}"
size="xs"
:name="`mdi-chevron-${currentExpanded[props.rowIndex] ? 'down' : 'up'}`"
/>
</div>
</q-btn>
</q-td>
</q-tr>
<q-tr v-show="currentExpanded[props.rowIndex]" :props="props">
<q-td colspan="100%" style="padding: 16px">
<TableEmployee :rows="props.row.list" />
</q-td>
</q-tr>
</template>
</q-table>
<div class="q-pt-md row items-center">
<span class="q-ml-auto q-mr-sm">
{{
$t('general.numberOf', {
msg: $t('productService.product.product'),
})
}}
</span>
<div class="surface-3 q-px-sm rounded">{{ taskList.length }}</div>
</div>
</main>
</q-expansion-item>
</template>
<style scoped>
.product-status {
padding-left: 8px;
border-radius: 20px;
color: hsl(var(--_color));
background: hsla(var(--_color) / 0.15);
&.warning {
--_color: var(--warning-bg);
}
&.positive {
--_color: var(--positive-bg);
}
&.negative {
--_color: var(--negative-bg);
}
}
</style>