Add multiple select customer product price recalculation (#261)

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
This commit is contained in:
Jeppe B
2026-08-04 16:13:23 +00:00
committed by GitHub
co-authored by Jeppe Bundgaard
parent fd8b896c56
commit 1548ae8cd5
9 changed files with 108 additions and 9 deletions
@@ -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)
+25 -4
View File
@@ -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}`
@@ -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"),
+2
View File
@@ -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);
});
+3
View File
@@ -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 ?? ""),
};
}
+3
View File
@@ -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");