365 lines
12 KiB
JavaScript
365 lines
12 KiB
JavaScript
// @vitest-environment jsdom
|
|
|
|
import { flushPromises, mount } from "@vue/test-utils";
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => {
|
|
const order = {
|
|
id: 9001,
|
|
invoice_collection_id: 3001,
|
|
department_id: 75,
|
|
created_at: "2026-06-18",
|
|
total_net_amount: 240,
|
|
reference: "REF-9001",
|
|
notes: "",
|
|
po: "",
|
|
reg_1: "AB12345",
|
|
};
|
|
return {
|
|
order,
|
|
economicDetails: vi.fn(async () => ({
|
|
data: {
|
|
data: {
|
|
collected_invoice_id: 3001,
|
|
economic: {
|
|
draft_id: 88,
|
|
booked_id: 99,
|
|
},
|
|
internal: {
|
|
normalized: {
|
|
totals: {
|
|
net_total: 240,
|
|
line_count: 1,
|
|
},
|
|
},
|
|
},
|
|
draft: {
|
|
exists: false,
|
|
normalized: null,
|
|
},
|
|
booked: {
|
|
exists: true,
|
|
normalized: {
|
|
totals: {
|
|
net_total: 240,
|
|
line_count: 1,
|
|
},
|
|
},
|
|
},
|
|
warnings: [],
|
|
},
|
|
},
|
|
})),
|
|
economicPdf: vi.fn(async () => ({ data: { data: { url: "https://example.test/invoice.pdf" } } })),
|
|
request: vi.fn(async (url, method) => {
|
|
if (url === "/order/items" && method === "GET") {
|
|
return {
|
|
data: {
|
|
data: [
|
|
{
|
|
id: 501,
|
|
order_id: 9001,
|
|
product_name: "Premium wash",
|
|
quantity: 2,
|
|
price: 120,
|
|
reference: "",
|
|
notes: "",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
return { data: { data: [] } };
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock("buefy", () => ({
|
|
BCheckbox: {
|
|
name: "BCheckbox",
|
|
props: {
|
|
modelValue: Boolean,
|
|
indeterminate: Boolean,
|
|
disabled: Boolean,
|
|
},
|
|
emits: ["update:modelValue"],
|
|
template: `
|
|
<button
|
|
class="b-checkbox-stub"
|
|
type="button"
|
|
:disabled="disabled"
|
|
:data-checked="modelValue"
|
|
:data-indeterminate="indeterminate"
|
|
@click="$emit('update:modelValue', !modelValue)"
|
|
></button>
|
|
`,
|
|
},
|
|
}));
|
|
|
|
vi.mock("sweetalert2", () => ({
|
|
default: {
|
|
fire: vi.fn(async () => ({ isConfirmed: false })),
|
|
},
|
|
}));
|
|
|
|
vi.mock("vue-i18n", () => ({
|
|
useI18n: () => ({
|
|
t: (key) => key,
|
|
locale: { value: "da-DK" },
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/components/displays/buttons/EditableTableColumn.vue", () => ({
|
|
default: {
|
|
name: "EditableTableColumn",
|
|
props: ["object", "column", "parseFunction"],
|
|
template: `<span class="editable-table-column-stub">{{ parseFunction ? parseFunction(object?.[column]) : object?.[column] }}</span>`,
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/displays/buttons/ActionSettingsWheelButton.vue", () => ({
|
|
default: {
|
|
name: "ActionSettingsWheelButton",
|
|
props: ["menuSections", "order_id", "department_id", "invoice_collection_id", "customer_number"],
|
|
template: `
|
|
<button
|
|
type="button"
|
|
class="action-settings-wheel-stub"
|
|
:data-menu-count="menuSections.length"
|
|
:data-action-count="menuSections.reduce((count, section) => count + (section.items?.length || 0), 0)"
|
|
:data-order-id="order_id || undefined"
|
|
:data-department-id="department_id || undefined"
|
|
:data-invoice-collection-id="invoice_collection_id || undefined"
|
|
:data-customer-number="customer_number || undefined"
|
|
></button>
|
|
`,
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
|
SessionUser: {
|
|
request: mocks.request,
|
|
functions: {
|
|
currency: {
|
|
toLocal: (value) => `${Number(value || 0)} DKK`,
|
|
},
|
|
parseErrorMessage: (error) => error?.message || String(error),
|
|
},
|
|
hasPermission: () => true,
|
|
canAccessAdmin: () => true,
|
|
canAccessSuperUser: () => true,
|
|
canAccessDepartment: () => true,
|
|
objects: {
|
|
orders: {
|
|
columns: {
|
|
reference: { label: "Reference" },
|
|
notes: { label: "Noter" },
|
|
po: { label: "PO" },
|
|
reg_1: { label: "Reg 1" },
|
|
reg_2: { label: "Reg 2" },
|
|
reg_3: { label: "Reg 3" },
|
|
include_in_invoice: { label: "Faktura" },
|
|
total_net_amount: { label: "Total" },
|
|
},
|
|
get: {
|
|
single: vi.fn(async () => mocks.order),
|
|
},
|
|
functions: {
|
|
fetchAttachments: vi.fn(async () => []),
|
|
downloadAttachment: vi.fn(async () => "https://example.test/attachment.pdf"),
|
|
removeAttachment: vi.fn(),
|
|
resendWashCertificate: vi.fn(),
|
|
mark_as_completed: vi.fn(),
|
|
},
|
|
set: {
|
|
include_in_invoice: vi.fn(),
|
|
invoice_collection_id: vi.fn(),
|
|
booking_id: vi.fn(),
|
|
wash_id: vi.fn(),
|
|
},
|
|
delete: {
|
|
single: vi.fn(),
|
|
},
|
|
showEditObjectFieldForm: vi.fn(),
|
|
},
|
|
collectedOrderInvoices: {
|
|
columns: {
|
|
name: { label: "Navn" },
|
|
notes: { label: "Noter" },
|
|
po_number: { label: "PO" },
|
|
customer_number: { label: "Kundenr." },
|
|
external_id: { label: "Eksternt ID" },
|
|
closed_at: { label: "Lukket" },
|
|
total_net_amount: { label: "Total" },
|
|
},
|
|
functions: {
|
|
economic: {
|
|
v2: {
|
|
details: mocks.economicDetails,
|
|
},
|
|
pdf: mocks.economicPdf,
|
|
},
|
|
bulk_action_preview: vi.fn(),
|
|
bulk_action_apply: vi.fn(),
|
|
},
|
|
showEditObjectFieldForm: vi.fn(),
|
|
},
|
|
order_bookings: {
|
|
get: {
|
|
single: vi.fn(),
|
|
},
|
|
functions: {
|
|
resendBookingConfirmation: vi.fn(),
|
|
resendBookingCompletionConfirmation: vi.fn(),
|
|
},
|
|
delete: {
|
|
single: vi.fn(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue", () => ({
|
|
invoiceQueue: {
|
|
addInvoiceCollectionsToQueue: vi.fn(),
|
|
processInvoiceCollectionQueue: vi.fn(),
|
|
},
|
|
default: {},
|
|
}));
|
|
|
|
vi.mock("@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js", () => ({
|
|
INVOICE_COLLECTION_BULK_ACTIONS: {
|
|
QUEUE_ECONOMIC: "queue_economic",
|
|
CLEAN_CUSTOMER_RULES: "clean_customer_rules",
|
|
MERGE: "merge",
|
|
SPLIT_BY_MONTH: "split_by_month",
|
|
RESET_HIDDEN_PRICES: "reset_hidden_prices",
|
|
},
|
|
}));
|
|
|
|
import InvoicingPeriodObjectTree from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue";
|
|
|
|
const mountTree = () => mount(InvoicingPeriodObjectTree, {
|
|
props: {
|
|
customer: {
|
|
customer_number: 2001,
|
|
customer_name: "ACME",
|
|
draft: {
|
|
has_valid_draft: true,
|
|
invoice_collection_ids: [3001],
|
|
},
|
|
},
|
|
transactions: [
|
|
{
|
|
...mocks.order,
|
|
booked: true,
|
|
},
|
|
],
|
|
excludedOrderIds: [],
|
|
dates: {
|
|
dateFrom: "2026-06-01",
|
|
dateTo: "2026-06-30",
|
|
},
|
|
invoicePeriodFlags: [],
|
|
},
|
|
});
|
|
|
|
const expandNode = async (wrapper, key) => {
|
|
const node = wrapper.find(`[data-node-key="${key}"]`);
|
|
expect(node.exists()).toBe(true);
|
|
await node.find(".b-tree-node-toggle").trigger("click");
|
|
await flushPromises();
|
|
};
|
|
|
|
describe("InvoicingPeriodObjectTree", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
Object.defineProperty(window, "open", {
|
|
configurable: true,
|
|
value: vi.fn(),
|
|
});
|
|
});
|
|
|
|
it("auto-expands order items when an order node is expanded", async () => {
|
|
const wrapper = mountTree();
|
|
|
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
|
await expandNode(wrapper, "category:3001:collection_orders");
|
|
await expandNode(wrapper, "order:9001");
|
|
|
|
expect(mocks.request).toHaveBeenCalledWith("/order/items", "GET", { order_id: 9001 });
|
|
expect(wrapper.find('[data-node-key="category:9001:order_items"]').classes()).toContain("is-expanded");
|
|
expect(wrapper.text()).toContain("Premium wash");
|
|
});
|
|
|
|
it("renders row action wheels for actionable collection, order, and order item nodes", async () => {
|
|
const wrapper = mountTree();
|
|
|
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
|
await expandNode(wrapper, "category:3001:collection_orders");
|
|
await expandNode(wrapper, "order:9001");
|
|
|
|
const collectionWheel = wrapper.find('[data-testid="invoice-period-tree-action-wheel-collected_order_invoice:3001"]');
|
|
const orderWheel = wrapper.find('[data-testid="invoice-period-tree-action-wheel-order:9001"]');
|
|
const orderItemWheel = wrapper.find('[data-testid="invoice-period-tree-action-wheel-order_item:501"]');
|
|
|
|
expect(collectionWheel.exists()).toBe(true);
|
|
expect(orderWheel.exists()).toBe(true);
|
|
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-order-id")).toBe("9001");
|
|
expect(orderWheel.attributes("data-department-id")).toBe("75");
|
|
expect(orderItemWheel.attributes("data-action-count")).toBe("1");
|
|
expect(wrapper.find('[data-testid="invoice-period-tree-action-wheel-category:9001:order_items"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("shows empty editable fields without a reveal toggle", async () => {
|
|
const wrapper = mountTree();
|
|
|
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
|
await expandNode(wrapper, "category:3001:collection_orders");
|
|
|
|
expect(wrapper.find('[data-testid="invoice-period-tree-field-order:9001-notes"]').exists()).toBe(true);
|
|
expect(wrapper.find('[data-testid="invoice-period-tree-field-value-order:9001-notes"]').text()).toContain("Tom");
|
|
expect(wrapper.find('[data-testid="invoice-period-tree-field-order:9001-reg_2"]').exists()).toBe(true);
|
|
expect(wrapper.find('[data-testid^="invoice-period-tree-field-empty-toggle-"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("omits redundant collection name and customer number fields", async () => {
|
|
const wrapper = mountTree();
|
|
|
|
expect(wrapper.find('[data-testid="invoice-period-tree-field-collected_order_invoice:3001-notes"]').exists()).toBe(true);
|
|
expect(wrapper.find('[data-testid="invoice-period-tree-field-collected_order_invoice:3001-name"]').exists()).toBe(false);
|
|
expect(wrapper.find('[data-testid="invoice-period-tree-field-collected_order_invoice:3001-customer_number"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("renders only resolved draft/booked e-conomic invoice children", async () => {
|
|
const wrapper = mountTree();
|
|
|
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
|
await expandNode(wrapper, "category:3001:collection_economic");
|
|
|
|
expect(mocks.economicDetails).toHaveBeenCalledWith(3001);
|
|
expect(wrapper.text()).not.toContain("E-conomic kladde #88");
|
|
expect(wrapper.text()).toContain("E-conomic faktura #99");
|
|
});
|
|
|
|
it("downloads booked e-conomic invoice pdfs for booked invoice nodes", async () => {
|
|
const wrapper = mountTree();
|
|
|
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
|
await expandNode(wrapper, "category:3001:collection_economic");
|
|
|
|
const bookedInvoiceNode = wrapper.find('[data-node-key="economic_invoice:3001:booked:99"]');
|
|
expect(bookedInvoiceNode.exists()).toBe(true);
|
|
await bookedInvoiceNode.find(".b-tree-node-content").trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.economicPdf).toHaveBeenCalledWith(3001, "booked");
|
|
expect(window.open).toHaveBeenCalledWith("https://example.test/invoice.pdf", "_blank", "noopener,noreferrer");
|
|
});
|
|
});
|