Add multiple select customer product price recalculation (#261)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
This commit is contained in:
co-authored by
Jeppe Bundgaard
parent
fd8b896c56
commit
1548ae8cd5
@@ -2,7 +2,7 @@
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { editOrderItem } from "@/components/shop/OrdersItems.vue";
|
||||
import { buildAuditedOrderItemReasonPayload, editOrderItem } from "@/components/shop/OrdersItems.vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -85,7 +85,12 @@ const saveChanges = async () => {
|
||||
Number(form.price),
|
||||
form.notes,
|
||||
form.reference,
|
||||
Number(form.quantity)
|
||||
Number(form.quantity),
|
||||
buildAuditedOrderItemReasonPayload(props.orderItem.product_id ?? props.orderItem.product?.id, form.notes, {
|
||||
reason_code: props.orderItem.reason_code,
|
||||
reason_label_snapshot: props.orderItem.reason_label_snapshot,
|
||||
reason_comment: form.notes,
|
||||
})
|
||||
);
|
||||
emits('saved');
|
||||
} catch (error) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
loadCustomerAttributes
|
||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import {getProductCategory, getProducts} from "@/components/shop/Products.vue";
|
||||
import {createOrderItem} from "@/components/shop/OrdersItems.vue";
|
||||
import { AUDITED_ORDER_ITEM_PRODUCT_IDS, createOrderItem } from "@/components/shop/OrdersItems.vue";
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from "vue-i18n";
|
||||
@@ -435,6 +435,18 @@ const showAddMultipleProducts = (productId) => {
|
||||
});
|
||||
};
|
||||
|
||||
const EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME = "Ekstraordinær pr. 10 min inkl. kemi";
|
||||
|
||||
const productRequiresOrderItemNote = (product) => {
|
||||
if (!product) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(product.requires_note)
|
||||
|| AUDITED_ORDER_ITEM_PRODUCT_IDS.has(Number(product.id))
|
||||
|| String(product.name || "").trim() === EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME;
|
||||
};
|
||||
|
||||
// Define the categories
|
||||
const categories_static = ref([
|
||||
{
|
||||
@@ -636,7 +648,7 @@ const addProductWithAddonsToOrder = async (product_id) => {
|
||||
return;
|
||||
}
|
||||
// Check if the product requires a note
|
||||
if (product.requires_note) {
|
||||
if (productRequiresOrderItemNote(product)) {
|
||||
// Show the note input
|
||||
await Swal.fire({
|
||||
title: 'Tilføj en note',
|
||||
|
||||
@@ -864,6 +864,7 @@ const assignDraftOrderCustomer = async ({
|
||||
*/
|
||||
showChangeCustomerForm: showChangeOrderCustomerForm,
|
||||
assignDraftCustomer: assignDraftOrderCustomer,
|
||||
recalculateOrderItemPricesForCustomer,
|
||||
/**
|
||||
* Show the change invoice collection form
|
||||
* @param id (order id)
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
import axios from 'axios'
|
||||
import {API_URL} from "@/config.js";
|
||||
|
||||
export const AUDITED_ORDER_ITEM_PRODUCT_IDS = new Set([21, 22, 24, 25, 26, 27]);
|
||||
export const DEFAULT_AUDITED_ORDER_ITEM_REASON_CODE = "customer_approved_extra_work";
|
||||
export const DEFAULT_AUDITED_ORDER_ITEM_REASON_LABEL = "Kunde godkendte ekstra arbejde";
|
||||
|
||||
export const buildAuditedOrderItemReasonPayload = (product_id, notes = null, reasonData = null) => {
|
||||
if (!AUDITED_ORDER_ITEM_PRODUCT_IDS.has(Number(product_id))) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const reason = reasonData && typeof reasonData === "object" ? reasonData : {};
|
||||
const comment = String(reason.reason_comment ?? reason.comment ?? notes ?? "").trim();
|
||||
|
||||
return {
|
||||
reason_code: String(reason.reason_code || DEFAULT_AUDITED_ORDER_ITEM_REASON_CODE),
|
||||
reason_label_snapshot: String(reason.reason_label_snapshot || DEFAULT_AUDITED_ORDER_ITEM_REASON_LABEL),
|
||||
reason_comment: comment,
|
||||
};
|
||||
};
|
||||
|
||||
export const getOrderItems = (order_id) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
@@ -15,7 +34,7 @@ export const getOrderItems = (order_id) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const createOrderItem = (order_id, product_id, quantity, related_item_id = null, notes = null, forcePrice = null) => {
|
||||
export const createOrderItem = (order_id, product_id, quantity, related_item_id = null, notes = null, forcePrice = null, reasonData = null) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
return null;
|
||||
@@ -29,7 +48,8 @@ export const createOrderItem = (order_id, product_id, quantity, related_item_id
|
||||
product_id,
|
||||
quantity,
|
||||
related_item_id,
|
||||
notes
|
||||
notes,
|
||||
...buildAuditedOrderItemReasonPayload(product_id, notes, reasonData),
|
||||
};
|
||||
if (forcePrice !== null && forcePrice !== undefined) {
|
||||
payload.price = forcePrice;
|
||||
@@ -53,7 +73,7 @@ export const removeOrderItem = (id) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const editOrderItem = (id, price, notes, reference, quantity) => {
|
||||
export const editOrderItem = (id, price, notes, reference, quantity, reasonData = null) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
return null;
|
||||
@@ -63,7 +83,8 @@ export const editOrderItem = (id, price, notes, reference, quantity) => {
|
||||
price,
|
||||
notes,
|
||||
reference,
|
||||
quantity
|
||||
quantity,
|
||||
...(reasonData && typeof reasonData === "object" ? reasonData : {}),
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
|
||||
+52
@@ -481,6 +481,17 @@ const orderIdsFromNodes = (nodes: TreeNode[]) =>
|
||||
orderNodesFromNodes(nodes)
|
||||
.map((node) => node.meta.orderId)
|
||||
.filter(Boolean);
|
||||
const orderRepricingTargetsFromNodes = (nodes: TreeNode[]) =>
|
||||
orderNodesFromNodes(nodes)
|
||||
.map((node) => {
|
||||
const order = node.meta?.order || {};
|
||||
return {
|
||||
orderId: toPositiveInteger(node.meta?.orderId),
|
||||
departmentId: toPositiveInteger(order.department_id ?? node.meta?.departmentId),
|
||||
customerNumber: toPositiveInteger(order.customer_number ?? props.customer?.customer_number),
|
||||
};
|
||||
})
|
||||
.filter((target) => target.orderId && target.departmentId && target.customerNumber);
|
||||
const orderItemIdsFromNodes = (nodes: TreeNode[]) =>
|
||||
uniqueValues(nodesOfType(nodes, TREE_NODE_TYPES.ORDER_ITEM).map((node) => node.meta.itemId));
|
||||
const attachmentNodesFromNodes = (nodes: TreeNode[]) =>
|
||||
@@ -517,6 +528,7 @@ const selectedOrdersWithBookingCount = computed(
|
||||
() => orderNodes().filter((node: TreeNode) => node.meta.bookingId).length
|
||||
);
|
||||
const selectedOrdersWithWashCount = computed(() => orderNodes().filter((node: TreeNode) => node.meta.washId).length);
|
||||
const selectedOrderRepricingCount = computed(() => orderRepricingTargetsFromNodes(selectedNodes.value).length);
|
||||
const certificateAttachmentOrderIds = computed(() => certificateAttachmentOrderIdsFromNodes(selectedNodes.value));
|
||||
const actionableXlvaskNodes = computed(() => actionableXlvaskNodesFromNodes(selectedNodes.value));
|
||||
|
||||
@@ -2481,6 +2493,31 @@ const moveOrdersToInvoiceCollection = async (nodes: TreeNode[] = orderNodes(), o
|
||||
|
||||
const moveSelectedOrdersToInvoiceCollection = () => moveOrdersToInvoiceCollection(orderNodes());
|
||||
|
||||
const recalculateOrderPrices = (nodes: TreeNode[] = orderNodes(), options: TreeActionRunOptions = {}) => {
|
||||
const targets = orderRepricingTargetsFromNodes(nodes);
|
||||
return runActionWithPreview(
|
||||
treeText("actions.orders.recalculate_prices.title", "Genberegn ordrepriser"),
|
||||
[
|
||||
treeText("actions.orders.recalculate_prices.preview", "{count} orders får genberegnet priser.", {
|
||||
count: targets.length,
|
||||
}),
|
||||
],
|
||||
async () => {
|
||||
for (const target of targets) {
|
||||
await SessionUser.objects.orders.functions.recalculateOrderItemPricesForCustomer({
|
||||
order_id: target.orderId,
|
||||
department_id: target.departmentId,
|
||||
customer_id: target.customerNumber,
|
||||
});
|
||||
}
|
||||
},
|
||||
true,
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
const recalculateSelectedOrderPrices = () => recalculateOrderPrices(orderNodes());
|
||||
|
||||
const unlinkOrderBookings = (nodes: TreeNode[] = orderNodes(), options: TreeActionRunOptions = {}) => {
|
||||
const orders = orderNodesFromNodes(nodes).filter((node) => node.meta.bookingId);
|
||||
return runActionWithPreview(
|
||||
@@ -2769,6 +2806,14 @@ const orderActions = (): TreeAction[] => [
|
||||
affectedCount: orderIds().length,
|
||||
run: moveSelectedOrdersToInvoiceCollection,
|
||||
},
|
||||
{
|
||||
key: "order:recalculate-prices",
|
||||
label: treeText("buttons.recalculate_prices", "Genberegn priser"),
|
||||
icon: "fa-calculator",
|
||||
affectedCount: selectedOrderRepricingCount.value,
|
||||
disabled: selectedOrderRepricingCount.value === 0,
|
||||
run: recalculateSelectedOrderPrices,
|
||||
},
|
||||
{
|
||||
key: "order:unlink-booking",
|
||||
label: treeText("buttons.unlink_booking", "Fjern booking"),
|
||||
@@ -3059,6 +3104,13 @@ const orderNodeActions = (node: TreeNode): TreeAction[] => {
|
||||
affectedCount: 1,
|
||||
run: () => setOrdersInvoiceIncluded(false, nodes, rowActionOptions()),
|
||||
}),
|
||||
rowAction(node, "order:recalculate-prices", {
|
||||
label: treeText("buttons.recalculate_prices", "Genberegn priser"),
|
||||
icon: "fa-calculator",
|
||||
affectedCount: orderRepricingTargetsFromNodes(nodes).length,
|
||||
disabled: orderRepricingTargetsFromNodes(nodes).length === 0,
|
||||
run: () => recalculateOrderPrices(nodes, rowActionOptions()),
|
||||
}),
|
||||
hasBooking
|
||||
? rowAction(node, "order:unlink-booking", {
|
||||
label: treeText("buttons.unlink_booking", "Fjern booking"),
|
||||
|
||||
@@ -4041,6 +4041,8 @@ test.describe("POS mobile order flow", () => {
|
||||
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
|
||||
const product27Create = fixture.requestLog.orderItemCreates.find((entry) => Number(entry.product_id) === 27);
|
||||
expect(product27Create?.notes).toBe("Extra chemical treatment on left side");
|
||||
expect(product27Create?.reason_code).toBe("customer_approved_extra_work");
|
||||
expect(product27Create?.reason_comment).toBe("Extra chemical treatment on left side");
|
||||
await waitForStepReset(page);
|
||||
});
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ function buildOrderItem(product, overrides = {}, id = null) {
|
||||
reference: String(overrides.reference ?? ""),
|
||||
related_item_id: overrides.related_item_id ?? null,
|
||||
price: Number(overrides.price ?? product.price ?? 0),
|
||||
reason_code: String(overrides.reason_code ?? ""),
|
||||
reason_label_snapshot: String(overrides.reason_label_snapshot ?? ""),
|
||||
reason_comment: String(overrides.reason_comment ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2855,6 +2855,9 @@ function buildPosOrderItem(product, body, id) {
|
||||
reference: body.reference || "",
|
||||
related_item_id: body.related_item_id ?? null,
|
||||
price: Number(body.price ?? product.price ?? 0),
|
||||
reason_code: body.reason_code || "",
|
||||
reason_label_snapshot: body.reason_label_snapshot || "",
|
||||
reason_comment: body.reason_comment || "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -593,7 +593,7 @@ describe("InvoicingPeriodObjectTree", () => {
|
||||
expect(orderItemWheel.exists()).toBe(true);
|
||||
expect(collectionWheel.attributes("data-action-count")).toBe("4");
|
||||
expect(collectionWheel.attributes("data-invoice-collection-id")).toBe("3001");
|
||||
expect(orderWheel.attributes("data-action-count")).toBe("3");
|
||||
expect(orderWheel.attributes("data-action-count")).toBe("4");
|
||||
expect(orderWheel.attributes("data-order-id")).toBe("9001");
|
||||
expect(orderWheel.attributes("data-department-id")).toBe("75");
|
||||
expect(orderItemWheel.attributes("data-action-count")).toBe("1");
|
||||
|
||||
Reference in New Issue
Block a user