Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c26da10677 |
@@ -12,6 +12,11 @@ import UserOtherVaskeabonnement
|
|||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import OrderItemsTable from "@/components/displays/department/pos/order/orderItemsTable.vue";
|
import OrderItemsTable from "@/components/displays/department/pos/order/orderItemsTable.vue";
|
||||||
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
|
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
|
||||||
|
import {
|
||||||
|
buildMultiMonthInvoiceContext,
|
||||||
|
MULTI_MONTH_INVOICE_ACTION,
|
||||||
|
promptMultiMonthInvoiceWarning,
|
||||||
|
} from "@/services/invoiceMonthSplitWarning.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
orders: {
|
orders: {
|
||||||
@@ -126,32 +131,34 @@ const isAnyOrderSelected = () => {
|
|||||||
return selectedInvoiceCollections.value.length > 0;
|
return selectedInvoiceCollections.value.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSelectedOrders = () => {
|
||||||
|
return props.orders.filter((order) => selectedInvoiceCollections.value.includes(order.invoice_collection_id));
|
||||||
|
}
|
||||||
|
|
||||||
/** Invoice collections */
|
/** Invoice collections */
|
||||||
const onInvoiceCollections = async () => {
|
const onInvoiceCollections = async () => {
|
||||||
// Check if any orders are selected
|
// Check if any orders are selected
|
||||||
if (selectedInvoiceCollections.value.length === 0) {
|
if (selectedInvoiceCollections.value.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/** Create the invoice */
|
const selectedOrders = getSelectedOrders();
|
||||||
const onCreateInvoiceDraft = async () => {
|
const invoiceWarningContext = buildMultiMonthInvoiceContext(selectedOrders, {
|
||||||
// Create the invoice
|
getDate: (order) => order?.created_at ?? order?.date,
|
||||||
console.log('Create invoice');
|
getInvoiceCollectionId: (order) => order?.invoice_collection_id,
|
||||||
await SessionUser.objects.collectedOrderInvoices.functions.economic.invoice(parseInt(props.collectedOrderInvoice.id)).then((response) => {
|
});
|
||||||
console.log('Invoice created successfully', response);
|
const invoiceWarningAction = await promptMultiMonthInvoiceWarning({
|
||||||
Swal.fire({
|
context: invoiceWarningContext,
|
||||||
title: 'Fakturaen er oprettet',
|
splitByMonth: SessionUser.objects.collectedOrderInvoices.functions.split_by_month,
|
||||||
text: 'Fakturaen er oprettet i E-conomic',
|
parseErrorMessage: SessionUser.functions.parseErrorMessage,
|
||||||
icon: 'success',
|
});
|
||||||
showConfirmButton: false,
|
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.SPLIT) {
|
||||||
timer: 2000
|
location.reload();
|
||||||
}).then(() => {
|
return;
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.log('Error creating invoice', error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.CANCEL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < selectedInvoiceCollections.value.length; i++) {
|
for (let i = 0; i < selectedInvoiceCollections.value.length; i++) {
|
||||||
const selectedInvoiceCollectionId = selectedInvoiceCollections.value[i];
|
const selectedInvoiceCollectionId = selectedInvoiceCollections.value[i];
|
||||||
// Check if the invoice collection is already booked
|
// Check if the invoice collection is already booked
|
||||||
@@ -210,6 +217,7 @@ const isOrderContentVisible = (order) => {
|
|||||||
class="button is-small"
|
class="button is-small"
|
||||||
@click="onInvoiceCollections()"
|
@click="onInvoiceCollections()"
|
||||||
:disabled="!isAnyOrderSelected()"
|
:disabled="!isAnyOrderSelected()"
|
||||||
|
data-testid="invoice-order-table-invoice-button"
|
||||||
>
|
>
|
||||||
{{ $t('global.invoice_now') }}
|
{{ $t('global.invoice_now') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -274,4 +282,4 @@ const isOrderContentVisible = (order) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -444,10 +444,14 @@ export const CollectedOrderInvoices = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
split_by_month: async (dateFrom, dateTo, options = {}) => {
|
split_by_month: async (dateFrom, dateTo, options = {}) => {
|
||||||
|
const invoiceCollectionIds = Array.isArray(options.invoiceCollectionIds)
|
||||||
|
? options.invoiceCollectionIds
|
||||||
|
: options.invoice_collection_ids;
|
||||||
return authenticatedRequest('/collected-invoices/split-by-month', 'POST', {
|
return authenticatedRequest('/collected-invoices/split-by-month', 'POST', {
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
...(options.preview !== undefined ? { preview: !!options.preview } : {}),
|
...(options.preview !== undefined ? { preview: !!options.preview } : {}),
|
||||||
|
...(Array.isArray(invoiceCollectionIds) ? { invoice_collection_ids: invoiceCollectionIds } : {}),
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -4028,6 +4028,15 @@
|
|||||||
"preview_title": "@:{'words.generated.forhandsvis'} @:{'words.generated.manedsopdeling'}",
|
"preview_title": "@:{'words.generated.forhandsvis'} @:{'words.generated.manedsopdeling'}",
|
||||||
"success_text": "@:{'words.generated.behandlede'} {processed} @:{'words.generated.fakturasamlinger'}. Opdelte {changed} @:{'words.generated.og'} sprang {skipped} @:{'words.generated.over'}.",
|
"success_text": "@:{'words.generated.behandlede'} {processed} @:{'words.generated.fakturasamlinger'}. Opdelte {changed} @:{'words.generated.og'} sprang {skipped} @:{'words.generated.over'}.",
|
||||||
"success_title": "@.capitalize:{'words.generated.manedsopdeling'} @:{'words.generated.fuldført'}"
|
"success_title": "@.capitalize:{'words.generated.manedsopdeling'} @:{'words.generated.fuldført'}"
|
||||||
|
},
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Fakturer sammen",
|
||||||
|
"split_by_month": "Opdel efter måned",
|
||||||
|
"split_error_title": "Månedsopdeling mislykkedes",
|
||||||
|
"split_success_text": "Behandlede {processed} fakturasamlinger. Opdelte {changed} og sprang {skipped} over.",
|
||||||
|
"split_success_title": "Månedsopdeling fuldført",
|
||||||
|
"text": "Du er ved at fakturere ordrer fra flere måneder sammen ({months}). Skal de i stedet opdeles efter måned?",
|
||||||
|
"title": "Ordrer fra flere måneder"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"invoicing": {
|
"invoicing": {
|
||||||
|
|||||||
@@ -4139,6 +4139,15 @@
|
|||||||
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
||||||
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
||||||
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
||||||
|
},
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Zusammen abrechnen",
|
||||||
|
"split_by_month": "Nach Monat aufteilen",
|
||||||
|
"split_error_title": "Monatsaufteilung fehlgeschlagen",
|
||||||
|
"split_success_text": "{processed} Rechnungssammlungen verarbeitet. {changed} aufgeteilt, {skipped} übersprungen.",
|
||||||
|
"split_success_title": "Monatsaufteilung abgeschlossen",
|
||||||
|
"text": "Sie sind dabei, Aufträge aus mehreren Monaten gemeinsam abzurechnen ({months}). Sollen sie stattdessen nach Monat aufgeteilt werden?",
|
||||||
|
"title": "Aufträge aus mehreren Monaten"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"invoicing": {
|
"invoicing": {
|
||||||
|
|||||||
@@ -3860,6 +3860,15 @@
|
|||||||
"preview_title": "@.capitalize:{'words.generated.preview'} @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
"preview_title": "@.capitalize:{'words.generated.preview'} @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
||||||
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
||||||
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
||||||
|
},
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Invoice together",
|
||||||
|
"split_by_month": "Split by month",
|
||||||
|
"split_error_title": "Monthly split failed",
|
||||||
|
"split_success_text": "Processed {processed} invoice collections. Split {changed}, skipped {skipped}.",
|
||||||
|
"split_success_title": "Monthly split completed",
|
||||||
|
"text": "You are about to invoice orders from multiple months together ({months}). Should they be split by month instead?",
|
||||||
|
"title": "Orders from multiple months"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"invoicing": {
|
"invoicing": {
|
||||||
|
|||||||
@@ -3149,6 +3149,15 @@
|
|||||||
"preview_title": "@:{'templates.generated.compat.invoicing_period.monthly_split.preview_title'}",
|
"preview_title": "@:{'templates.generated.compat.invoicing_period.monthly_split.preview_title'}",
|
||||||
"success_text": "@:{'templates.generated.compat.invoicing_period.monthly_split.success_text'}",
|
"success_text": "@:{'templates.generated.compat.invoicing_period.monthly_split.success_text'}",
|
||||||
"success_title": "@:{'templates.generated.compat.invoicing_period.monthly_split.success_title'}"
|
"success_title": "@:{'templates.generated.compat.invoicing_period.monthly_split.success_title'}"
|
||||||
|
},
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.invoice_together'}",
|
||||||
|
"split_by_month": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_by_month'}",
|
||||||
|
"split_error_title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_error_title'}",
|
||||||
|
"split_success_text": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_success_text'}",
|
||||||
|
"split_success_title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_success_title'}",
|
||||||
|
"text": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.text'}",
|
||||||
|
"title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.title'}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"invoicing": {
|
"invoicing": {
|
||||||
|
|||||||
@@ -4142,6 +4142,15 @@
|
|||||||
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
||||||
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
||||||
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
||||||
|
},
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Fakturer samlet",
|
||||||
|
"split_by_month": "Del opp etter måned",
|
||||||
|
"split_error_title": "Månedsdeling mislyktes",
|
||||||
|
"split_success_text": "Behandlet {processed} fakturasamlinger. Delte opp {changed}, hoppet over {skipped}.",
|
||||||
|
"split_success_title": "Månedsdeling fullført",
|
||||||
|
"text": "Du er i ferd med å fakturere ordrer fra flere måneder samlet ({months}). Skal de i stedet deles opp etter måned?",
|
||||||
|
"title": "Ordrer fra flere måneder"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"invoicing": {
|
"invoicing": {
|
||||||
|
|||||||
@@ -4192,6 +4192,15 @@
|
|||||||
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
||||||
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
||||||
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
||||||
|
},
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Fakturera tillsammans",
|
||||||
|
"split_by_month": "Dela upp per månad",
|
||||||
|
"split_error_title": "Månadsuppdelning misslyckades",
|
||||||
|
"split_success_text": "Bearbetade {processed} fakturasamlingar. Delade upp {changed}, hoppade över {skipped}.",
|
||||||
|
"split_success_title": "Månadsuppdelning klar",
|
||||||
|
"text": "Du håller på att fakturera ordrar från flera månader tillsammans ({months}). Ska de delas upp per månad i stället?",
|
||||||
|
"title": "Ordrar från flera månader"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"invoicing": {
|
"invoicing": {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compat": {
|
||||||
|
"invoicing_period": {
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Fakturer sammen",
|
||||||
|
"split_by_month": "Opdel efter måned",
|
||||||
|
"split_error_title": "Månedsopdeling mislykkedes",
|
||||||
|
"split_success_text": "Behandlede {processed} fakturasamlinger. Opdelte {changed} og sprang {skipped} over.",
|
||||||
|
"split_success_title": "Månedsopdeling fuldført",
|
||||||
|
"text": "Du er ved at fakturere ordrer fra flere måneder sammen ({months}). Skal de i stedet opdeles efter måned?",
|
||||||
|
"title": "Ordrer fra flere måneder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compat": {
|
||||||
|
"invoicing_period": {
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Zusammen abrechnen",
|
||||||
|
"split_by_month": "Nach Monat aufteilen",
|
||||||
|
"split_error_title": "Monatsaufteilung fehlgeschlagen",
|
||||||
|
"split_success_text": "{processed} Rechnungssammlungen verarbeitet. {changed} aufgeteilt, {skipped} übersprungen.",
|
||||||
|
"split_success_title": "Monatsaufteilung abgeschlossen",
|
||||||
|
"text": "Sie sind dabei, Aufträge aus mehreren Monaten gemeinsam abzurechnen ({months}). Sollen sie stattdessen nach Monat aufgeteilt werden?",
|
||||||
|
"title": "Aufträge aus mehreren Monaten"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compat": {
|
||||||
|
"invoicing_period": {
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Invoice together",
|
||||||
|
"split_by_month": "Split by month",
|
||||||
|
"split_error_title": "Monthly split failed",
|
||||||
|
"split_success_text": "Processed {processed} invoice collections. Split {changed}, skipped {skipped}.",
|
||||||
|
"split_success_title": "Monthly split completed",
|
||||||
|
"text": "You are about to invoice orders from multiple months together ({months}). Should they be split by month instead?",
|
||||||
|
"title": "Orders from multiple months"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"invoicing_period": {
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.invoice_together'}",
|
||||||
|
"split_by_month": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_by_month'}",
|
||||||
|
"split_error_title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_error_title'}",
|
||||||
|
"split_success_text": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_success_text'}",
|
||||||
|
"split_success_title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_success_title'}",
|
||||||
|
"text": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.text'}",
|
||||||
|
"title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.title'}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compat": {
|
||||||
|
"invoicing_period": {
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Fakturer samlet",
|
||||||
|
"split_by_month": "Del opp etter måned",
|
||||||
|
"split_error_title": "Månedsdeling mislyktes",
|
||||||
|
"split_success_text": "Behandlet {processed} fakturasamlinger. Delte opp {changed}, hoppet over {skipped}.",
|
||||||
|
"split_success_title": "Månedsdeling fullført",
|
||||||
|
"text": "Du er i ferd med å fakturere ordrer fra flere måneder samlet ({months}). Skal de i stedet deles opp etter måned?",
|
||||||
|
"title": "Ordrer fra flere måneder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compat": {
|
||||||
|
"invoicing_period": {
|
||||||
|
"multi_month_invoice_warning": {
|
||||||
|
"invoice_together": "Fakturera tillsammans",
|
||||||
|
"split_by_month": "Dela upp per månad",
|
||||||
|
"split_error_title": "Månadsuppdelning misslyckades",
|
||||||
|
"split_success_text": "Bearbetade {processed} fakturasamlingar. Delade upp {changed}, hoppade över {skipped}.",
|
||||||
|
"split_success_title": "Månadsuppdelning klar",
|
||||||
|
"text": "Du håller på att fakturera ordrar från flera månader tillsammans ({months}). Ska de delas upp per månad i stället?",
|
||||||
|
"title": "Ordrar från flera månader"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import Swal from "sweetalert2";
|
||||||
|
import i18n from "@/i18n";
|
||||||
|
|
||||||
|
export const MULTI_MONTH_INVOICE_ACTION = {
|
||||||
|
CONTINUE: "continue",
|
||||||
|
CANCEL: "cancel",
|
||||||
|
SPLIT: "split",
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPositiveInteger = (value) => {
|
||||||
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
||||||
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDateKey = (value) => {
|
||||||
|
const rawValue = String(value ?? "").trim();
|
||||||
|
const directMatch = rawValue.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||||
|
if (directMatch) {
|
||||||
|
return `${directMatch[1]}-${directMatch[2]}-${directMatch[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedDate = new Date(rawValue);
|
||||||
|
if (Number.isNaN(parsedDate.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = parsedDate.getFullYear();
|
||||||
|
const month = String(parsedDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(parsedDate.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMonthKey = (value) => {
|
||||||
|
const dateKey = getDateKey(value);
|
||||||
|
return dateKey ? dateKey.slice(0, 7) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const translate = (key, params = {}) => i18n.global.t(key, params);
|
||||||
|
|
||||||
|
const getSplitResponsePayload = (response = {}) => response?.data?.data ?? response?.data ?? {};
|
||||||
|
|
||||||
|
export const buildMultiMonthInvoiceContext = (
|
||||||
|
items = [],
|
||||||
|
{
|
||||||
|
getDate = (item) => item?.created_at ?? item?.date,
|
||||||
|
getInvoiceCollectionId = (item) => item?.invoice_collection_id,
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
|
const months = new Set();
|
||||||
|
const dateKeys = [];
|
||||||
|
const invoiceCollectionIds = new Set();
|
||||||
|
|
||||||
|
(Array.isArray(items) ? items : []).forEach((item) => {
|
||||||
|
const dateValue = getDate(item);
|
||||||
|
const dateKey = getDateKey(dateValue);
|
||||||
|
if (dateKey) {
|
||||||
|
dateKeys.push(dateKey);
|
||||||
|
months.add(dateKey.slice(0, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceCollectionId = toPositiveInteger(getInvoiceCollectionId(item));
|
||||||
|
if (invoiceCollectionId) {
|
||||||
|
invoiceCollectionIds.add(invoiceCollectionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dateKeys.sort();
|
||||||
|
|
||||||
|
return {
|
||||||
|
months: Array.from(months).sort(),
|
||||||
|
dateFrom: dateKeys[0] ?? null,
|
||||||
|
dateTo: dateKeys[dateKeys.length - 1] ?? null,
|
||||||
|
invoiceCollectionIds: Array.from(invoiceCollectionIds).sort((left, right) => left - right),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldWarnAboutMultiMonthInvoice = (context = {}) => (
|
||||||
|
Array.isArray(context.months) &&
|
||||||
|
context.months.length > 1 &&
|
||||||
|
Array.isArray(context.invoiceCollectionIds) &&
|
||||||
|
context.invoiceCollectionIds.length > 0 &&
|
||||||
|
Boolean(context.dateFrom) &&
|
||||||
|
Boolean(context.dateTo)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const promptMultiMonthInvoiceWarning = async ({
|
||||||
|
context,
|
||||||
|
splitByMonth,
|
||||||
|
parseErrorMessage = (error) => error?.message ?? String(error),
|
||||||
|
} = {}) => {
|
||||||
|
if (!shouldWarnAboutMultiMonthInvoice(context)) {
|
||||||
|
return MULTI_MONTH_INVOICE_ACTION.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const months = context.months.join(", ");
|
||||||
|
const confirmation = await Swal.fire({
|
||||||
|
icon: "warning",
|
||||||
|
title: translate("invoicing_period.multi_month_invoice_warning.title"),
|
||||||
|
text: translate("invoicing_period.multi_month_invoice_warning.text", { months }),
|
||||||
|
showCancelButton: true,
|
||||||
|
showDenyButton: true,
|
||||||
|
confirmButtonText: translate("invoicing_period.multi_month_invoice_warning.split_by_month"),
|
||||||
|
denyButtonText: translate("invoicing_period.multi_month_invoice_warning.invoice_together"),
|
||||||
|
cancelButtonText: translate("common.cancel"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirmation.isDenied) {
|
||||||
|
return MULTI_MONTH_INVOICE_ACTION.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmation.isConfirmed) {
|
||||||
|
return MULTI_MONTH_INVOICE_ACTION.CANCEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await splitByMonth(context.dateFrom, context.dateTo, {
|
||||||
|
invoiceCollectionIds: context.invoiceCollectionIds,
|
||||||
|
preview: false,
|
||||||
|
});
|
||||||
|
const result = getSplitResponsePayload(response);
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: translate("invoicing_period.multi_month_invoice_warning.split_success_title"),
|
||||||
|
text: translate("invoicing_period.multi_month_invoice_warning.split_success_text", {
|
||||||
|
processed: result.processed_count ?? 0,
|
||||||
|
changed: result.changed_count ?? 0,
|
||||||
|
skipped: result.skipped_count ?? 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return MULTI_MONTH_INVOICE_ACTION.SPLIT;
|
||||||
|
} catch (error) {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: translate("invoicing_period.multi_month_invoice_warning.split_error_title"),
|
||||||
|
text: parseErrorMessage(error),
|
||||||
|
});
|
||||||
|
return MULTI_MONTH_INVOICE_ACTION.CANCEL;
|
||||||
|
}
|
||||||
|
};
|
||||||
+36
-4
@@ -22,6 +22,11 @@ import InvoicingBillingPeriodFilters from "@/views/dashboards/superUserDashboard
|
|||||||
import InvoicingBillingPeriodCustomerAttributes from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/InvoicingBillingPeriodCustomerAttributes.vue";
|
import InvoicingBillingPeriodCustomerAttributes from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/InvoicingBillingPeriodCustomerAttributes.vue";
|
||||||
import InvoicingPeriodFlagList from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue";
|
import InvoicingPeriodFlagList from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue";
|
||||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
import {
|
||||||
|
buildMultiMonthInvoiceContext,
|
||||||
|
MULTI_MONTH_INVOICE_ACTION,
|
||||||
|
promptMultiMonthInvoiceWarning,
|
||||||
|
} from "@/services/invoiceMonthSplitWarning.js";
|
||||||
import {
|
import {
|
||||||
buildPossibleDuplicateGroups,
|
buildPossibleDuplicateGroups,
|
||||||
formatDuplicateDateLabel,
|
formatDuplicateDateLabel,
|
||||||
@@ -330,6 +335,13 @@ const parsePositiveInteger = (value: any) => {
|
|||||||
|
|
||||||
const getCustomerNumber = (customer: any) => parsePositiveInteger(customer?.customer_number);
|
const getCustomerNumber = (customer: any) => parsePositiveInteger(customer?.customer_number);
|
||||||
|
|
||||||
|
const markCustomerPeriodRefreshLoading = (customer: any) => {
|
||||||
|
const customerNumber = getCustomerNumber(customer);
|
||||||
|
if (customerNumber) {
|
||||||
|
invoiceQueue.markPeriodRefreshLoading?.([customerNumber], []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const queueInvoiceCollections = (invoiceCollectionIds: number[], customer: any = null) => {
|
const queueInvoiceCollections = (invoiceCollectionIds: number[], customer: any = null) => {
|
||||||
const uniqueInvoiceCollectionIds = Array.from(
|
const uniqueInvoiceCollectionIds = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@@ -351,12 +363,10 @@ const queueInvoiceCollections = (invoiceCollectionIds: number[], customer: any =
|
|||||||
|
|
||||||
const onClickInvoiceNow = async (customer: any, transactionIds: number[]) => {
|
const onClickInvoiceNow = async (customer: any, transactionIds: number[]) => {
|
||||||
const customerNumber = getCustomerNumber(customer);
|
const customerNumber = getCustomerNumber(customer);
|
||||||
if (customerNumber) {
|
|
||||||
invoiceQueue.markPeriodRefreshLoading?.([customerNumber], []);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (transactionIds.length === 0) {
|
if (transactionIds.length === 0) {
|
||||||
|
markCustomerPeriodRefreshLoading(customer);
|
||||||
const month = dates.variables.start.value.getMonth() + 1;
|
const month = dates.variables.start.value.getMonth() + 1;
|
||||||
const year = dates.variables.start.value.getFullYear();
|
const year = dates.variables.start.value.getFullYear();
|
||||||
|
|
||||||
@@ -392,10 +402,32 @@ const onClickInvoiceNow = async (customer: any, transactionIds: number[]) => {
|
|||||||
await fetchMissingInvoiceCollections(customer, transactionIds);
|
await fetchMissingInvoiceCollections(customer, transactionIds);
|
||||||
const invoiceCollectionIds = getInvoiceCollectionIdsForTransactionIds(customer, transactionIds);
|
const invoiceCollectionIds = getInvoiceCollectionIdsForTransactionIds(customer, transactionIds);
|
||||||
if (invoiceCollectionIds.length === 0) {
|
if (invoiceCollectionIds.length === 0) {
|
||||||
invoiceQueue.finishPeriodRefresh?.([customerNumber], []);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const invoiceWarningContext = buildMultiMonthInvoiceContext(
|
||||||
|
transactionIds
|
||||||
|
.map((transactionId) => getTransactionById(customer, transactionId))
|
||||||
|
.filter((transaction: any) => transaction !== null),
|
||||||
|
{
|
||||||
|
getDate: (transaction: any) => transaction?.date ?? transaction?.created_at,
|
||||||
|
getInvoiceCollectionId: (transaction: any) => getTransactionInvoiceCollectionId(transaction),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const invoiceWarningAction = await promptMultiMonthInvoiceWarning({
|
||||||
|
context: invoiceWarningContext,
|
||||||
|
splitByMonth: SessionUser.objects.collectedOrderInvoices.functions.split_by_month,
|
||||||
|
parseErrorMessage: SessionUser.functions.parseErrorMessage,
|
||||||
|
});
|
||||||
|
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.SPLIT) {
|
||||||
|
reloadPeriodPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.CANCEL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
markCustomerPeriodRefreshLoading(customer);
|
||||||
queueInvoiceCollections(invoiceCollectionIds, customer);
|
queueInvoiceCollections(invoiceCollectionIds, customer);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
invoiceQueue.finishPeriodRefresh?.([customerNumber], []);
|
invoiceQueue.finishPeriodRefresh?.([customerNumber], []);
|
||||||
|
|||||||
@@ -1933,6 +1933,111 @@ test.describe("Invoicing period tab", () => {
|
|||||||
await expect(page).toHaveURL(/activeTab=period/);
|
await expect(page).toHaveURL(/activeTab=period/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("@smoke period view warns and splits selected multi-month invoice collections", async ({ page }) => {
|
||||||
|
const splitRequests = [];
|
||||||
|
const economicInvoiceRequests = [];
|
||||||
|
await openPeriodView(page, {
|
||||||
|
payloadFactory: () => ({
|
||||||
|
types: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: 31,
|
||||||
|
customer_number: 4301,
|
||||||
|
customer_name: "Multi Month Fleet",
|
||||||
|
requires_action: true,
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
id: 8801,
|
||||||
|
date: "2026-03-28T10:00:00.000Z",
|
||||||
|
amount: 120,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
invoice_collection_id: 88001,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8802,
|
||||||
|
date: "2026-04-02T10:00:00.000Z",
|
||||||
|
amount: 180,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
invoice_collection_id: 88001,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queue: {
|
||||||
|
has_active_job: false,
|
||||||
|
statuses: [],
|
||||||
|
invoice_collection_ids: [],
|
||||||
|
is_action_blocked: false,
|
||||||
|
},
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invoice_per_order: [],
|
||||||
|
fixed_pricing: [],
|
||||||
|
tank_cleaning: [],
|
||||||
|
special_arrangements: [],
|
||||||
|
vehicle_subscriptions: [],
|
||||||
|
possible_duplicates: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/collected-invoices/split-by-month**", async (route) => {
|
||||||
|
if (
|
||||||
|
route.request().method() !== "POST" ||
|
||||||
|
!matchesApiPath(route.request().url(), "/collected-invoices/split-by-month")
|
||||||
|
) {
|
||||||
|
await route.fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.parse(route.request().postData() || "{}");
|
||||||
|
splitRequests.push(payload);
|
||||||
|
await route.fulfill(
|
||||||
|
json({
|
||||||
|
data: {
|
||||||
|
preview: false,
|
||||||
|
processed_count: 1,
|
||||||
|
changed_count: 1,
|
||||||
|
skipped_count: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/collected-invoices/economic**", async (route) => {
|
||||||
|
if (
|
||||||
|
route.request().method() === "POST" &&
|
||||||
|
matchesApiPath(route.request().url(), "/collected-invoices/economic")
|
||||||
|
) {
|
||||||
|
economicInvoiceRequests.push(JSON.parse(route.request().postData() || "{}"));
|
||||||
|
await route.fulfill(json({ data: {} }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.fallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
||||||
|
await expect(page.getByTestId("invoicing-period-customer-4301")).toBeVisible();
|
||||||
|
await page.getByTestId("invoicing-period-customer-invoice-4301").click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: /Orders from multiple months|Ordrer fra flere måneder/i })
|
||||||
|
).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: /Split by month|Opdel efter måned/i }).click();
|
||||||
|
|
||||||
|
await expect.poll(() => splitRequests.length).toBe(1);
|
||||||
|
expect(splitRequests[0]).toEqual({
|
||||||
|
dateFrom: "2026-03-28",
|
||||||
|
dateTo: "2026-04-02",
|
||||||
|
invoice_collection_ids: [88001],
|
||||||
|
preview: false,
|
||||||
|
});
|
||||||
|
expect(economicInvoiceRequests).toEqual([]);
|
||||||
|
await expect(page.getByText(/Monthly split completed|Månedsopdeling fuldført/i)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("@smoke period view invoices multiple customers with page refresh and independent loading", async ({ page }) => {
|
test("@smoke period view invoices multiple customers with page refresh and independent loading", async ({ page }) => {
|
||||||
const token = "superuser-period-parallel-token";
|
const token = "superuser-period-parallel-token";
|
||||||
const periodRequests = [];
|
const periodRequests = [];
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("sweetalert2", () => ({
|
||||||
|
default: {
|
||||||
|
fire: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import {
|
||||||
|
buildMultiMonthInvoiceContext,
|
||||||
|
MULTI_MONTH_INVOICE_ACTION,
|
||||||
|
promptMultiMonthInvoiceWarning,
|
||||||
|
shouldWarnAboutMultiMonthInvoice,
|
||||||
|
} from "@/services/invoiceMonthSplitWarning.js";
|
||||||
|
|
||||||
|
describe("invoice month split warning", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Swal.fire.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds a scoped multi-month context without timezone shifting date strings", () => {
|
||||||
|
const context = buildMultiMonthInvoiceContext([
|
||||||
|
{ id: 1, created_at: "2026-03-31 23:30:00", invoice_collection_id: 501 },
|
||||||
|
{ id: 2, created_at: "2026-04-01T00:30:00.000Z", invoice_collection_id: "501" },
|
||||||
|
{ id: 3, created_at: "2026-04-03 09:00:00", invoice_collection_id: 502 },
|
||||||
|
{ id: 4, created_at: "invalid", invoice_collection_id: null },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(context).toEqual({
|
||||||
|
months: ["2026-03", "2026-04"],
|
||||||
|
dateFrom: "2026-03-31",
|
||||||
|
dateTo: "2026-04-03",
|
||||||
|
invoiceCollectionIds: [501, 502],
|
||||||
|
});
|
||||||
|
expect(shouldWarnAboutMultiMonthInvoice(context)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues without a modal for single-month selections", async () => {
|
||||||
|
const action = await promptMultiMonthInvoiceWarning({
|
||||||
|
context: buildMultiMonthInvoiceContext([
|
||||||
|
{ created_at: "2026-04-01 10:00:00", invoice_collection_id: 501 },
|
||||||
|
{ created_at: "2026-04-02 10:00:00", invoice_collection_id: 501 },
|
||||||
|
]),
|
||||||
|
splitByMonth: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(action).toBe(MULTI_MONTH_INVOICE_ACTION.CONTINUE);
|
||||||
|
expect(Swal.fire).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits selected invoice collections when the warning is confirmed", async () => {
|
||||||
|
const splitByMonth = vi.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
processed_count: 2,
|
||||||
|
changed_count: 1,
|
||||||
|
skipped_count: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Swal.fire.mockResolvedValueOnce({ isConfirmed: true }).mockResolvedValueOnce({ isConfirmed: true });
|
||||||
|
|
||||||
|
const action = await promptMultiMonthInvoiceWarning({
|
||||||
|
context: buildMultiMonthInvoiceContext([
|
||||||
|
{ created_at: "2026-03-20 10:00:00", invoice_collection_id: 7001 },
|
||||||
|
{ created_at: "2026-04-02 10:00:00", invoice_collection_id: 7001 },
|
||||||
|
]),
|
||||||
|
splitByMonth,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(action).toBe(MULTI_MONTH_INVOICE_ACTION.SPLIT);
|
||||||
|
expect(splitByMonth).toHaveBeenCalledWith("2026-03-20", "2026-04-02", {
|
||||||
|
invoiceCollectionIds: [7001],
|
||||||
|
preview: false,
|
||||||
|
});
|
||||||
|
expect(Swal.fire).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({
|
||||||
|
icon: "warning",
|
||||||
|
showDenyButton: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(Swal.fire).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.objectContaining({
|
||||||
|
icon: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues invoicing together when the warning deny button is selected", async () => {
|
||||||
|
const splitByMonth = vi.fn();
|
||||||
|
Swal.fire.mockResolvedValueOnce({ isDenied: true });
|
||||||
|
|
||||||
|
const action = await promptMultiMonthInvoiceWarning({
|
||||||
|
context: buildMultiMonthInvoiceContext([
|
||||||
|
{ created_at: "2026-03-20 10:00:00", invoice_collection_id: 7001 },
|
||||||
|
{ created_at: "2026-04-02 10:00:00", invoice_collection_id: 7001 },
|
||||||
|
]),
|
||||||
|
splitByMonth,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(action).toBe(MULTI_MONTH_INVOICE_ACTION.CONTINUE);
|
||||||
|
expect(splitByMonth).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels invoicing when the warning is dismissed", async () => {
|
||||||
|
const splitByMonth = vi.fn();
|
||||||
|
Swal.fire.mockResolvedValueOnce({ isDismissed: true });
|
||||||
|
|
||||||
|
const action = await promptMultiMonthInvoiceWarning({
|
||||||
|
context: buildMultiMonthInvoiceContext([
|
||||||
|
{ created_at: "2026-03-20 10:00:00", invoice_collection_id: 7001 },
|
||||||
|
{ created_at: "2026-04-02 10:00:00", invoice_collection_id: 7001 },
|
||||||
|
]),
|
||||||
|
splitByMonth,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(action).toBe(MULTI_MONTH_INVOICE_ACTION.CANCEL);
|
||||||
|
expect(splitByMonth).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { flushPromises, mount } from "@vue/test-utils";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("vue-i18n", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useI18n: () => ({
|
||||||
|
t: (key) => key,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("sweetalert2", () => ({
|
||||||
|
default: {
|
||||||
|
fire: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||||
|
SessionUser: {
|
||||||
|
objects: {
|
||||||
|
global: {
|
||||||
|
language: {
|
||||||
|
hide_content: "Hide",
|
||||||
|
show_content: "Show",
|
||||||
|
status: "Status",
|
||||||
|
completed: "Completed",
|
||||||
|
not_completed: "Not completed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orders: {
|
||||||
|
columns: {
|
||||||
|
id: { label: "ID", visible: true },
|
||||||
|
created_at: { label: "Created", visible: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collectedOrderInvoices: {
|
||||||
|
functions: {
|
||||||
|
split_by_month: vi.fn(),
|
||||||
|
economic: {
|
||||||
|
invoice: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vehicles: {
|
||||||
|
columns: {
|
||||||
|
wash_subscription: {
|
||||||
|
label: "Subscription",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
functions: {
|
||||||
|
currency: {
|
||||||
|
toLocal: (value) => String(value),
|
||||||
|
},
|
||||||
|
parseErrorMessage: (error) => error?.message ?? String(error),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/views/dashboards/superUserDashboard/user/displays/other/UserOtherSpecialArrangement.vue", () => ({
|
||||||
|
default: { template: "<div />" },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/views/dashboards/superUserDashboard/user/displays/other/UserOtherVaskeabonnement.vue", () => ({
|
||||||
|
default: { template: "<div />" },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/displays/department/pos/order/orderItemsTable.vue", () => ({
|
||||||
|
default: { template: "<div />" },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/displays/superuser/tables/OrderContentTable.vue", () => ({
|
||||||
|
default: { template: "<div />" },
|
||||||
|
}));
|
||||||
|
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import InvoiceOrderTable from "@/components/displays/superuser/tables/InvoiceOrderTable.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const mountTable = (orders) =>
|
||||||
|
mount(InvoiceOrderTable, {
|
||||||
|
props: {
|
||||||
|
orders,
|
||||||
|
options: {},
|
||||||
|
columns: {},
|
||||||
|
user_id: 42,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$t: (key) =>
|
||||||
|
({
|
||||||
|
"global.invoice_now": "Invoice now",
|
||||||
|
"global.unselect": "Unselect",
|
||||||
|
"common.all": "All",
|
||||||
|
"common.select": "Select",
|
||||||
|
}[key] ?? key),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("InvoiceOrderTable multi-month warning", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Swal.fire.mockReset();
|
||||||
|
SessionUser.objects.collectedOrderInvoices.functions.split_by_month.mockReset();
|
||||||
|
SessionUser.objects.collectedOrderInvoices.functions.economic.invoice.mockReset();
|
||||||
|
SessionUser.objects.collectedOrderInvoices.functions.economic.invoice.mockResolvedValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns for selected invoice collections containing orders from multiple months before invoicing together", async () => {
|
||||||
|
Swal.fire.mockResolvedValueOnce({ isDenied: true }).mockReturnValueOnce(new Promise(() => {}));
|
||||||
|
const wrapper = mountTable([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
invoice_collection_id: 9001,
|
||||||
|
created_at: "2026-03-15 10:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
invoice_collection_id: 9001,
|
||||||
|
created_at: "2026-04-02 10:00:00",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await wrapper.find("tbody input[type='checkbox']").trigger("click");
|
||||||
|
await wrapper.get("[data-testid='invoice-order-table-invoice-button']").trigger("click");
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(Swal.fire).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({
|
||||||
|
icon: "warning",
|
||||||
|
showDenyButton: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).not.toHaveBeenCalled();
|
||||||
|
expect(SessionUser.objects.collectedOrderInvoices.functions.economic.invoice).toHaveBeenCalledWith(9001, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not warn for selected invoice collections containing only one month", async () => {
|
||||||
|
Swal.fire.mockReturnValueOnce(new Promise(() => {}));
|
||||||
|
const wrapper = mountTable([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
invoice_collection_id: 9002,
|
||||||
|
created_at: "2026-04-01 10:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
invoice_collection_id: 9002,
|
||||||
|
created_at: "2026-04-02 10:00:00",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await wrapper.find("tbody input[type='checkbox']").trigger("click");
|
||||||
|
await wrapper.get("[data-testid='invoice-order-table-invoice-button']").trigger("click");
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(Swal.fire).toHaveBeenCalledTimes(1);
|
||||||
|
expect(Swal.fire).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Fakturaer oprettet",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(SessionUser.objects.collectedOrderInvoices.functions.economic.invoice).toHaveBeenCalledWith(9002, 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
import { computed, nextTick } from "vue";
|
import { computed, nextTick } from "vue";
|
||||||
import { mount } from "@vue/test-utils";
|
import { flushPromises, mount } from "@vue/test-utils";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -34,6 +34,12 @@ vi.mock("@/services/economicTransferQueue.js", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("sweetalert2", () => ({
|
||||||
|
default: {
|
||||||
|
fire: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||||
SessionUser: {
|
SessionUser: {
|
||||||
objects: {
|
objects: {
|
||||||
@@ -62,6 +68,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
|||||||
createVehicleSubscriptionInvoice: vi.fn(),
|
createVehicleSubscriptionInvoice: vi.fn(),
|
||||||
add_fixed_pricing: vi.fn(),
|
add_fixed_pricing: vi.fn(),
|
||||||
add_vehicle_subscriptions: vi.fn(),
|
add_vehicle_subscriptions: vi.fn(),
|
||||||
|
split_by_month: vi.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
vehicles: {
|
vehicles: {
|
||||||
@@ -76,6 +83,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
|||||||
currency: {
|
currency: {
|
||||||
toLocal: (value) => String(value),
|
toLocal: (value) => String(value),
|
||||||
},
|
},
|
||||||
|
parseErrorMessage: (error) => error?.message ?? String(error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -148,6 +156,7 @@ vi.mock(
|
|||||||
import InvoicingBillingPeriodViewAll from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue";
|
import InvoicingBillingPeriodViewAll from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { invoiceQueue } from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue";
|
import { invoiceQueue } from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
import {
|
import {
|
||||||
periodPaging,
|
periodPaging,
|
||||||
resetPeriodPagingState,
|
resetPeriodPagingState,
|
||||||
@@ -222,11 +231,23 @@ describe("Invoicing period queue state", () => {
|
|||||||
loadingCustomerNumbersRef.value = [];
|
loadingCustomerNumbersRef.value = [];
|
||||||
SessionUser.objects.orders.get.multiple.mockReset();
|
SessionUser.objects.orders.get.multiple.mockReset();
|
||||||
SessionUser.objects.orders.get.multiple.mockResolvedValue([]);
|
SessionUser.objects.orders.get.multiple.mockResolvedValue([]);
|
||||||
|
SessionUser.objects.collectedOrderInvoices.functions.split_by_month.mockReset();
|
||||||
|
SessionUser.objects.collectedOrderInvoices.functions.split_by_month.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
processed_count: 1,
|
||||||
|
changed_count: 1,
|
||||||
|
skipped_count: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
invoiceQueue.addInvoiceCollectionsToQueue.mockReset();
|
invoiceQueue.addInvoiceCollectionsToQueue.mockReset();
|
||||||
invoiceQueue.processInvoiceCollectionQueue.mockReset();
|
invoiceQueue.processInvoiceCollectionQueue.mockReset();
|
||||||
invoiceQueue.markPeriodRefreshLoading.mockClear();
|
invoiceQueue.markPeriodRefreshLoading.mockClear();
|
||||||
invoiceQueue.finishPeriodRefresh.mockClear();
|
invoiceQueue.finishPeriodRefresh.mockClear();
|
||||||
invoiceQueue.isPeriodCustomerRefreshLoading.mockClear();
|
invoiceQueue.isPeriodCustomerRefreshLoading.mockClear();
|
||||||
|
Swal.fire.mockReset();
|
||||||
|
Swal.fire.mockResolvedValue({ isDenied: true });
|
||||||
resetPeriodPagingState();
|
resetPeriodPagingState();
|
||||||
sharedVariablesRef.value = {
|
sharedVariablesRef.value = {
|
||||||
types: {
|
types: {
|
||||||
@@ -644,6 +665,168 @@ describe("Invoicing period queue state", () => {
|
|||||||
expect(windowOpenSpy).not.toHaveBeenCalled();
|
expect(windowOpenSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("splits instead of queueing when multi-month invoicing warning is confirmed", async () => {
|
||||||
|
sharedVariablesRef.value = {
|
||||||
|
types: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
customer_number: 1008,
|
||||||
|
customer_name: "Multi Month Customer",
|
||||||
|
requires_action: true,
|
||||||
|
queue: {
|
||||||
|
has_active_job: false,
|
||||||
|
statuses: [],
|
||||||
|
invoice_collection_ids: [],
|
||||||
|
is_action_blocked: false,
|
||||||
|
},
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
id: 8101,
|
||||||
|
amount: 75,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
invoice_collection_id: 8100,
|
||||||
|
date: "2026-03-28T10:00:00.000Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8102,
|
||||||
|
amount: 125,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
invoice_collection_id: 8100,
|
||||||
|
date: "2026-04-02T10:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Swal.fire.mockResolvedValueOnce({ isConfirmed: true }).mockResolvedValueOnce({ isConfirmed: true });
|
||||||
|
|
||||||
|
const wrapper = mountView();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1008']").trigger("click");
|
||||||
|
await flushPromises();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).toHaveBeenCalledWith(
|
||||||
|
"2026-03-28",
|
||||||
|
"2026-04-02",
|
||||||
|
{
|
||||||
|
invoiceCollectionIds: [8100],
|
||||||
|
preview: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(invoiceQueue.addInvoiceCollectionsToQueue).not.toHaveBeenCalled();
|
||||||
|
expect(invoiceQueue.processInvoiceCollectionQueue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues queueing together when multi-month invoicing warning is denied", async () => {
|
||||||
|
sharedVariablesRef.value = {
|
||||||
|
types: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
customer_number: 1009,
|
||||||
|
customer_name: "Invoice Together Customer",
|
||||||
|
requires_action: true,
|
||||||
|
queue: {
|
||||||
|
has_active_job: false,
|
||||||
|
statuses: [],
|
||||||
|
invoice_collection_ids: [],
|
||||||
|
is_action_blocked: false,
|
||||||
|
},
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
id: 8201,
|
||||||
|
amount: 75,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
invoice_collection_id: 8200,
|
||||||
|
date: "2026-03-28T10:00:00.000Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8202,
|
||||||
|
amount: 125,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
invoice_collection_id: 8200,
|
||||||
|
date: "2026-04-02T10:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Swal.fire.mockResolvedValueOnce({ isDenied: true });
|
||||||
|
|
||||||
|
const wrapper = mountView();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1009']").trigger("click");
|
||||||
|
await flushPromises();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).not.toHaveBeenCalled();
|
||||||
|
expect(invoiceQueue.addInvoiceCollectionsToQueue).toHaveBeenCalledWith([8200], {
|
||||||
|
customerNumber: 1009,
|
||||||
|
});
|
||||||
|
expect(invoiceQueue.processInvoiceCollectionQueue).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not queue when multi-month invoicing warning is dismissed", async () => {
|
||||||
|
sharedVariablesRef.value = {
|
||||||
|
types: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
customer_number: 1010,
|
||||||
|
customer_name: "Cancel Multi Month Customer",
|
||||||
|
requires_action: true,
|
||||||
|
queue: {
|
||||||
|
has_active_job: false,
|
||||||
|
statuses: [],
|
||||||
|
invoice_collection_ids: [],
|
||||||
|
is_action_blocked: false,
|
||||||
|
},
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
id: 8301,
|
||||||
|
amount: 75,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
invoice_collection_id: 8300,
|
||||||
|
date: "2026-03-28T10:00:00.000Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8302,
|
||||||
|
amount: 125,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
invoice_collection_id: 8300,
|
||||||
|
date: "2026-04-02T10:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Swal.fire.mockResolvedValueOnce({ isDismissed: true });
|
||||||
|
|
||||||
|
const wrapper = mountView();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1010']").trigger("click");
|
||||||
|
await flushPromises();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).not.toHaveBeenCalled();
|
||||||
|
expect(invoiceQueue.addInvoiceCollectionsToQueue).not.toHaveBeenCalled();
|
||||||
|
expect(invoiceQueue.processInvoiceCollectionQueue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("applies invoice-now loading only to the affected customer", async () => {
|
it("applies invoice-now loading only to the affected customer", async () => {
|
||||||
sharedVariablesRef.value = {
|
sharedVariablesRef.value = {
|
||||||
types: {
|
types: {
|
||||||
|
|||||||
Reference in New Issue
Block a user