u
This commit is contained in:
15
API.md
15
API.md
@@ -401,6 +401,21 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `create(googleAuthId: number, input: {accountId: number, manifestId: number, type: string, amount: number}, tx?: object): Promise<{id: number, accountId: number, manifestId: number, type: string, amount: number}>`
|
- `create(googleAuthId: number, input: {accountId: number, manifestId: number, type: string, amount: number}, tx?: object): Promise<{id: number, accountId: number, manifestId: number, type: string, amount: number}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, accountId: number, manifestId: number, type: string, amount: number}>>`
|
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, accountId: number, manifestId: number, type: string, amount: number}>>`
|
||||||
|
- [retoure](API.retoure.md)
|
||||||
|
- `create(googleAuthId: number, body: {number?: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<object>}, tx?: object): Promise<object>`
|
||||||
|
- `createShipment(googleAuthId: number, retoureId: number, input: object, tx?: object): Promise<object>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{retoures: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `syncSummary(): void`
|
||||||
|
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<object>}, tx?: object): Promise<object>`
|
||||||
|
- [return](API.return.md)
|
||||||
|
- `book(provider: object): Function`
|
||||||
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<object>`
|
||||||
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnbooked?: boolean, supplierId?: number, seekMonths?: number}, tx?: object): Promise<{returns: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `syncSummary(): void`
|
||||||
- [subagentschedule](API.subagentschedule.md)
|
- [subagentschedule](API.subagentschedule.md)
|
||||||
- `create(googleAuthId: number, input: {prompt: string, cron: string, desc?: string, skills?: string|Array<string>, lastagentnote?: string, max_itr?: number}, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
|
- `create(googleAuthId: number, input: {prompt: string, cron: string, desc?: string, skills?: string|Array<string>, lastagentnote?: string, max_itr?: number}, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
|||||||
155
API.retoure.md
Normal file
155
API.retoure.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# retoure
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `retoure.create`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/create.js`*
|
||||||
|
|
||||||
|
Creates a new retoure for the user's selected company with its lines.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `body` | `object` | The retoure input. |
|
||||||
|
| `[body.number]` | `string` | The retoure number. |
|
||||||
|
| `body.date` | `string` | The retoure date. |
|
||||||
|
| `[body.entityId]` | `number` | The id of the customer entity. |
|
||||||
|
| `[body.externalNumber]` | `string` | An external reference number. |
|
||||||
|
| `[body.lines]` | `Array<object>` | The retoure lines. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The created retoure.
|
||||||
|
|
||||||
|
## `retoure.createShipment`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/createShipment.js`*
|
||||||
|
|
||||||
|
Creates a shipment manifest for a retoure.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `retoureId` | `number` | The parent retoure manifest id. |
|
||||||
|
| `input` | `object` | Shipment input. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The created shipment.
|
||||||
|
|
||||||
|
## `retoure.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/delete.js`*
|
||||||
|
|
||||||
|
Deletes a retoure by its id within the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the retoure to delete. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the retoure is not found.
|
||||||
|
|
||||||
|
## `retoure.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/getById.js`*
|
||||||
|
|
||||||
|
Returns a single retoure by its id within the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the retoure. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The retoure with its lines.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the retoure is not found.
|
||||||
|
|
||||||
|
## `retoure.list`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/list.js`*
|
||||||
|
|
||||||
|
Lists the retoures of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of retoures to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of retoures to return. |
|
||||||
|
| `[opts.search]` | `string` | Free-text search matching retoure number, external number, or customer name. |
|
||||||
|
| `[opts.onlyUnpaid=false]` | `boolean` | When true, returns only unpaid retoures. |
|
||||||
|
| `[opts.onlyUndelivered=false]` | `boolean` | When true, returns only undelivered retoures. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{retoures: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
|
||||||
|
## `retoure.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `retouresummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `retoure.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `retouresummary` table.
|
||||||
|
|
||||||
|
## `retoure.update`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/update.js`*
|
||||||
|
|
||||||
|
Updates a retoure's header and/or lines.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the retoure to update. |
|
||||||
|
| `patch` | `object` | The update patch. |
|
||||||
|
| `[patch.number]` | `string` | The new retoure number. |
|
||||||
|
| `[patch.date]` | `string` | The new retoure date. |
|
||||||
|
| `[patch.entityId]` | `number` | The new customer entity id. |
|
||||||
|
| `[patch.externalNumber]` | `string` | The new external reference number. |
|
||||||
|
| `[patch.lines]` | `Array<object>` | The new retoure lines. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The updated retoure.
|
||||||
83
API.return.md
Normal file
83
API.return.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# return
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `return.book`
|
||||||
|
|
||||||
|
*Source: `src/store/return/book.js`*
|
||||||
|
|
||||||
|
Books all positions of a return manifest to the ledger.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `object` | Storage provider instance. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Function` — `(googleAuthId, input, tx) => Promise<object>`
|
||||||
|
|
||||||
|
## `return.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/return/getById.js`*
|
||||||
|
|
||||||
|
Returns a return manifest by its id within the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the return manifest. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The return manifest with lines and summary.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the return is not found.
|
||||||
|
|
||||||
|
## `return.list`
|
||||||
|
|
||||||
|
*Source: `src/store/return/list.js`*
|
||||||
|
|
||||||
|
Lists the returns of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of returns to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of returns to return. |
|
||||||
|
| `[opts.search]` | `string` | Free-text search matching return number, external number, or supplier name. |
|
||||||
|
| `[opts.onlyUnbooked=false]` | `boolean` | When true, returns only returns with unbooked stock items. |
|
||||||
|
| `[opts.supplierId]` | `number` | Filter by supplier entity id. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{returns: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
|
||||||
|
## `return.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/return/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `returnsummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `return.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/return/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `returnsummary` table. Keeps `returnsummary` in sync with manifest (return), manifestarticle, article (track_stock), and ledger (RETURN transactions).
|
||||||
34
src/index.js
34
src/index.js
@@ -108,6 +108,20 @@ import orderCreateShipment from './store/order/createShipment.js';
|
|||||||
import orderUpdate from './store/order/update.js';
|
import orderUpdate from './store/order/update.js';
|
||||||
import orderDelete from './store/order/delete.js';
|
import orderDelete from './store/order/delete.js';
|
||||||
import orderRebuildSummaries from './store/order/rebuildSummaries.js';
|
import orderRebuildSummaries from './store/order/rebuildSummaries.js';
|
||||||
|
import returnList from './store/return/list.js';
|
||||||
|
import returnGetById from './store/return/getById.js';
|
||||||
|
import returnCreate from './store/return/create.js';
|
||||||
|
import returnUpdate from './store/return/update.js';
|
||||||
|
import returnDelete from './store/return/delete.js';
|
||||||
|
import returnBook from './store/return/book.js';
|
||||||
|
import returnRebuildSummaries from './store/return/rebuildSummaries.js';
|
||||||
|
import retoureList from './store/retoure/list.js';
|
||||||
|
import retoureGetById from './store/retoure/getById.js';
|
||||||
|
import retoureCreate from './store/retoure/create.js';
|
||||||
|
import retoureCreateShipment from './store/retoure/createShipment.js';
|
||||||
|
import retoureUpdate from './store/retoure/update.js';
|
||||||
|
import retoureDelete from './store/retoure/delete.js';
|
||||||
|
import retoureRebuildSummaries from './store/retoure/rebuildSummaries.js';
|
||||||
import posterminalList from './store/posterminal/list.js';
|
import posterminalList from './store/posterminal/list.js';
|
||||||
import posterminalUpsert from './store/posterminal/upsert.js';
|
import posterminalUpsert from './store/posterminal/upsert.js';
|
||||||
import posterminalDelete from './store/posterminal/delete.js';
|
import posterminalDelete from './store/posterminal/delete.js';
|
||||||
@@ -472,6 +486,26 @@ export default class LibDashStore {
|
|||||||
rebuildSummaries: orderRebuildSummaries(provider),
|
rebuildSummaries: orderRebuildSummaries(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.return = {
|
||||||
|
list: returnList(provider),
|
||||||
|
getById: returnGetById(provider),
|
||||||
|
create: returnCreate(provider),
|
||||||
|
update: returnUpdate(provider),
|
||||||
|
delete: returnDelete(provider),
|
||||||
|
book: returnBook(provider),
|
||||||
|
rebuildSummaries: returnRebuildSummaries(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.retoure = {
|
||||||
|
list: retoureList(provider),
|
||||||
|
getById: retoureGetById(provider),
|
||||||
|
create: retoureCreate(provider),
|
||||||
|
createShipment: retoureCreateShipment(provider),
|
||||||
|
update: retoureUpdate(provider),
|
||||||
|
delete: retoureDelete(provider),
|
||||||
|
rebuildSummaries: retoureRebuildSummaries(provider),
|
||||||
|
};
|
||||||
|
|
||||||
this.posterminal = {
|
this.posterminal = {
|
||||||
list: posterminalList(provider),
|
list: posterminalList(provider),
|
||||||
upsert: posterminalUpsert(provider),
|
upsert: posterminalUpsert(provider),
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
export const QUOTE_TYPE = 'quote';
|
export const QUOTE_TYPE = 'quote';
|
||||||
export const ORDER_TYPE = 'order';
|
export const ORDER_TYPE = 'order';
|
||||||
|
export const RETOURE_TYPE = 'retoure';
|
||||||
|
export const RETURN_TYPE = 'return';
|
||||||
|
|
||||||
export const LIST_MANIFESTS = `
|
export const LIST_MANIFESTS = `
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
25
src/providers/mssql/queries/retoure.js
Normal file
25
src/providers/mssql/queries/retoure.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export const RETOURE_TYPE = 'retoure';
|
||||||
|
|
||||||
|
export const GET_RETOURE_HEADER = `
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.[number],
|
||||||
|
m.[type],
|
||||||
|
m.entity_id AS entityId,
|
||||||
|
m.external_number AS externalNumber,
|
||||||
|
m.[date],
|
||||||
|
m.created_at AS createdAt,
|
||||||
|
rs.positions,
|
||||||
|
rs.gross_total AS grossTotal,
|
||||||
|
rs.paid_total AS paidTotal,
|
||||||
|
rs.ordered_quantity AS orderedQuantity,
|
||||||
|
rs.shipped_quantity AS shippedQuantity,
|
||||||
|
rs.is_paid AS isPaid,
|
||||||
|
rs.is_shipped AS isShipped,
|
||||||
|
rs.customer_name AS customerName
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN retouresummary rs ON rs.manifest_id = m.id
|
||||||
|
WHERE m.id = @id
|
||||||
|
AND m.company_id = @companyId
|
||||||
|
AND m.[type] = @type
|
||||||
|
`;
|
||||||
79
src/providers/mssql/queries/return.js
Normal file
79
src/providers/mssql/queries/return.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export const RETURN_TYPE = 'return';
|
||||||
|
|
||||||
|
export const GET_RETURN_HEADER = `
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.[number],
|
||||||
|
m.[type],
|
||||||
|
m.entity_id AS supplierId,
|
||||||
|
m.external_number AS externalNumber,
|
||||||
|
m.[date],
|
||||||
|
m.created_at AS createdAt,
|
||||||
|
rs.positions,
|
||||||
|
rs.gross_total AS grossTotal,
|
||||||
|
rs.net_total AS netTotal,
|
||||||
|
rs.total_quantity AS totalQuantity,
|
||||||
|
rs.stock_positions AS stockPositions,
|
||||||
|
rs.booked_positions AS bookedPositions,
|
||||||
|
rs.unbooked_positions AS unbookedPositions,
|
||||||
|
rs.stock_quantity AS stockQuantity,
|
||||||
|
rs.booked_quantity AS bookedQuantity,
|
||||||
|
rs.unbooked_quantity AS unbookedQuantity,
|
||||||
|
rs.is_booked AS isBooked,
|
||||||
|
rs.supplier_name AS supplierName
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN returnsummary rs ON rs.manifest_id = m.id
|
||||||
|
WHERE m.id = @id
|
||||||
|
AND m.company_id = @companyId
|
||||||
|
AND m.[type] = @type
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_RETURN_LINES = `
|
||||||
|
SELECT
|
||||||
|
ma.id,
|
||||||
|
ma.sku,
|
||||||
|
ma.name,
|
||||||
|
ma.pu,
|
||||||
|
ma.amount,
|
||||||
|
ma.net,
|
||||||
|
ma.rebate,
|
||||||
|
ma.taxrate,
|
||||||
|
ma.comment
|
||||||
|
FROM manifestarticle ma
|
||||||
|
WHERE ma.manifest_id = @manifestId
|
||||||
|
ORDER BY ma.id
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LIST_RETURNS = `
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.[number],
|
||||||
|
m.[type],
|
||||||
|
m.entity_id AS supplierId,
|
||||||
|
m.external_number AS externalNumber,
|
||||||
|
m.[date],
|
||||||
|
m.created_at AS createdAt,
|
||||||
|
COUNT(ma.id) AS totalPositions,
|
||||||
|
COUNT(CASE WHEN ma.id IS NOT NULL AND (
|
||||||
|
(ma.amount > 0 AND COALESCE(ma_ledger.booked_amount, 0) >= ma.amount) OR
|
||||||
|
(ma.amount <= 0 AND COALESCE(ma_ledger.booked_amount, 0) > 0)
|
||||||
|
) THEN 1 END) AS completelyOnLedgerPositions
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN manifestarticle ma ON ma.manifest_id = m.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT manifestarticle_id, SUM(amount) AS booked_amount
|
||||||
|
FROM ledger
|
||||||
|
GROUP BY manifestarticle_id
|
||||||
|
) ma_ledger ON ma_ledger.manifestarticle_id = ma.id
|
||||||
|
WHERE m.company_id = @companyId
|
||||||
|
AND m.[type] = @type
|
||||||
|
GROUP BY
|
||||||
|
m.id,
|
||||||
|
m.[number],
|
||||||
|
m.[type],
|
||||||
|
m.entity_id,
|
||||||
|
m.external_number,
|
||||||
|
m.[date],
|
||||||
|
m.created_at
|
||||||
|
ORDER BY m.[date] DESC, m.id DESC
|
||||||
|
`;
|
||||||
@@ -3,7 +3,7 @@ export const SHIPMENT_TYPE = 'shipment';
|
|||||||
export const GET_ORDER_HEADER = `
|
export const GET_ORDER_HEADER = `
|
||||||
SELECT id, company_id AS companyId, [number], [type], entity_id AS entityId, external_number AS externalNumber, [date]
|
SELECT id, company_id AS companyId, [number], [type], entity_id AS entityId, external_number AS externalNumber, [date]
|
||||||
FROM manifest
|
FROM manifest
|
||||||
WHERE id = @orderId AND company_id = @companyId AND [type] = 'order';
|
WHERE id = @orderId AND company_id = @companyId AND [type] IN ('order', 'retoure');
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GET_ORDER_LINES = `
|
export const GET_ORDER_LINES = `
|
||||||
|
|||||||
@@ -1398,6 +1398,87 @@ BEGIN
|
|||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const INIT_RETURNSUMMARY = `
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'returnsummary')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE returnsummary (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
company_id INT NOT NULL,
|
||||||
|
manifest_id INT NOT NULL CONSTRAINT UQ_returnsummary_manifest UNIQUE,
|
||||||
|
return_number NVARCHAR(100) NOT NULL,
|
||||||
|
external_number NVARCHAR(100) NULL,
|
||||||
|
return_date DATETIME2 NOT NULL,
|
||||||
|
created_at DATETIME2 NOT NULL CONSTRAINT DF_returnsummary_created_at DEFAULT SYSUTCDATETIME(),
|
||||||
|
supplier_id INT NULL,
|
||||||
|
supplier_name NVARCHAR(255) NULL,
|
||||||
|
positions INT NOT NULL CONSTRAINT DF_returnsummary_positions DEFAULT 0,
|
||||||
|
gross_total FLOAT NOT NULL CONSTRAINT DF_returnsummary_gross_total DEFAULT 0,
|
||||||
|
net_total FLOAT NOT NULL CONSTRAINT DF_returnsummary_net_total DEFAULT 0,
|
||||||
|
total_quantity FLOAT NOT NULL CONSTRAINT DF_returnsummary_total_quantity DEFAULT 0,
|
||||||
|
stock_positions INT NOT NULL CONSTRAINT DF_returnsummary_stock_positions DEFAULT 0,
|
||||||
|
booked_positions INT NOT NULL CONSTRAINT DF_returnsummary_booked_positions DEFAULT 0,
|
||||||
|
unbooked_positions INT NOT NULL CONSTRAINT DF_returnsummary_unbooked_positions DEFAULT 0,
|
||||||
|
stock_quantity FLOAT NOT NULL CONSTRAINT DF_returnsummary_stock_quantity DEFAULT 0,
|
||||||
|
booked_quantity FLOAT NOT NULL CONSTRAINT DF_returnsummary_booked_quantity DEFAULT 0,
|
||||||
|
unbooked_quantity FLOAT NOT NULL CONSTRAINT DF_returnsummary_unbooked_quantity DEFAULT 0,
|
||||||
|
is_booked TINYINT NOT NULL CONSTRAINT DF_returnsummary_is_booked DEFAULT 0,
|
||||||
|
CONSTRAINT FK_returnsummary_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT FK_returnsummary_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT FK_returnsummary_supplier FOREIGN KEY (supplier_id) REFERENCES entity(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_returnsummary_company_date
|
||||||
|
ON returnsummary (company_id, return_date DESC, manifest_id DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IX_returnsummary_company_number
|
||||||
|
ON returnsummary (company_id, return_number);
|
||||||
|
|
||||||
|
CREATE INDEX IX_returnsummary_company_booked
|
||||||
|
ON returnsummary (company_id, is_booked);
|
||||||
|
|
||||||
|
CREATE INDEX IX_returnsummary_company_supplier
|
||||||
|
ON returnsummary (company_id, supplier_id);
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const INIT_RETOURESUMMARY = `
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'retouresummary')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE retouresummary (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
company_id INT NOT NULL,
|
||||||
|
manifest_id INT NOT NULL CONSTRAINT UQ_retouresummary_manifest UNIQUE,
|
||||||
|
retoure_number NVARCHAR(100) NOT NULL,
|
||||||
|
retoure_date DATETIME2 NOT NULL,
|
||||||
|
created_at DATETIME2 NOT NULL CONSTRAINT DF_retouresummary_created_at DEFAULT SYSUTCDATETIME(),
|
||||||
|
entity_id INT NULL,
|
||||||
|
customer_name NVARCHAR(255) NULL,
|
||||||
|
positions INT NOT NULL CONSTRAINT DF_retouresummary_positions DEFAULT 0,
|
||||||
|
gross_total FLOAT NOT NULL CONSTRAINT DF_retouresummary_gross_total DEFAULT 0,
|
||||||
|
paid_total FLOAT NOT NULL CONSTRAINT DF_retouresummary_paid_total DEFAULT 0,
|
||||||
|
ordered_quantity FLOAT NOT NULL CONSTRAINT DF_retouresummary_ordered_quantity DEFAULT 0,
|
||||||
|
shipped_quantity FLOAT NOT NULL CONSTRAINT DF_retouresummary_shipped_quantity DEFAULT 0,
|
||||||
|
is_paid TINYINT NOT NULL CONSTRAINT DF_retouresummary_is_paid DEFAULT 0,
|
||||||
|
is_shipped TINYINT NOT NULL CONSTRAINT DF_retouresummary_is_shipped DEFAULT 0,
|
||||||
|
CONSTRAINT FK_retouresummary_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
||||||
|
CONSTRAINT FK_retouresummary_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT FK_retouresummary_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_retouresummary_company_date
|
||||||
|
ON retouresummary (company_id, retoure_date DESC, manifest_id DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IX_retouresummary_company_shipped
|
||||||
|
ON retouresummary (company_id, is_shipped);
|
||||||
|
|
||||||
|
CREATE INDEX IX_retouresummary_company_paid
|
||||||
|
ON retouresummary (company_id, is_paid);
|
||||||
|
|
||||||
|
CREATE INDEX IX_retouresummary_company_number
|
||||||
|
ON retouresummary (company_id, retoure_number);
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLEWAREHOUSESUMMARY = `
|
export const INIT_ARTICLEWAREHOUSESUMMARY = `
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlewarehousesummary')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlewarehousesummary')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -1575,6 +1656,8 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_QUOTESUMMARY,
|
INIT_QUOTESUMMARY,
|
||||||
INIT_SHIPMENTSUMMARY,
|
INIT_SHIPMENTSUMMARY,
|
||||||
INIT_DELIVERYSUMMARY,
|
INIT_DELIVERYSUMMARY,
|
||||||
|
INIT_RETURNSUMMARY,
|
||||||
|
INIT_RETOURESUMMARY,
|
||||||
INIT_ARTICLEWAREHOUSESUMMARY,
|
INIT_ARTICLEWAREHOUSESUMMARY,
|
||||||
INIT_MCP_CLIENT,
|
INIT_MCP_CLIENT,
|
||||||
INIT_MCP_AUTH_CODE,
|
INIT_MCP_AUTH_CODE,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const queries = translateQueries(source, 'manifestDoc');
|
|||||||
export const {
|
export const {
|
||||||
CHECK_ARTICLE_IS_PARENT, COUNT_MANIFESTS, CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST,
|
CHECK_ARTICLE_IS_PARENT, COUNT_MANIFESTS, CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST,
|
||||||
DELETE_MANIFEST, GET_ARTICLE_SUPPLIER_LINK, GET_MANIFEST_CONTEXT, GET_MANIFEST_HEADER,
|
DELETE_MANIFEST, GET_ARTICLE_SUPPLIER_LINK, GET_MANIFEST_CONTEXT, GET_MANIFEST_HEADER,
|
||||||
GET_MANIFEST_LINES, LIST_MANIFESTS, ORDER_TYPE, QUOTE_TYPE, UPDATE_ARTICLE_SUPPLIER_LINK, UPDATE_MANIFEST,
|
GET_MANIFEST_LINES, LIST_MANIFESTS, ORDER_TYPE, QUOTE_TYPE, RETOURE_TYPE, RETURN_TYPE, UPDATE_ARTICLE_SUPPLIER_LINK, UPDATE_MANIFEST,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
export const LIST_MANIFESTS_PAGED = `
|
export const LIST_MANIFESTS_PAGED = `
|
||||||
|
|||||||
8
src/providers/sqlite3/queries/retoure.js
Normal file
8
src/providers/sqlite3/queries/retoure.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as source from '../../mssql/queries/retoure.js';
|
||||||
|
import { translateQueries } from './translate.js';
|
||||||
|
|
||||||
|
const queries = translateQueries(source, 'retoure');
|
||||||
|
export const {
|
||||||
|
GET_RETOURE_HEADER,
|
||||||
|
RETOURE_TYPE,
|
||||||
|
} = queries;
|
||||||
10
src/providers/sqlite3/queries/return.js
Normal file
10
src/providers/sqlite3/queries/return.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import * as source from '../../mssql/queries/return.js';
|
||||||
|
import { translateQueries } from './translate.js';
|
||||||
|
|
||||||
|
const queries = translateQueries(source, 'return');
|
||||||
|
export const {
|
||||||
|
GET_RETURN_HEADER,
|
||||||
|
GET_RETURN_LINES,
|
||||||
|
LIST_RETURNS,
|
||||||
|
RETURN_TYPE,
|
||||||
|
} = queries;
|
||||||
@@ -1000,6 +1000,73 @@ CREATE INDEX IF NOT EXISTS IX_deliverysummary_company_supplier
|
|||||||
ON deliverysummary (company_id, supplier_id);
|
ON deliverysummary (company_id, supplier_id);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const INIT_RETURNSUMMARY = `
|
||||||
|
CREATE TABLE IF NOT EXISTS returnsummary (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
company_id INTEGER NOT NULL,
|
||||||
|
manifest_id INTEGER NOT NULL UNIQUE,
|
||||||
|
return_number TEXT NOT NULL,
|
||||||
|
external_number TEXT,
|
||||||
|
return_date TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
|
supplier_id INTEGER NULL,
|
||||||
|
supplier_name TEXT NULL,
|
||||||
|
positions INTEGER NOT NULL DEFAULT 0,
|
||||||
|
gross_total REAL NOT NULL DEFAULT 0,
|
||||||
|
net_total REAL NOT NULL DEFAULT 0,
|
||||||
|
total_quantity REAL NOT NULL DEFAULT 0,
|
||||||
|
stock_positions INTEGER NOT NULL DEFAULT 0,
|
||||||
|
booked_positions INTEGER NOT NULL DEFAULT 0,
|
||||||
|
unbooked_positions INTEGER NOT NULL DEFAULT 0,
|
||||||
|
stock_quantity REAL NOT NULL DEFAULT 0,
|
||||||
|
booked_quantity REAL NOT NULL DEFAULT 0,
|
||||||
|
unbooked_quantity REAL NOT NULL DEFAULT 0,
|
||||||
|
is_booked INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
||||||
|
FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (supplier_id) REFERENCES entity(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_returnsummary_company_date
|
||||||
|
ON returnsummary (company_id, return_date DESC, manifest_id DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_returnsummary_company_number
|
||||||
|
ON returnsummary (company_id, return_number);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_returnsummary_company_booked
|
||||||
|
ON returnsummary (company_id, is_booked);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_returnsummary_company_supplier
|
||||||
|
ON returnsummary (company_id, supplier_id);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const INIT_RETOURESUMMARY = `
|
||||||
|
CREATE TABLE IF NOT EXISTS retouresummary (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
company_id INTEGER NOT NULL,
|
||||||
|
manifest_id INTEGER NOT NULL UNIQUE,
|
||||||
|
retoure_number TEXT NOT NULL,
|
||||||
|
retoure_date TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
|
entity_id INTEGER NULL,
|
||||||
|
customer_name TEXT NULL,
|
||||||
|
positions INTEGER NOT NULL DEFAULT 0,
|
||||||
|
gross_total REAL NOT NULL DEFAULT 0,
|
||||||
|
paid_total REAL NOT NULL DEFAULT 0,
|
||||||
|
ordered_quantity REAL NOT NULL DEFAULT 0,
|
||||||
|
shipped_quantity REAL NOT NULL DEFAULT 0,
|
||||||
|
is_paid INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_shipped INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
||||||
|
FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_retouresummary_company_date
|
||||||
|
ON retouresummary (company_id, retoure_date DESC, manifest_id DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_retouresummary_company_shipped
|
||||||
|
ON retouresummary (company_id, is_shipped);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_retouresummary_company_paid
|
||||||
|
ON retouresummary (company_id, is_paid);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_retouresummary_company_number
|
||||||
|
ON retouresummary (company_id, retoure_number);
|
||||||
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLEWAREHOUSESUMMARY = `
|
export const INIT_ARTICLEWAREHOUSESUMMARY = `
|
||||||
CREATE TABLE IF NOT EXISTS articlewarehousesummary (
|
CREATE TABLE IF NOT EXISTS articlewarehousesummary (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -1153,6 +1220,8 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_QUOTESUMMARY,
|
INIT_QUOTESUMMARY,
|
||||||
INIT_SHIPMENTSUMMARY,
|
INIT_SHIPMENTSUMMARY,
|
||||||
INIT_DELIVERYSUMMARY,
|
INIT_DELIVERYSUMMARY,
|
||||||
|
INIT_RETURNSUMMARY,
|
||||||
|
INIT_RETOURESUMMARY,
|
||||||
INIT_ARTICLEWAREHOUSESUMMARY,
|
INIT_ARTICLEWAREHOUSESUMMARY,
|
||||||
INIT_MCP_CLIENT,
|
INIT_MCP_CLIENT,
|
||||||
INIT_MCP_AUTH_CODE,
|
INIT_MCP_AUTH_CODE,
|
||||||
|
|||||||
@@ -67,13 +67,17 @@ export async function ensureNumberType(runner, companyId, type) {
|
|||||||
}
|
}
|
||||||
} else if (type === 'adjustment') {
|
} else if (type === 'adjustment') {
|
||||||
prefix = prefixStyle.endsWith('-') ? 'KORR-' : '';
|
prefix = prefixStyle.endsWith('-') ? 'KORR-' : '';
|
||||||
|
} else if (type === 'return') {
|
||||||
|
prefix = prefixStyle.endsWith('-') ? 'RTN-' : 'RTN';
|
||||||
|
} else if (type === 'retoure') {
|
||||||
|
prefix = prefixStyle.endsWith('-') ? 'RET-' : 'RET';
|
||||||
}
|
}
|
||||||
|
|
||||||
const padding = (type === 'order' || type === 'quote') ? (pad || 6) : (type === 'adjustment' ? 5 : (refSeq?.padding ?? 0));
|
const padding = (type === 'order' || type === 'quote' || type === 'return' || type === 'retoure') ? (pad || 6) : (type === 'adjustment' ? 5 : (refSeq?.padding ?? 0));
|
||||||
|
|
||||||
// Determine next counter by scanning manifest for existing documents
|
// Determine next counter by scanning manifest for existing documents
|
||||||
let next = 1;
|
let next = 1;
|
||||||
if (type === 'order' || type === 'quote') {
|
if (type === 'order' || type === 'quote' || type === 'return' || type === 'retoure') {
|
||||||
const manifestSql = isSqlite
|
const manifestSql = isSqlite
|
||||||
? 'SELECT "number" FROM manifest WHERE company_id = @companyId AND "type" = @type'
|
? 'SELECT "number" FROM manifest WHERE company_id = @companyId AND "type" = @type'
|
||||||
: 'SELECT [number] FROM manifest WHERE company_id = @companyId AND [type] = @type';
|
: 'SELECT [number] FROM manifest WHERE company_id = @companyId AND [type] = @type';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { queriesFor, requireId, requireText } from '../logistics/helpers.js';
|
|||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveManifestAccess from '../manifestattachment/resolveManifestAccess.js';
|
import resolveManifestAccess from '../manifestattachment/resolveManifestAccess.js';
|
||||||
import { syncOrderSummary } from '../order/syncSummary.js';
|
import { syncOrderSummary } from '../order/syncSummary.js';
|
||||||
|
import { syncRetoureSummary } from '../retoure/syncSummary.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function shape(row) {
|
function shape(row) {
|
||||||
@@ -67,7 +68,17 @@ export default function create(provider) {
|
|||||||
type,
|
type,
|
||||||
amount: input.amount,
|
amount: input.amount,
|
||||||
};
|
};
|
||||||
|
const mTypeRes = await runner.query(
|
||||||
|
runner.dialect === 'sqlite3'
|
||||||
|
? 'SELECT "type" FROM manifest WHERE id = @manifestId'
|
||||||
|
: 'SELECT [type] FROM manifest WHERE id = @manifestId',
|
||||||
|
{ manifestId },
|
||||||
|
);
|
||||||
|
if (mTypeRes.rows[0]?.type === 'retoure') {
|
||||||
|
await syncRetoureSummary(runner, { companyId, manifestId });
|
||||||
|
} else {
|
||||||
await syncOrderSummary(runner, { companyId, manifestId });
|
await syncOrderSummary(runner, { companyId, manifestId });
|
||||||
|
}
|
||||||
return shape(row);
|
return shape(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
129
src/store/retoure/create.js
Normal file
129
src/store/retoure/create.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
CREATE_MANIFEST as MSSQL_CREATE_MANIFEST,
|
||||||
|
RETOURE_TYPE as MSSQL_RETOURE_TYPE,
|
||||||
|
} from '../../providers/mssql/queries/manifestDoc.js';
|
||||||
|
import {
|
||||||
|
CREATE_MANIFEST as SQLITE_CREATE_MANIFEST,
|
||||||
|
RETOURE_TYPE as SQLITE_RETOURE_TYPE,
|
||||||
|
} from '../../providers/sqlite3/queries/manifestDoc.js';
|
||||||
|
import {
|
||||||
|
FIND_ARTICLES_FOR_HISTORY as MSSQL_FIND_ARTICLES,
|
||||||
|
} from '../../providers/mssql/queries/ledger.js';
|
||||||
|
import {
|
||||||
|
FIND_ARTICLES_FOR_HISTORY as SQLITE_FIND_ARTICLES,
|
||||||
|
} from '../../providers/sqlite3/queries/ledger.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import taxEnsure from '../tax/ensure.js';
|
||||||
|
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||||
|
import { assertCustomerEntity, assertUniqueManifestNumber, loadDoc } from '../manifestDoc/helpers.js';
|
||||||
|
import { syncRetoureSummary } from './syncSummary.js';
|
||||||
|
import { ensureNumberType } from '../numbers/ensureType.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function create(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const ensureTax = taxEnsure(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const createManifestSql = isSqlite ? SQLITE_CREATE_MANIFEST : MSSQL_CREATE_MANIFEST;
|
||||||
|
const retoureType = isSqlite ? SQLITE_RETOURE_TYPE : MSSQL_RETOURE_TYPE;
|
||||||
|
const findArticlesSql = isSqlite ? SQLITE_FIND_ARTICLES : MSSQL_FIND_ARTICLES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new retoure for the user's selected company with its lines.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} body - The retoure input.
|
||||||
|
* @param {string} [body.number] - The retoure number.
|
||||||
|
* @param {string} body.date - The retoure date.
|
||||||
|
* @param {number} [body.entityId] - The id of the customer entity.
|
||||||
|
* @param {string} [body.externalNumber] - An external reference number.
|
||||||
|
* @param {Array<object>} [body.lines] - The retoure lines.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
|
* @returns {Promise<object>} The created retoure.
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, body = {}, tx) => {
|
||||||
|
const date = parseDate(body.date);
|
||||||
|
if (!date) {
|
||||||
|
throw new Error('Invalid retoure');
|
||||||
|
}
|
||||||
|
const createdAt = body.createdAt ? parseDate(body.createdAt) : new Date();
|
||||||
|
if (body.createdAt && !createdAt) {
|
||||||
|
throw new Error('Invalid retoure');
|
||||||
|
}
|
||||||
|
const externalNumber = body.externalNumber?.trim?.()
|
||||||
|
? body.externalNumber.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const lines = normalizeLines(body.lines ?? []);
|
||||||
|
|
||||||
|
const runCreate = async (runner) => {
|
||||||
|
const { companyId, companyuserId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
|
let entityId = null;
|
||||||
|
if (body.entityId != null) {
|
||||||
|
entityId = await assertCustomerEntity(
|
||||||
|
runner,
|
||||||
|
companyId,
|
||||||
|
body.entityId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let number = body.number?.trim?.() ? body.number.trim() : null;
|
||||||
|
if (!number) {
|
||||||
|
const seq = await ensureNumberType(runner, companyId, 'retoure');
|
||||||
|
const numVal = seq.next ?? 1;
|
||||||
|
number = (seq.prefix || 'RET-') + (seq.padding > 0 ? String(numVal).padStart(seq.padding, '0') : String(numVal));
|
||||||
|
await runner.query(
|
||||||
|
isSqlite
|
||||||
|
? 'UPDATE numbers SET "next" = "next" + 1 WHERE id = @id'
|
||||||
|
: 'UPDATE numbers SET [next] = [next] + 1 WHERE id = @id',
|
||||||
|
{ id: seq.id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertUniqueManifestNumber(runner, {
|
||||||
|
companyId,
|
||||||
|
type: retoureType,
|
||||||
|
number,
|
||||||
|
errorMessage: 'Retoure number already exists',
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await runner.query(createManifestSql, {
|
||||||
|
companyId,
|
||||||
|
number,
|
||||||
|
type: retoureType,
|
||||||
|
entityId,
|
||||||
|
externalNumber,
|
||||||
|
date,
|
||||||
|
createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestId = created.rows[0]?.id ?? created.insertId;
|
||||||
|
|
||||||
|
await insertLines(runner, {
|
||||||
|
manifestId,
|
||||||
|
lines,
|
||||||
|
ensureTax,
|
||||||
|
});
|
||||||
|
|
||||||
|
const doc = await loadDoc(runner, {
|
||||||
|
type: retoureType,
|
||||||
|
companyId,
|
||||||
|
id: manifestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncRetoureSummary(runner, { companyId, manifestId });
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
return runCreate(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runCreate);
|
||||||
|
}
|
||||||
|
return runCreate(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
18
src/store/retoure/createShipment.js
Normal file
18
src/store/retoure/createShipment.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import shipmentCreate from '../shipment/create.js';
|
||||||
|
|
||||||
|
export default function createShipment(provider) {
|
||||||
|
const create = shipmentCreate(provider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a shipment manifest for a retoure.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} retoureId - The parent retoure manifest id.
|
||||||
|
* @param {object} input - Shipment input.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<object>} The created shipment.
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, retoureId, input = {}, tx) => {
|
||||||
|
return create(googleAuthId, { ...input, orderId: retoureId }, tx);
|
||||||
|
};
|
||||||
|
}
|
||||||
75
src/store/retoure/delete.js
Normal file
75
src/store/retoure/delete.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
DELETE_MANIFEST as MSSQL_DELETE,
|
||||||
|
GET_MANIFEST_HEADER as MSSQL_GET_HEADER,
|
||||||
|
RETOURE_TYPE as MSSQL_RETOURE_TYPE,
|
||||||
|
} from '../../providers/mssql/queries/manifestDoc.js';
|
||||||
|
import {
|
||||||
|
DELETE_MANIFEST as SQLITE_DELETE,
|
||||||
|
GET_MANIFEST_HEADER as SQLITE_GET_HEADER,
|
||||||
|
RETOURE_TYPE as SQLITE_RETOURE_TYPE,
|
||||||
|
} from '../../providers/sqlite3/queries/manifestDoc.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function del(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
const getHeaderSql = isSqlite ? SQLITE_GET_HEADER : MSSQL_GET_HEADER;
|
||||||
|
const retoureType = isSqlite ? SQLITE_RETOURE_TYPE : MSSQL_RETOURE_TYPE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a retoure by its id within the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} id - The id of the retoure to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If the retoure is not found.
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, id, tx) => {
|
||||||
|
if (!Number.isFinite(id)) {
|
||||||
|
throw new Error('Retoure not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = async (runner) => {
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
|
const headerRes = await runner.query(getHeaderSql, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: retoureType,
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = headerRes.rows[0];
|
||||||
|
if (!header) {
|
||||||
|
throw new Error('Retoure not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await runner.query(
|
||||||
|
isSqlite
|
||||||
|
? 'DELETE FROM retouresummary WHERE manifest_id = @id'
|
||||||
|
: 'DELETE FROM retouresummary WHERE manifest_id = @id',
|
||||||
|
{ id },
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await runner.query(deleteSql, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: retoureType,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.rowsAffected) {
|
||||||
|
throw new Error('Retoure not found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
return run(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(run);
|
||||||
|
}
|
||||||
|
return run(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
35
src/store/retoure/getById.js
Normal file
35
src/store/retoure/getById.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { RETOURE_TYPE as MSSQL_RETOURE_TYPE } from '../../providers/mssql/queries/manifestDoc.js';
|
||||||
|
import { RETOURE_TYPE as SQLITE_RETOURE_TYPE } from '../../providers/sqlite3/queries/manifestDoc.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import { loadDoc } from '../manifestDoc/helpers.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function getById(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const retoureType = isSqlite ? SQLITE_RETOURE_TYPE : MSSQL_RETOURE_TYPE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a single retoure by its id within the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} id - The id of the retoure.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The retoure with its lines.
|
||||||
|
* @throws {Error} If the retoure is not found.
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, id, tx) => {
|
||||||
|
if (!Number.isFinite(id)) {
|
||||||
|
throw new Error('Retoure not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
|
const doc = await loadDoc(runner, { type: retoureType, companyId, id });
|
||||||
|
if (!doc) {
|
||||||
|
throw new Error('Retoure not found');
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
};
|
||||||
|
}
|
||||||
274
src/store/retoure/list.js
Normal file
274
src/store/retoure/list.js
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import {
|
||||||
|
COUNT_MANIFESTS as MSSQL_COUNT_MANIFESTS,
|
||||||
|
GET_MANIFEST_LINES_BY_MANIFEST_IDS as MSSQL_LINES_BY_MANIFEST_IDS,
|
||||||
|
RETOURE_TYPE as MSSQL_RETOURE_TYPE,
|
||||||
|
} from '../../providers/mssql/queries/manifestDoc.js';
|
||||||
|
import {
|
||||||
|
COUNT_MANIFESTS as SQLITE_COUNT_MANIFESTS,
|
||||||
|
GET_MANIFEST_LINES_BY_MANIFEST_IDS as SQLITE_LINES_BY_MANIFEST_IDS,
|
||||||
|
RETOURE_TYPE as SQLITE_RETOURE_TYPE,
|
||||||
|
} from '../../providers/sqlite3/queries/manifestDoc.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import { shapeSummary } from '../manifestDoc/helpers.js';
|
||||||
|
import { shapeLine } from '../manifestLines.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function list(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const sqlLinesByManifestIds = isSqlite
|
||||||
|
? SQLITE_LINES_BY_MANIFEST_IDS
|
||||||
|
: MSSQL_LINES_BY_MANIFEST_IDS;
|
||||||
|
const retoureType = isSqlite ? SQLITE_RETOURE_TYPE : MSSQL_RETOURE_TYPE;
|
||||||
|
const sqlCountManifests = isSqlite
|
||||||
|
? SQLITE_COUNT_MANIFESTS
|
||||||
|
: MSSQL_COUNT_MANIFESTS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the retoures of the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [opts] - Optional pagination and filter options.
|
||||||
|
* @param {number} [opts.offset=0] - Number of retoures to skip.
|
||||||
|
* @param {number} [opts.limit=50] - Number of retoures to return.
|
||||||
|
* @param {string} [opts.search] - Free-text search matching retoure number, external number, or customer name.
|
||||||
|
* @param {boolean} [opts.onlyUnpaid=false] - When true, returns only unpaid retoures.
|
||||||
|
* @param {boolean} [opts.onlyUndelivered=false] - When true, returns only undelivered retoures.
|
||||||
|
* @param {number} [opts.seekMonths] - Negative integer for cutoff months ago (e.g. -1).
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<{retoures: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>}
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, opts, tx) => {
|
||||||
|
let offset = null;
|
||||||
|
let limit = null;
|
||||||
|
let isPaged = false;
|
||||||
|
let resolvedTx = tx;
|
||||||
|
|
||||||
|
const hasFilter = opts != null && typeof opts === 'object' && !opts._isTx && (
|
||||||
|
opts.offset !== undefined ||
|
||||||
|
opts.limit !== undefined ||
|
||||||
|
(typeof opts.search === 'string' && opts.search.trim() !== '') ||
|
||||||
|
opts.onlyUnpaid === true ||
|
||||||
|
opts.onlyUndelivered === true ||
|
||||||
|
opts.entityId !== undefined ||
|
||||||
|
opts.seekMonths !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (opts != null && typeof opts === 'object' && !opts._isTx) {
|
||||||
|
if (hasFilter || opts.offset !== undefined || opts.limit !== undefined) {
|
||||||
|
isPaged = true;
|
||||||
|
const parsedOffset = Number(opts.offset ?? 0);
|
||||||
|
const parsedLimit = Number(opts.limit ?? 50);
|
||||||
|
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
|
||||||
|
throw new Error('Invalid offset');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
|
||||||
|
throw new Error('Invalid limit');
|
||||||
|
}
|
||||||
|
offset = parsedOffset;
|
||||||
|
limit = parsedLimit;
|
||||||
|
}
|
||||||
|
} else if (opts != null) {
|
||||||
|
resolvedTx = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = makeRunner(provider, resolvedTx);
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
|
const conditions = ['m.company_id = @companyId', `m.${isSqlite ? '"type"' : '[type]'} = @type`];
|
||||||
|
const params = {
|
||||||
|
companyId,
|
||||||
|
type: retoureType,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts?.entityId !== undefined && opts.entityId !== null) {
|
||||||
|
const entityId = Number(opts.entityId);
|
||||||
|
if (!Number.isInteger(entityId) || entityId <= 0) {
|
||||||
|
throw new Error('Invalid entityId');
|
||||||
|
}
|
||||||
|
params.entityId = entityId;
|
||||||
|
conditions.push('m.entity_id = @entityId');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.seekMonths !== undefined) {
|
||||||
|
if (typeof opts.seekMonths !== 'number' || !Number.isInteger(opts.seekMonths)) {
|
||||||
|
throw new Error('Invalid seekMonths');
|
||||||
|
}
|
||||||
|
const d = new Date();
|
||||||
|
d.setMonth(d.getMonth() - Math.abs(opts.seekMonths));
|
||||||
|
params.dateTo = d.toISOString();
|
||||||
|
conditions.push(`m.${isSqlite ? '"date"' : '[date]'} <= @dateTo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof opts?.search === 'string' && opts.search.trim() !== '') {
|
||||||
|
params.searchPattern = `%${opts.search.trim()}%`;
|
||||||
|
if (isSqlite) {
|
||||||
|
conditions.push(`(
|
||||||
|
m."number" LIKE @searchPattern
|
||||||
|
OR m.external_number LIKE @searchPattern
|
||||||
|
OR e.name LIKE @searchPattern
|
||||||
|
OR e.first_name LIKE @searchPattern
|
||||||
|
OR (IFNULL(e.first_name, '') || ' ' || IFNULL(e.name, '')) LIKE @searchPattern
|
||||||
|
OR (IFNULL(e.name, '') || ' ' || IFNULL(e.first_name, '')) LIKE @searchPattern
|
||||||
|
)`);
|
||||||
|
} else {
|
||||||
|
conditions.push(`(
|
||||||
|
m.[number] LIKE @searchPattern
|
||||||
|
OR m.external_number LIKE @searchPattern
|
||||||
|
OR e.name LIKE @searchPattern
|
||||||
|
OR e.first_name LIKE @searchPattern
|
||||||
|
OR (ISNULL(e.first_name, '') + ' ' + ISNULL(e.name, '')) LIKE @searchPattern
|
||||||
|
OR (ISNULL(e.name, '') + ' ' + ISNULL(e.first_name, '')) LIKE @searchPattern
|
||||||
|
)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.onlyUnpaid === true) {
|
||||||
|
conditions.push('COALESCE(rs.is_paid, 0) = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.onlyUndelivered === true) {
|
||||||
|
conditions.push('COALESCE(rs.is_shipped, 0) = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.join('\n AND ');
|
||||||
|
const sqlCount = `
|
||||||
|
SELECT COUNT(*) AS total
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN retouresummary rs ON rs.manifest_id = m.id
|
||||||
|
LEFT JOIN entity e ON e.id = m.entity_id
|
||||||
|
WHERE ${whereClause}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let sqlList;
|
||||||
|
if (isSqlite) {
|
||||||
|
sqlList = `
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m."number",
|
||||||
|
m."type",
|
||||||
|
m.entity_id AS entityId,
|
||||||
|
m.external_number AS externalNumber,
|
||||||
|
m."date",
|
||||||
|
m.created_at AS createdAt,
|
||||||
|
rs.positions,
|
||||||
|
rs.gross_total AS grossTotal,
|
||||||
|
rs.paid_total AS paidTotal,
|
||||||
|
rs.ordered_quantity AS orderedQuantity,
|
||||||
|
rs.shipped_quantity AS shippedQuantity,
|
||||||
|
rs.is_paid AS isPaid,
|
||||||
|
rs.is_shipped AS isShipped,
|
||||||
|
rs.customer_name AS customerName
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN retouresummary rs ON rs.manifest_id = m.id
|
||||||
|
LEFT JOIN entity e ON e.id = m.entity_id
|
||||||
|
WHERE ${whereClause}
|
||||||
|
ORDER BY m."date" DESC, m.id DESC
|
||||||
|
${isPaged ? 'LIMIT @limit OFFSET @offset' : ''}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
sqlList = `
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.[number],
|
||||||
|
m.[type],
|
||||||
|
m.entity_id AS entityId,
|
||||||
|
m.external_number AS externalNumber,
|
||||||
|
m.[date],
|
||||||
|
m.created_at AS createdAt,
|
||||||
|
rs.positions,
|
||||||
|
rs.gross_total AS grossTotal,
|
||||||
|
rs.paid_total AS paidTotal,
|
||||||
|
rs.ordered_quantity AS orderedQuantity,
|
||||||
|
rs.shipped_quantity AS shippedQuantity,
|
||||||
|
rs.is_paid AS isPaid,
|
||||||
|
rs.is_shipped AS isShipped,
|
||||||
|
rs.customer_name AS customerName
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN retouresummary rs ON rs.manifest_id = m.id
|
||||||
|
LEFT JOIN entity e ON e.id = m.entity_id
|
||||||
|
WHERE ${whereClause}
|
||||||
|
ORDER BY m.[date] DESC, m.id DESC
|
||||||
|
${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUserFilters = (
|
||||||
|
opts?.seekMonths !== undefined ||
|
||||||
|
(typeof opts?.search === 'string' && opts.search.trim() !== '') ||
|
||||||
|
opts?.onlyUnpaid === true ||
|
||||||
|
opts?.onlyUndelivered === true
|
||||||
|
);
|
||||||
|
|
||||||
|
let manifestRows;
|
||||||
|
let total = 0;
|
||||||
|
let unfilteredTotal = 0;
|
||||||
|
|
||||||
|
if (isPaged) {
|
||||||
|
params.offset = offset;
|
||||||
|
params.limit = limit;
|
||||||
|
const [countResult, listResult, unfilteredCountResult] = await Promise.all([
|
||||||
|
runner.query(sqlCount, params),
|
||||||
|
runner.query(sqlList, params),
|
||||||
|
hasUserFilters ? runner.query(sqlCountManifests, { companyId, type: retoureType }) : null,
|
||||||
|
]);
|
||||||
|
total = Number(countResult.rows[0]?.total ?? 0);
|
||||||
|
unfilteredTotal = hasUserFilters
|
||||||
|
? Number(unfilteredCountResult?.rows[0]?.total ?? 0)
|
||||||
|
: total;
|
||||||
|
manifestRows = listResult.rows;
|
||||||
|
} else {
|
||||||
|
const result = await runner.query(sqlList, params);
|
||||||
|
manifestRows = result.rows;
|
||||||
|
total = manifestRows.length;
|
||||||
|
if (hasUserFilters) {
|
||||||
|
const countRes = await runner.query(sqlCountManifests, { companyId, type: retoureType });
|
||||||
|
unfilteredTotal = Number(countRes.rows[0]?.total ?? 0);
|
||||||
|
} else {
|
||||||
|
unfilteredTotal = total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifestIds = manifestRows.map((r) => r.id);
|
||||||
|
const linesByManifestId = new Map();
|
||||||
|
|
||||||
|
if (manifestIds.length > 0) {
|
||||||
|
const linesResult = await runner.query(sqlLinesByManifestIds, {
|
||||||
|
manifestIdsJson: JSON.stringify(manifestIds),
|
||||||
|
});
|
||||||
|
for (const row of linesResult.rows) {
|
||||||
|
let lines = linesByManifestId.get(row.manifestId);
|
||||||
|
if (!lines) {
|
||||||
|
lines = [];
|
||||||
|
linesByManifestId.set(row.manifestId, lines);
|
||||||
|
}
|
||||||
|
lines.push(shapeLine(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const retoures = manifestRows.map((row) => ({
|
||||||
|
...shapeSummary(retoureType, row),
|
||||||
|
lines: linesByManifestId.get(row.id) ?? [],
|
||||||
|
positions: row.positions !== undefined && row.positions !== null ? Number(row.positions) : undefined,
|
||||||
|
grossTotal: row.grossTotal !== undefined && row.grossTotal !== null ? Number(row.grossTotal) : undefined,
|
||||||
|
paidTotal: row.paidTotal !== undefined && row.paidTotal !== null ? Number(row.paidTotal) : undefined,
|
||||||
|
orderedQuantity: row.orderedQuantity !== undefined && row.orderedQuantity !== null ? Number(row.orderedQuantity) : undefined,
|
||||||
|
shippedQuantity: row.shippedQuantity !== undefined && row.shippedQuantity !== null ? Number(row.shippedQuantity) : undefined,
|
||||||
|
isPaid: row.isPaid !== undefined && row.isPaid !== null ? Boolean(row.isPaid) : undefined,
|
||||||
|
isShipped: row.isShipped !== undefined && row.isShipped !== null ? Boolean(row.isShipped) : undefined,
|
||||||
|
customerName: row.customerName ?? undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (isPaged) {
|
||||||
|
return {
|
||||||
|
retoures,
|
||||||
|
total,
|
||||||
|
unfilteredTotal,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return retoures;
|
||||||
|
};
|
||||||
|
}
|
||||||
19
src/store/retoure/rebuildSummaries.js
Normal file
19
src/store/retoure/rebuildSummaries.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
import { rebuildRetoureSummaries } from './syncSummary.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the `retouresummary` table for the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {import('../../providers/provider.js').DatabaseProvider} provider
|
||||||
|
*/
|
||||||
|
export default function rebuildSummaries(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
|
||||||
|
return async (googleAuthId, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
await rebuildRetoureSummaries(runner, { companyId });
|
||||||
|
return { ok: true, companyId };
|
||||||
|
};
|
||||||
|
}
|
||||||
368
src/store/retoure/syncSummary.js
Normal file
368
src/store/retoure/syncSummary.js
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
/**
|
||||||
|
* Centralized sync and rebuild logic for the materialized `retouresummary` table.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SQL_GET_SHIPMENT_PREFIX = `
|
||||||
|
SELECT prefix FROM numbers WHERE company_id = @companyId AND [type] = 'shipment'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_GET_RETOURE_BY_ID_SQLITE = `
|
||||||
|
SELECT m.id, m.company_id, m."number", m."date", m.created_at, m.entity_id
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m.id = @manifestId AND m.company_id = @companyId AND m."type" = 'retoure'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_GET_RETOURE_BY_ID_MSSQL = `
|
||||||
|
SELECT m.id, m.company_id, m.[number], m.[date], m.created_at, m.entity_id
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m.id = @manifestId AND m.company_id = @companyId AND m.[type] = 'retoure'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_GET_RETOURE_BY_NUMBER_SQLITE = `
|
||||||
|
SELECT m.id, m.company_id, m."number", m."date", m.created_at, m.entity_id
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m."number" = @number AND m.company_id = @companyId AND m."type" = 'retoure'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_GET_RETOURE_BY_NUMBER_MSSQL = `
|
||||||
|
SELECT m.id, m.company_id, m.[number], m.[date], m.created_at, m.entity_id
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m.[number] = @number AND m.company_id = @companyId AND m.[type] = 'retoure'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_LINE_TOTALS = `
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS positions,
|
||||||
|
COALESCE(SUM(CASE WHEN shippable = 1 THEN amount ELSE 0 END), 0) AS ordered_quantity,
|
||||||
|
COALESCE(SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)), 0) AS gross_total
|
||||||
|
FROM manifestarticle
|
||||||
|
WHERE manifest_id = @manifestId
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_PAYMENT_TOTAL = `
|
||||||
|
SELECT COALESCE(SUM(amount), 0) AS paid_total
|
||||||
|
FROM payment
|
||||||
|
WHERE manifest_id = @manifestId
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_SHIPPED_QTY_SQLITE = `
|
||||||
|
SELECT COALESCE(SUM(ma_s.amount), 0) AS shipped_quantity
|
||||||
|
FROM manifest m_s
|
||||||
|
JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id
|
||||||
|
LEFT JOIN shipmentsummary ss ON ss.manifest_id = m_s.id
|
||||||
|
WHERE m_s.company_id = @companyId
|
||||||
|
AND m_s."type" = 'shipment'
|
||||||
|
AND COALESCE(ss.state, 'new') != 'cancelled'
|
||||||
|
AND m_s."number" NOT LIKE '%#cancelled#%'
|
||||||
|
AND (
|
||||||
|
ss.order_id = @manifestId
|
||||||
|
OR ss.order_number = @retoureNumber
|
||||||
|
OR m_s."number" LIKE @retoureNumber || @shipmentPrefix || '%'
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_SHIPPED_QTY_MSSQL = `
|
||||||
|
SELECT COALESCE(SUM(ma_s.amount), 0) AS shipped_quantity
|
||||||
|
FROM manifest m_s WITH (NOLOCK)
|
||||||
|
JOIN manifestarticle ma_s WITH (NOLOCK) ON ma_s.manifest_id = m_s.id
|
||||||
|
LEFT JOIN shipmentsummary ss WITH (NOLOCK) ON ss.manifest_id = m_s.id
|
||||||
|
WHERE m_s.company_id = @companyId
|
||||||
|
AND m_s.[type] = 'shipment'
|
||||||
|
AND ISNULL(ss.state, 'new') != 'cancelled'
|
||||||
|
AND m_s.[number] NOT LIKE '%#cancelled#%'
|
||||||
|
AND (
|
||||||
|
ss.order_id = @manifestId
|
||||||
|
OR ss.order_number = @retoureNumber
|
||||||
|
OR m_s.[number] LIKE @retoureNumber + @shipmentPrefix + '%'
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_CUSTOMER_NAME_SQLITE = `
|
||||||
|
SELECT TRIM(COALESCE(first_name || ' ', '') || COALESCE(name, '')) AS customer_name
|
||||||
|
FROM entity
|
||||||
|
WHERE id = @entityId
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_CUSTOMER_NAME_MSSQL = `
|
||||||
|
SELECT LTRIM(RTRIM(ISNULL(first_name + ' ', '') + ISNULL(name, ''))) AS customer_name
|
||||||
|
FROM entity
|
||||||
|
WHERE id = @entityId
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_UPSERT_SUMMARY_SQLITE = `
|
||||||
|
INSERT INTO retouresummary (
|
||||||
|
company_id, manifest_id, retoure_number, retoure_date, created_at,
|
||||||
|
entity_id, customer_name, positions, gross_total, paid_total,
|
||||||
|
ordered_quantity, shipped_quantity, is_paid, is_shipped
|
||||||
|
) VALUES (
|
||||||
|
@companyId, @manifestId, @retoureNumber, @retoureDate, @createdAt,
|
||||||
|
@entityId, @customerName, @positions, @grossTotal, @paidTotal,
|
||||||
|
@orderedQuantity, @shippedQuantity, @isPaid, @isShipped
|
||||||
|
)
|
||||||
|
ON CONFLICT(manifest_id) DO UPDATE SET
|
||||||
|
retoure_number = excluded.retoure_number,
|
||||||
|
retoure_date = excluded.retoure_date,
|
||||||
|
created_at = excluded.created_at,
|
||||||
|
entity_id = excluded.entity_id,
|
||||||
|
customer_name = excluded.customer_name,
|
||||||
|
positions = excluded.positions,
|
||||||
|
gross_total = excluded.gross_total,
|
||||||
|
paid_total = excluded.paid_total,
|
||||||
|
ordered_quantity = excluded.ordered_quantity,
|
||||||
|
shipped_quantity = excluded.shipped_quantity,
|
||||||
|
is_paid = excluded.is_paid,
|
||||||
|
is_shipped = excluded.is_shipped
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_UPSERT_SUMMARY_MSSQL = `
|
||||||
|
MERGE INTO retouresummary AS target
|
||||||
|
USING (SELECT @manifestId AS manifest_id) AS source
|
||||||
|
ON (target.manifest_id = source.manifest_id)
|
||||||
|
WHEN MATCHED THEN
|
||||||
|
UPDATE SET
|
||||||
|
retoure_number = @retoureNumber,
|
||||||
|
retoure_date = @retoureDate,
|
||||||
|
created_at = @createdAt,
|
||||||
|
entity_id = @entityId,
|
||||||
|
customer_name = @customerName,
|
||||||
|
positions = @positions,
|
||||||
|
gross_total = @grossTotal,
|
||||||
|
paid_total = @paidTotal,
|
||||||
|
ordered_quantity = @orderedQuantity,
|
||||||
|
shipped_quantity = @shippedQuantity,
|
||||||
|
is_paid = @isPaid,
|
||||||
|
is_shipped = @isShipped
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT (
|
||||||
|
company_id, manifest_id, retoure_number, retoure_date, created_at,
|
||||||
|
entity_id, customer_name, positions, gross_total, paid_total,
|
||||||
|
ordered_quantity, shipped_quantity, is_paid, is_shipped
|
||||||
|
) VALUES (
|
||||||
|
@companyId, @manifestId, @retoureNumber, @retoureDate, @createdAt,
|
||||||
|
@entityId, @customerName, @positions, @grossTotal, @paidTotal,
|
||||||
|
@orderedQuantity, @shippedQuantity, @isPaid, @isShipped
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculates and upserts a single retoure summary into `retouresummary`.
|
||||||
|
*
|
||||||
|
* @param {object} runner - Database runner.
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {number} opts.companyId
|
||||||
|
* @param {number} [opts.manifestId] - Retoure manifest id.
|
||||||
|
* @param {string} [opts.retoureNumber] - Retoure number (if manifestId not given).
|
||||||
|
*/
|
||||||
|
export async function syncRetoureSummary(runner, { companyId, manifestId, retoureNumber }) {
|
||||||
|
const isSqlite = runner.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
let retoureRow;
|
||||||
|
if (manifestId) {
|
||||||
|
const res = await runner.query(
|
||||||
|
isSqlite ? SQL_GET_RETOURE_BY_ID_SQLITE : SQL_GET_RETOURE_BY_ID_MSSQL,
|
||||||
|
{ manifestId, companyId },
|
||||||
|
);
|
||||||
|
retoureRow = res.rows[0];
|
||||||
|
} else if (retoureNumber) {
|
||||||
|
const res = await runner.query(
|
||||||
|
isSqlite ? SQL_GET_RETOURE_BY_NUMBER_SQLITE : SQL_GET_RETOURE_BY_NUMBER_MSSQL,
|
||||||
|
{ number: retoureNumber, companyId },
|
||||||
|
);
|
||||||
|
retoureRow = res.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!retoureRow) {
|
||||||
|
if (manifestId) {
|
||||||
|
await runner.query('DELETE FROM retouresummary WHERE manifest_id = @manifestId', { manifestId });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retoureId = retoureRow.id;
|
||||||
|
const num = retoureRow.number;
|
||||||
|
|
||||||
|
const prefixRes = await runner.query(SQL_GET_SHIPMENT_PREFIX, { companyId });
|
||||||
|
const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-';
|
||||||
|
|
||||||
|
const lineRes = await runner.query(SQL_LINE_TOTALS, { manifestId: retoureId });
|
||||||
|
const payRes = await runner.query(SQL_PAYMENT_TOTAL, { manifestId: retoureId });
|
||||||
|
const shipRes = await runner.query(isSqlite ? SQL_SHIPPED_QTY_SQLITE : SQL_SHIPPED_QTY_MSSQL, {
|
||||||
|
companyId,
|
||||||
|
manifestId: retoureId,
|
||||||
|
retoureNumber: num,
|
||||||
|
shipmentPrefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
let customerName = null;
|
||||||
|
if (retoureRow.entity_id) {
|
||||||
|
const custRes = await runner.query(
|
||||||
|
isSqlite ? SQL_CUSTOMER_NAME_SQLITE : SQL_CUSTOMER_NAME_MSSQL,
|
||||||
|
{ entityId: retoureRow.entity_id },
|
||||||
|
);
|
||||||
|
customerName = custRes.rows[0]?.customer_name || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = Number(lineRes.rows[0]?.positions || 0);
|
||||||
|
const orderedQuantity = Math.round(Number(lineRes.rows[0]?.ordered_quantity || 0) * 1000) / 1000;
|
||||||
|
const grossTotal = Math.round(Number(lineRes.rows[0]?.gross_total || 0) * 100) / 100;
|
||||||
|
const paidTotal = Math.round(Number(payRes.rows[0]?.paid_total || 0) * 100) / 100;
|
||||||
|
const shippedQuantity = Math.round(Number(shipRes.rows[0]?.shipped_quantity || 0) * 1000) / 1000;
|
||||||
|
|
||||||
|
const isPaid = Math.abs(grossTotal) < 0.01 || Math.abs(paidTotal - grossTotal) < 0.01 ? 1 : 0;
|
||||||
|
const isShipped = orderedQuantity !== 0 && Math.abs(shippedQuantity - orderedQuantity) < 0.001 ? 1 : 0;
|
||||||
|
|
||||||
|
await runner.query(isSqlite ? SQL_UPSERT_SUMMARY_SQLITE : SQL_UPSERT_SUMMARY_MSSQL, {
|
||||||
|
companyId,
|
||||||
|
manifestId: retoureId,
|
||||||
|
retoureNumber: num,
|
||||||
|
retoureDate: retoureRow.date,
|
||||||
|
createdAt: retoureRow.created_at,
|
||||||
|
entityId: retoureRow.entity_id ?? null,
|
||||||
|
customerName,
|
||||||
|
positions,
|
||||||
|
grossTotal,
|
||||||
|
paidTotal,
|
||||||
|
orderedQuantity,
|
||||||
|
shippedQuantity,
|
||||||
|
isPaid,
|
||||||
|
isShipped,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full rebuild of all retoures in `retouresummary` for a given company.
|
||||||
|
*
|
||||||
|
* @param {object} runner
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {number} opts.companyId
|
||||||
|
*/
|
||||||
|
export async function rebuildRetoureSummaries(runner, { companyId }) {
|
||||||
|
const isSqlite = runner.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const prefixRes = await runner.query(SQL_GET_SHIPMENT_PREFIX, { companyId });
|
||||||
|
const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-';
|
||||||
|
|
||||||
|
if (isSqlite) {
|
||||||
|
await runner.query('DELETE FROM retouresummary WHERE company_id = @companyId', { companyId });
|
||||||
|
const sqlRebuildSqlite = `
|
||||||
|
WITH
|
||||||
|
retoure_articles AS (
|
||||||
|
SELECT
|
||||||
|
ma.manifest_id,
|
||||||
|
COUNT(*) AS positions,
|
||||||
|
SUM(CASE WHEN ma.shippable = 1 THEN ma.amount ELSE 0 END) AS ordered_quantity,
|
||||||
|
SUM(ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0)) AS gross_total
|
||||||
|
FROM manifestarticle ma
|
||||||
|
JOIN manifest m ON m.id = ma.manifest_id AND m.company_id = @companyId AND m."type" = 'retoure'
|
||||||
|
GROUP BY ma.manifest_id
|
||||||
|
),
|
||||||
|
retoure_payments AS (
|
||||||
|
SELECT
|
||||||
|
p.manifest_id,
|
||||||
|
SUM(p.amount) AS paid_total
|
||||||
|
FROM payment p
|
||||||
|
JOIN manifest m ON m.id = p.manifest_id AND m.company_id = @companyId AND m."type" = 'retoure'
|
||||||
|
GROUP BY p.manifest_id
|
||||||
|
),
|
||||||
|
shipment_totals AS (
|
||||||
|
SELECT
|
||||||
|
ss.order_id,
|
||||||
|
SUM(ma_s.amount) AS shipped_quantity
|
||||||
|
FROM shipmentsummary ss
|
||||||
|
JOIN manifestarticle ma_s ON ma_s.manifest_id = ss.manifest_id
|
||||||
|
WHERE ss.company_id = @companyId
|
||||||
|
AND COALESCE(ss.state, 'new') != 'cancelled'
|
||||||
|
AND ss.order_id IS NOT NULL
|
||||||
|
GROUP BY ss.order_id
|
||||||
|
)
|
||||||
|
INSERT INTO retouresummary (
|
||||||
|
company_id, manifest_id, retoure_number, retoure_date, created_at,
|
||||||
|
entity_id, customer_name, positions, gross_total, paid_total,
|
||||||
|
ordered_quantity, shipped_quantity, is_paid, is_shipped
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.company_id,
|
||||||
|
m.id AS manifest_id,
|
||||||
|
m."number" AS retoure_number,
|
||||||
|
m."date" AS retoure_date,
|
||||||
|
m.created_at,
|
||||||
|
m.entity_id,
|
||||||
|
TRIM(COALESCE(e.first_name || ' ', '') || COALESCE(e.name, '')) AS customer_name,
|
||||||
|
COALESCE(oa.positions, 0) AS positions,
|
||||||
|
ROUND(COALESCE(oa.gross_total, 0), 2) AS gross_total,
|
||||||
|
ROUND(COALESCE(op.paid_total, 0), 2) AS paid_total,
|
||||||
|
ROUND(COALESCE(oa.ordered_quantity, 0), 3) AS ordered_quantity,
|
||||||
|
ROUND(COALESCE(st.shipped_quantity, 0), 3) AS shipped_quantity,
|
||||||
|
CASE WHEN ABS(COALESCE(oa.gross_total, 0)) < 0.01 OR ABS(COALESCE(op.paid_total, 0) - oa.gross_total) < 0.01 THEN 1 ELSE 0 END AS is_paid,
|
||||||
|
CASE WHEN COALESCE(oa.ordered_quantity, 0) != 0 AND ABS(COALESCE(st.shipped_quantity, 0) - oa.ordered_quantity) < 0.001 THEN 1 ELSE 0 END AS is_shipped
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN entity e ON e.id = m.entity_id
|
||||||
|
LEFT JOIN retoure_articles oa ON oa.manifest_id = m.id
|
||||||
|
LEFT JOIN retoure_payments op ON op.manifest_id = m.id
|
||||||
|
LEFT JOIN shipment_totals st ON st.order_id = m.id
|
||||||
|
WHERE m.company_id = @companyId AND m."type" = 'retoure';
|
||||||
|
`;
|
||||||
|
await runner.query(sqlRebuildSqlite, { companyId, shipmentPrefix });
|
||||||
|
} else {
|
||||||
|
await runner.query('DELETE FROM retouresummary WHERE company_id = @companyId', { companyId });
|
||||||
|
const sqlRebuildMssql = `
|
||||||
|
WITH
|
||||||
|
retoure_articles AS (
|
||||||
|
SELECT
|
||||||
|
ma.manifest_id,
|
||||||
|
COUNT(*) AS positions,
|
||||||
|
SUM(CASE WHEN ma.shippable = 1 THEN ma.amount ELSE 0 END) AS ordered_quantity,
|
||||||
|
SUM(ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0)) AS gross_total
|
||||||
|
FROM manifestarticle ma WITH (NOLOCK)
|
||||||
|
JOIN manifest m WITH (NOLOCK) ON m.id = ma.manifest_id AND m.company_id = @companyId AND m.[type] = 'retoure'
|
||||||
|
GROUP BY ma.manifest_id
|
||||||
|
),
|
||||||
|
retoure_payments AS (
|
||||||
|
SELECT
|
||||||
|
p.manifest_id,
|
||||||
|
SUM(p.amount) AS paid_total
|
||||||
|
FROM payment p WITH (NOLOCK)
|
||||||
|
JOIN manifest m WITH (NOLOCK) ON m.id = p.manifest_id AND m.company_id = @companyId AND m.[type] = 'retoure'
|
||||||
|
GROUP BY p.manifest_id
|
||||||
|
),
|
||||||
|
shipment_totals AS (
|
||||||
|
SELECT
|
||||||
|
ss.order_id,
|
||||||
|
SUM(ma_s.amount) AS shipped_quantity
|
||||||
|
FROM shipmentsummary ss WITH (NOLOCK)
|
||||||
|
JOIN manifestarticle ma_s WITH (NOLOCK) ON ma_s.manifest_id = ss.manifest_id
|
||||||
|
WHERE ss.company_id = @companyId
|
||||||
|
AND ISNULL(ss.state, 'new') != 'cancelled'
|
||||||
|
AND ss.order_id IS NOT NULL
|
||||||
|
GROUP BY ss.order_id
|
||||||
|
)
|
||||||
|
INSERT INTO retouresummary (
|
||||||
|
company_id, manifest_id, retoure_number, retoure_date, created_at,
|
||||||
|
entity_id, customer_name, positions, gross_total, paid_total,
|
||||||
|
ordered_quantity, shipped_quantity, is_paid, is_shipped
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.company_id,
|
||||||
|
m.id AS manifest_id,
|
||||||
|
m.[number] AS retoure_number,
|
||||||
|
m.[date] AS retoure_date,
|
||||||
|
m.created_at,
|
||||||
|
m.entity_id,
|
||||||
|
LTRIM(RTRIM(ISNULL(e.first_name + ' ', '') + ISNULL(e.name, ''))) AS customer_name,
|
||||||
|
COALESCE(oa.positions, 0) AS positions,
|
||||||
|
ROUND(COALESCE(oa.gross_total, 0), 2) AS gross_total,
|
||||||
|
ROUND(COALESCE(op.paid_total, 0), 2) AS paid_total,
|
||||||
|
ROUND(COALESCE(oa.ordered_quantity, 0), 3) AS ordered_quantity,
|
||||||
|
ROUND(COALESCE(st.shipped_quantity, 0), 3) AS shipped_quantity,
|
||||||
|
CASE WHEN ABS(COALESCE(oa.gross_total, 0)) < 0.01 OR ABS(COALESCE(op.paid_total, 0) - oa.gross_total) < 0.01 THEN 1 ELSE 0 END AS is_paid,
|
||||||
|
CASE WHEN COALESCE(oa.ordered_quantity, 0) != 0 AND ABS(COALESCE(st.shipped_quantity, 0) - oa.ordered_quantity) < 0.001 THEN 1 ELSE 0 END AS is_shipped
|
||||||
|
FROM manifest m WITH (NOLOCK)
|
||||||
|
LEFT JOIN entity e WITH (NOLOCK) ON e.id = m.entity_id
|
||||||
|
LEFT JOIN retoure_articles oa ON oa.manifest_id = m.id
|
||||||
|
LEFT JOIN retoure_payments op ON op.manifest_id = m.id
|
||||||
|
LEFT JOIN shipment_totals st ON st.order_id = m.id
|
||||||
|
WHERE m.company_id = @companyId AND m.[type] = 'retoure';
|
||||||
|
`;
|
||||||
|
await runner.query(sqlRebuildMssql, { companyId, shipmentPrefix });
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/store/retoure/update.js
Normal file
144
src/store/retoure/update.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import {
|
||||||
|
DELETE_LINES_BY_MANIFEST as MSSQL_DELETE_LINES,
|
||||||
|
GET_MANIFEST_HEADER as MSSQL_GET_HEADER,
|
||||||
|
RETOURE_TYPE as MSSQL_RETOURE_TYPE,
|
||||||
|
UPDATE_MANIFEST as MSSQL_UPDATE_MANIFEST,
|
||||||
|
} from '../../providers/mssql/queries/manifestDoc.js';
|
||||||
|
import {
|
||||||
|
DELETE_LINES_BY_MANIFEST as SQLITE_DELETE_LINES,
|
||||||
|
GET_MANIFEST_HEADER as SQLITE_GET_HEADER,
|
||||||
|
RETOURE_TYPE as SQLITE_RETOURE_TYPE,
|
||||||
|
UPDATE_MANIFEST as SQLITE_UPDATE_MANIFEST,
|
||||||
|
} from '../../providers/sqlite3/queries/manifestDoc.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import taxEnsure from '../tax/ensure.js';
|
||||||
|
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||||
|
import { assertCustomerEntity, assertUniqueManifestNumber, loadDoc } from '../manifestDoc/helpers.js';
|
||||||
|
import { syncRetoureSummary } from './syncSummary.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function update(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const ensureTax = taxEnsure(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const deleteLinesSql = isSqlite ? SQLITE_DELETE_LINES : MSSQL_DELETE_LINES;
|
||||||
|
const getHeaderSql = isSqlite ? SQLITE_GET_HEADER : MSSQL_GET_HEADER;
|
||||||
|
const retoureType = isSqlite ? SQLITE_RETOURE_TYPE : MSSQL_RETOURE_TYPE;
|
||||||
|
const updateManifestSql = isSqlite ? SQLITE_UPDATE_MANIFEST : MSSQL_UPDATE_MANIFEST;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a retoure's header and/or lines.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} id - The id of the retoure to update.
|
||||||
|
* @param {object} patch - The update patch.
|
||||||
|
* @param {string} [patch.number] - The new retoure number.
|
||||||
|
* @param {string} [patch.date] - The new retoure date.
|
||||||
|
* @param {number} [patch.entityId] - The new customer entity id.
|
||||||
|
* @param {string} [patch.externalNumber] - The new external reference number.
|
||||||
|
* @param {Array<object>} [patch.lines] - The new retoure lines.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
|
* @returns {Promise<object>} The updated retoure.
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, id, patch = {}, tx) => {
|
||||||
|
if (!Number.isFinite(id)) {
|
||||||
|
throw new Error('Retoure not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const runUpdate = async (runner) => {
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
|
const current = await runner.query(getHeaderSql, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: retoureType,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!current.rows[0]) {
|
||||||
|
throw new Error('Retoure not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = current.rows[0];
|
||||||
|
|
||||||
|
let number = existing.number;
|
||||||
|
if (patch.number !== undefined) {
|
||||||
|
const trimmed = patch.number?.trim?.() ? patch.number.trim() : null;
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error('Invalid retoure');
|
||||||
|
}
|
||||||
|
number = trimmed;
|
||||||
|
if (number !== existing.number) {
|
||||||
|
await assertUniqueManifestNumber(runner, {
|
||||||
|
companyId,
|
||||||
|
type: retoureType,
|
||||||
|
number,
|
||||||
|
errorMessage: 'Retoure number already exists',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let date = existing.date;
|
||||||
|
if (patch.date !== undefined) {
|
||||||
|
const parsed = parseDate(patch.date);
|
||||||
|
if (!parsed) {
|
||||||
|
throw new Error('Invalid retoure');
|
||||||
|
}
|
||||||
|
date = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entityId = existing.entityId;
|
||||||
|
if (patch.entityId !== undefined) {
|
||||||
|
entityId = patch.entityId == null
|
||||||
|
? null
|
||||||
|
: await assertCustomerEntity(runner, companyId, patch.entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let externalNumber = existing.externalNumber;
|
||||||
|
if (patch.externalNumber !== undefined) {
|
||||||
|
externalNumber = patch.externalNumber?.trim?.()
|
||||||
|
? patch.externalNumber.trim()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runner.query(updateManifestSql, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: retoureType,
|
||||||
|
number,
|
||||||
|
entityId,
|
||||||
|
externalNumber,
|
||||||
|
date,
|
||||||
|
createdAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(patch.lines)) {
|
||||||
|
await runner.query(deleteLinesSql, { manifestId: id });
|
||||||
|
const lines = normalizeLines(patch.lines);
|
||||||
|
await insertLines(runner, {
|
||||||
|
manifestId: id,
|
||||||
|
lines,
|
||||||
|
ensureTax,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await loadDoc(runner, {
|
||||||
|
type: retoureType,
|
||||||
|
companyId,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncRetoureSummary(runner, { companyId, manifestId: id });
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
return runUpdate(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runUpdate);
|
||||||
|
}
|
||||||
|
return runUpdate(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
49
src/store/return/book.js
Normal file
49
src/store/return/book.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import stockAccept from '../stock/accept.js';
|
||||||
|
import { loadReturn } from './helpers.js';
|
||||||
|
import { syncReturnSummary } from './syncSummary.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Books all positions of a return manifest to the ledger.
|
||||||
|
*
|
||||||
|
* @param {object} provider - Storage provider instance.
|
||||||
|
* @returns {Function} `(googleAuthId, input, tx) => Promise<object>`
|
||||||
|
*/
|
||||||
|
export default function book(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const accept = stockAccept(provider);
|
||||||
|
|
||||||
|
return async (googleAuthId, input = {}, tx) => {
|
||||||
|
const rawId = typeof input === 'number' ? input : (input?.id ?? input?.returnId);
|
||||||
|
const id = Number(rawId);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
throw new Error('Return not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const warehouseId = input?.warehouseId ?? input?.warehouse_id ?? null;
|
||||||
|
|
||||||
|
const doBook = async (runner, activeTx) => {
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
const returnDoc = await loadReturn(provider, activeTx, companyId, id);
|
||||||
|
if (!returnDoc) {
|
||||||
|
throw new Error('Return not found');
|
||||||
|
}
|
||||||
|
if (!returnDoc.lines || returnDoc.lines.length === 0) {
|
||||||
|
throw new Error('Return has no lines');
|
||||||
|
}
|
||||||
|
|
||||||
|
await accept(googleAuthId, { deliveryId: id, warehouseId }, activeTx);
|
||||||
|
await syncReturnSummary(runner, { companyId, manifestId: id });
|
||||||
|
return loadReturn(provider, activeTx, companyId, id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
return doBook(makeRunner(provider, tx), tx);
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(doBook);
|
||||||
|
}
|
||||||
|
return doBook(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
89
src/store/return/create.js
Normal file
89
src/store/return/create.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { syncReturnSummary } from './syncSummary.js';
|
||||||
|
import * as mssqlQueries from '../../providers/mssql/queries/delivery.js';
|
||||||
|
import * as sqliteQueries from '../../providers/sqlite3/queries/delivery.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import nextNumber from '../numbers/nextNumber.js';
|
||||||
|
import taxEnsure from '../tax/ensure.js';
|
||||||
|
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||||
|
import { loadReturn, RETURN_TYPE } from './helpers.js';
|
||||||
|
import { assertSupplier } from '../delivery/writeLines.js';
|
||||||
|
import { assertUniqueManifestNumber } from '../manifestDoc/helpers.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function create(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const ensureTax = taxEnsure(provider);
|
||||||
|
const allocateNumber = nextNumber(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
|
return async (googleAuthId, body = {}) => {
|
||||||
|
const date = parseDate(body.date);
|
||||||
|
if (!date) {
|
||||||
|
throw new Error('Invalid return');
|
||||||
|
}
|
||||||
|
const createdAt = body.createdAt ? parseDate(body.createdAt) : new Date();
|
||||||
|
if (body.createdAt && !createdAt) {
|
||||||
|
throw new Error('Invalid return');
|
||||||
|
}
|
||||||
|
const externalNumber = body.externalNumber?.trim?.()
|
||||||
|
? body.externalNumber.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const lines = normalizeLines(body.lines ?? []);
|
||||||
|
|
||||||
|
const { companyId, companyuserId } = await access(googleAuthId);
|
||||||
|
|
||||||
|
const run = async (runner, tx) => {
|
||||||
|
const targetSupplierId = body.supplierId !== undefined ? body.supplierId : body.entityId;
|
||||||
|
if (targetSupplierId == null) {
|
||||||
|
throw new Error('No supplier selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierId = await assertSupplier(
|
||||||
|
provider,
|
||||||
|
tx,
|
||||||
|
companyId,
|
||||||
|
targetSupplierId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const number = body.number?.trim?.()
|
||||||
|
? body.number.trim()
|
||||||
|
: await allocateNumber(companyId, RETURN_TYPE, tx);
|
||||||
|
|
||||||
|
await assertUniqueManifestNumber(provider, tx, {
|
||||||
|
companyId,
|
||||||
|
type: RETURN_TYPE,
|
||||||
|
number,
|
||||||
|
errorMessage: 'Return number already exists',
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await runner.query(queries.CREATE_MANIFEST, {
|
||||||
|
companyId,
|
||||||
|
number,
|
||||||
|
type: RETURN_TYPE,
|
||||||
|
entityId: supplierId,
|
||||||
|
externalNumber,
|
||||||
|
date,
|
||||||
|
createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestId = created.rows[0]?.id ?? created.insertId;
|
||||||
|
|
||||||
|
await insertLines(provider, tx, {
|
||||||
|
manifestId,
|
||||||
|
lines,
|
||||||
|
ensureTax,
|
||||||
|
companyuserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncReturnSummary(runner, { companyId, manifestId });
|
||||||
|
return loadReturn(provider, tx, companyId, manifestId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction((runner, tx) => run(runner, tx));
|
||||||
|
}
|
||||||
|
return run(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
40
src/store/return/delete.js
Normal file
40
src/store/return/delete.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import * as mssqlQueries from '../../providers/mssql/queries/delivery.js';
|
||||||
|
import * as sqliteQueries from '../../providers/sqlite3/queries/delivery.js';
|
||||||
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import { RETURN_TYPE } from './helpers.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function del(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
|
return async (googleAuthId, id) => {
|
||||||
|
if (!Number.isFinite(id)) {
|
||||||
|
throw new Error('Return not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { companyId } = await access(googleAuthId);
|
||||||
|
const runner = makeRunner(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runner.query('DELETE FROM returnsummary WHERE manifest_id = @id AND company_id = @companyId', { id, companyId });
|
||||||
|
const result = await runner.query(queries.DELETE_MANIFEST, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: RETURN_TYPE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rowsAffected = result.rowsAffected?.[0] ?? (typeof result.rowsAffected === 'number' ? result.rowsAffected : 0);
|
||||||
|
if (!rowsAffected) {
|
||||||
|
throw new Error('Return not found');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (isForeignKeyConstraintError(err)) {
|
||||||
|
throw new Error('Return in use in ledger');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
28
src/store/return/getById.js
Normal file
28
src/store/return/getById.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import { loadReturn } from './helpers.js';
|
||||||
|
|
||||||
|
export default function getById(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a return manifest by its id within the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} id - The id of the return manifest.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<object>} The return manifest with lines and summary.
|
||||||
|
* @throws {Error} If the return is not found.
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, id, tx) => {
|
||||||
|
if (!Number.isFinite(id)) {
|
||||||
|
throw new Error('Return not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
const result = await loadReturn(provider, tx, companyId, id);
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Return not found');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
90
src/store/return/helpers.js
Normal file
90
src/store/return/helpers.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import * as mssqlQueries from '../../providers/mssql/queries/return.js';
|
||||||
|
import * as sqliteQueries from '../../providers/sqlite3/queries/return.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
import { shapeLine } from '../manifestLines.js';
|
||||||
|
import { syncReturnSummary } from './syncSummary.js';
|
||||||
|
|
||||||
|
export const RETURN_TYPE = mssqlQueries.RETURN_TYPE;
|
||||||
|
|
||||||
|
export function shapeSummary(row) {
|
||||||
|
const stockPositions = Number(row.stockPositions ?? 0);
|
||||||
|
const totalPositions = Number(row.stockPositions ?? row.totalPositions ?? row.positions ?? 0);
|
||||||
|
const bookedPositions = Number(row.bookedPositions ?? row.completelyOnLedgerPositions ?? 0);
|
||||||
|
const unbookedPositions = Number(row.unbookedPositions ?? Math.max(0, totalPositions - bookedPositions));
|
||||||
|
|
||||||
|
const totalQuantity = Number(row.totalQuantity ?? row.stockQuantity ?? 0);
|
||||||
|
const stockQuantity = Number(row.stockQuantity ?? 0);
|
||||||
|
const bookedQuantity = Number(row.bookedQuantity ?? 0);
|
||||||
|
const unbookedQuantity = Number(row.unbookedQuantity ?? Math.max(0, (stockQuantity || totalQuantity) - bookedQuantity));
|
||||||
|
const isBooked = row.isBooked !== undefined && row.isBooked !== null
|
||||||
|
? Boolean(row.isBooked)
|
||||||
|
: (totalPositions > 0 && bookedPositions >= totalPositions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
number: row.number,
|
||||||
|
type: RETURN_TYPE,
|
||||||
|
supplierId: row.supplierId ?? null,
|
||||||
|
supplierName: row.supplierName ?? undefined,
|
||||||
|
externalNumber: row.externalNumber ?? null,
|
||||||
|
date: row.date,
|
||||||
|
createdAt: row.createdAt ?? row.created_at ?? null,
|
||||||
|
positions: Number(row.positions ?? totalPositions),
|
||||||
|
grossTotal: row.grossTotal !== undefined && row.grossTotal !== null ? Number(row.grossTotal) : 0,
|
||||||
|
netTotal: row.netTotal !== undefined && row.netTotal !== null ? Number(row.netTotal) : 0,
|
||||||
|
totalQuantity,
|
||||||
|
stockPositions,
|
||||||
|
bookedPositions,
|
||||||
|
unbookedPositions,
|
||||||
|
stockQuantity,
|
||||||
|
bookedQuantity,
|
||||||
|
unbookedQuantity,
|
||||||
|
isBooked,
|
||||||
|
ledger: {
|
||||||
|
totalPositions,
|
||||||
|
completelyOnLedgerPositions: bookedPositions,
|
||||||
|
bookedPositions,
|
||||||
|
unbookedPositions,
|
||||||
|
totalQuantity: stockQuantity || totalQuantity,
|
||||||
|
bookedQuantity,
|
||||||
|
unbookedQuantity,
|
||||||
|
isBooked,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadReturn(provider, tx, companyId, id) {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
|
let header = await runner.query(queries.GET_RETURN_HEADER, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: RETURN_TYPE,
|
||||||
|
});
|
||||||
|
|
||||||
|
let row = header.rows[0];
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.positions === null || row.positions === undefined) {
|
||||||
|
await syncReturnSummary(runner, { companyId, manifestId: id });
|
||||||
|
header = await runner.query(queries.GET_RETURN_HEADER, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: RETURN_TYPE,
|
||||||
|
});
|
||||||
|
row = header.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = await runner.query(queries.GET_RETURN_LINES, {
|
||||||
|
manifestId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...shapeSummary(row),
|
||||||
|
lines: lines.rows.map(shapeLine),
|
||||||
|
};
|
||||||
|
}
|
||||||
237
src/store/return/list.js
Normal file
237
src/store/return/list.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import * as mssqlQueries from '../../providers/mssql/queries/return.js';
|
||||||
|
import * as sqliteQueries from '../../providers/sqlite3/queries/return.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import { shapeSummary } from './helpers.js';
|
||||||
|
|
||||||
|
export default function list(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
|
const sqlCountManifests = isSqlite
|
||||||
|
? 'SELECT COUNT(*) AS total FROM manifest WHERE company_id = @companyId AND "type" = @type'
|
||||||
|
: 'SELECT COUNT(*) AS total FROM manifest WHERE company_id = @companyId AND [type] = @type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the returns of the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [opts] - Optional pagination and filter options.
|
||||||
|
* @param {number} [opts.offset=0] - Number of returns to skip.
|
||||||
|
* @param {number} [opts.limit=50] - Number of returns to return.
|
||||||
|
* @param {string} [opts.search] - Free-text search matching return number, external number, or supplier name.
|
||||||
|
* @param {boolean} [opts.onlyUnbooked=false] - When true, returns only returns with unbooked stock items.
|
||||||
|
* @param {number} [opts.supplierId] - Filter by supplier entity id.
|
||||||
|
* @param {number} [opts.seekMonths] - Negative integer for cutoff months ago.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<{returns: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>}
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, opts, tx) => {
|
||||||
|
let offset = null;
|
||||||
|
let limit = null;
|
||||||
|
let isPaged = false;
|
||||||
|
let resolvedTx = tx;
|
||||||
|
|
||||||
|
const hasFilter = opts != null && typeof opts === 'object' && !opts._isTx && (
|
||||||
|
opts.offset !== undefined ||
|
||||||
|
opts.limit !== undefined ||
|
||||||
|
(typeof opts.search === 'string' && opts.search.trim() !== '') ||
|
||||||
|
opts.onlyUnbooked === true ||
|
||||||
|
opts.supplierId !== undefined ||
|
||||||
|
opts.seekMonths !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (opts != null && typeof opts === 'object' && !opts._isTx) {
|
||||||
|
if (hasFilter || opts.offset !== undefined || opts.limit !== undefined) {
|
||||||
|
isPaged = true;
|
||||||
|
const parsedOffset = Number(opts.offset ?? 0);
|
||||||
|
const parsedLimit = Number(opts.limit ?? 50);
|
||||||
|
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
|
||||||
|
throw new Error('Invalid offset');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
|
||||||
|
throw new Error('Invalid limit');
|
||||||
|
}
|
||||||
|
offset = parsedOffset;
|
||||||
|
limit = parsedLimit;
|
||||||
|
}
|
||||||
|
} else if (opts != null) {
|
||||||
|
resolvedTx = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = makeRunner(provider, resolvedTx);
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
|
const conditions = ['m.company_id = @companyId', `m.${isSqlite ? '"type"' : '[type]'} = @type`];
|
||||||
|
const params = {
|
||||||
|
companyId,
|
||||||
|
type: queries.RETURN_TYPE,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts?.supplierId !== undefined && opts.supplierId !== null) {
|
||||||
|
const supplierId = Number(opts.supplierId);
|
||||||
|
if (!Number.isInteger(supplierId) || supplierId <= 0) {
|
||||||
|
throw new Error('Invalid supplierId');
|
||||||
|
}
|
||||||
|
params.supplierId = supplierId;
|
||||||
|
conditions.push('m.entity_id = @supplierId');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.seekMonths !== undefined) {
|
||||||
|
if (typeof opts.seekMonths !== 'number' || !Number.isInteger(opts.seekMonths)) {
|
||||||
|
throw new Error('Invalid seekMonths');
|
||||||
|
}
|
||||||
|
const d = new Date();
|
||||||
|
d.setMonth(d.getMonth() - Math.abs(opts.seekMonths));
|
||||||
|
params.dateTo = d.toISOString();
|
||||||
|
conditions.push(`m.${isSqlite ? '"date"' : '[date]'} <= @dateTo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof opts?.search === 'string' && opts.search.trim() !== '') {
|
||||||
|
params.searchPattern = `%${opts.search.trim()}%`;
|
||||||
|
if (isSqlite) {
|
||||||
|
conditions.push(`(
|
||||||
|
m."number" LIKE @searchPattern
|
||||||
|
OR m.external_number LIKE @searchPattern
|
||||||
|
OR e.name LIKE @searchPattern
|
||||||
|
OR e.first_name LIKE @searchPattern
|
||||||
|
OR (IFNULL(e.first_name, '') || ' ' || IFNULL(e.name, '')) LIKE @searchPattern
|
||||||
|
OR (IFNULL(e.name, '') || ' ' || IFNULL(e.first_name, '')) LIKE @searchPattern
|
||||||
|
)`);
|
||||||
|
} else {
|
||||||
|
conditions.push(`(
|
||||||
|
m.[number] LIKE @searchPattern
|
||||||
|
OR m.external_number LIKE @searchPattern
|
||||||
|
OR e.name LIKE @searchPattern
|
||||||
|
OR e.first_name LIKE @searchPattern
|
||||||
|
OR (ISNULL(e.first_name, '') + ' ' + ISNULL(e.name, '')) LIKE @searchPattern
|
||||||
|
OR (ISNULL(e.name, '') + ' ' + ISNULL(e.first_name, '')) LIKE @searchPattern
|
||||||
|
)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.onlyUnbooked === true) {
|
||||||
|
conditions.push('COALESCE(rs.is_booked, 0) = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.join('\n AND ');
|
||||||
|
const sqlCount = `
|
||||||
|
SELECT COUNT(*) AS total
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN returnsummary rs ON rs.manifest_id = m.id
|
||||||
|
LEFT JOIN entity e ON e.id = m.entity_id
|
||||||
|
WHERE ${whereClause}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let sqlList;
|
||||||
|
if (isSqlite) {
|
||||||
|
sqlList = `
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m."number",
|
||||||
|
m."type",
|
||||||
|
m.entity_id AS supplierId,
|
||||||
|
m.external_number AS externalNumber,
|
||||||
|
m."date",
|
||||||
|
m.created_at AS createdAt,
|
||||||
|
rs.positions,
|
||||||
|
rs.gross_total AS grossTotal,
|
||||||
|
rs.net_total AS netTotal,
|
||||||
|
rs.total_quantity AS totalQuantity,
|
||||||
|
rs.stock_positions AS stockPositions,
|
||||||
|
rs.booked_positions AS bookedPositions,
|
||||||
|
rs.unbooked_positions AS unbookedPositions,
|
||||||
|
rs.stock_quantity AS stockQuantity,
|
||||||
|
rs.booked_quantity AS bookedQuantity,
|
||||||
|
rs.unbooked_quantity AS unbookedQuantity,
|
||||||
|
rs.is_booked AS isBooked,
|
||||||
|
rs.supplier_name AS supplierName
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN returnsummary rs ON rs.manifest_id = m.id
|
||||||
|
LEFT JOIN entity e ON e.id = m.entity_id
|
||||||
|
WHERE ${whereClause}
|
||||||
|
ORDER BY m."date" DESC, m.id DESC
|
||||||
|
${isPaged ? 'LIMIT @limit OFFSET @offset' : ''}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
sqlList = `
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.[number],
|
||||||
|
m.[type],
|
||||||
|
m.entity_id AS supplierId,
|
||||||
|
m.external_number AS externalNumber,
|
||||||
|
m.[date],
|
||||||
|
m.created_at AS createdAt,
|
||||||
|
rs.positions,
|
||||||
|
rs.gross_total AS grossTotal,
|
||||||
|
rs.net_total AS netTotal,
|
||||||
|
rs.total_quantity AS totalQuantity,
|
||||||
|
rs.stock_positions AS stockPositions,
|
||||||
|
rs.booked_positions AS bookedPositions,
|
||||||
|
rs.unbooked_positions AS unbookedPositions,
|
||||||
|
rs.stock_quantity AS stockQuantity,
|
||||||
|
rs.booked_quantity AS bookedQuantity,
|
||||||
|
rs.unbooked_quantity AS unbookedQuantity,
|
||||||
|
rs.is_booked AS isBooked,
|
||||||
|
rs.supplier_name AS supplierName
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN returnsummary rs ON rs.manifest_id = m.id
|
||||||
|
LEFT JOIN entity e ON e.id = m.entity_id
|
||||||
|
WHERE ${whereClause}
|
||||||
|
ORDER BY m.[date] DESC, m.id DESC
|
||||||
|
${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUserFilters = (
|
||||||
|
opts?.seekMonths !== undefined ||
|
||||||
|
(typeof opts?.search === 'string' && opts.search.trim() !== '') ||
|
||||||
|
opts?.onlyUnbooked === true
|
||||||
|
);
|
||||||
|
|
||||||
|
let rows;
|
||||||
|
let total = 0;
|
||||||
|
let unfilteredTotal = 0;
|
||||||
|
|
||||||
|
if (isPaged) {
|
||||||
|
params.offset = offset;
|
||||||
|
params.limit = limit;
|
||||||
|
const [countResult, listResult, unfilteredCountResult] = await Promise.all([
|
||||||
|
runner.query(sqlCount, params),
|
||||||
|
runner.query(sqlList, params),
|
||||||
|
hasUserFilters ? runner.query(sqlCountManifests, { companyId, type: queries.RETURN_TYPE }) : null,
|
||||||
|
]);
|
||||||
|
total = Number(countResult.rows[0]?.total ?? 0);
|
||||||
|
unfilteredTotal = hasUserFilters
|
||||||
|
? Number(unfilteredCountResult?.rows[0]?.total ?? 0)
|
||||||
|
: total;
|
||||||
|
rows = listResult.rows;
|
||||||
|
} else {
|
||||||
|
const result = await runner.query(sqlList, params);
|
||||||
|
rows = result.rows;
|
||||||
|
total = rows.length;
|
||||||
|
if (hasUserFilters) {
|
||||||
|
const countRes = await runner.query(sqlCountManifests, { companyId, type: queries.RETURN_TYPE });
|
||||||
|
unfilteredTotal = Number(countRes.rows[0]?.total ?? 0);
|
||||||
|
} else {
|
||||||
|
unfilteredTotal = total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const returns = rows.map(shapeSummary);
|
||||||
|
|
||||||
|
if (isPaged) {
|
||||||
|
return {
|
||||||
|
returns,
|
||||||
|
total,
|
||||||
|
unfilteredTotal,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returns;
|
||||||
|
};
|
||||||
|
}
|
||||||
19
src/store/return/rebuildSummaries.js
Normal file
19
src/store/return/rebuildSummaries.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
import { rebuildReturnSummaries } from './syncSummary.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the `returnsummary` table for the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {import('../../providers/provider.js').DatabaseProvider} provider
|
||||||
|
*/
|
||||||
|
export default function rebuildSummaries(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
|
||||||
|
return async (googleAuthId, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
await rebuildReturnSummaries(runner, { companyId });
|
||||||
|
return { ok: true, companyId };
|
||||||
|
};
|
||||||
|
}
|
||||||
308
src/store/return/syncSummary.js
Normal file
308
src/store/return/syncSummary.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
/**
|
||||||
|
* Centralized sync and rebuild logic for the materialized `returnsummary` table.
|
||||||
|
*
|
||||||
|
* Keeps `returnsummary` in sync with manifest (return), manifestarticle,
|
||||||
|
* article (track_stock), and ledger (RETURN transactions).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SQL_GET_RETURN_BY_ID_SQLITE = `
|
||||||
|
SELECT m.id, m.company_id, m."number", m.external_number, m."date", m.created_at, m.entity_id
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m.id = @manifestId AND m.company_id = @companyId AND m."type" = 'return'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_GET_RETURN_BY_ID_MSSQL = `
|
||||||
|
SELECT m.id, m.company_id, m.[number], m.external_number, m.[date], m.created_at, m.entity_id
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m.id = @manifestId AND m.company_id = @companyId AND m.[type] = 'return'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_GET_RETURN_BY_NUMBER_SQLITE = `
|
||||||
|
SELECT m.id, m.company_id, m."number", m.external_number, m."date", m.created_at, m.entity_id
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m."number" = @number AND m.company_id = @companyId AND m."type" = 'return'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_GET_RETURN_BY_NUMBER_MSSQL = `
|
||||||
|
SELECT m.id, m.company_id, m.[number], m.external_number, m.[date], m.created_at, m.entity_id
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m.[number] = @number AND m.company_id = @companyId AND m.[type] = 'return'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_SUPPLIER_NAME_SQLITE = `
|
||||||
|
SELECT TRIM(COALESCE(first_name || ' ', '') || COALESCE(name, '')) AS supplier_name
|
||||||
|
FROM entity
|
||||||
|
WHERE id = @entityId
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_SUPPLIER_NAME_MSSQL = `
|
||||||
|
SELECT LTRIM(RTRIM(ISNULL(first_name + ' ', '') + ISNULL(name, ''))) AS supplier_name
|
||||||
|
FROM entity
|
||||||
|
WHERE id = @entityId
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_RETURN_TOTALS_SQLITE = `
|
||||||
|
WITH line_ledger AS (
|
||||||
|
SELECT
|
||||||
|
manifestarticle_id,
|
||||||
|
SUM(amount) AS booked_amount
|
||||||
|
FROM ledger
|
||||||
|
WHERE warehouse_id IS NULL
|
||||||
|
AND UPPER(transaction_type) IN ('RETURN', 'ACCEPTANCE', 'DELIVERY', 'GOODS_RECEIPT')
|
||||||
|
GROUP BY manifestarticle_id
|
||||||
|
),
|
||||||
|
line_data AS (
|
||||||
|
SELECT
|
||||||
|
ma.id,
|
||||||
|
ma.amount,
|
||||||
|
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0) AS line_gross,
|
||||||
|
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) AS line_net,
|
||||||
|
CASE WHEN COALESCE(a.track_stock, 1) = 0 THEN 0 ELSE 1 END AS track_stock,
|
||||||
|
COALESCE(ll.booked_amount, 0) AS booked_amount
|
||||||
|
FROM manifestarticle ma
|
||||||
|
LEFT JOIN article a ON a.company_id = @companyId AND (a.sku = ma.sku OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = ma.sku))
|
||||||
|
LEFT JOIN line_ledger ll ON ll.manifestarticle_id = ma.id
|
||||||
|
WHERE ma.manifest_id = @manifestId
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS positions,
|
||||||
|
COALESCE(SUM(line_gross), 0) AS gross_total,
|
||||||
|
COALESCE(SUM(line_net), 0) AS net_total,
|
||||||
|
COALESCE(SUM(amount), 0) AS total_quantity,
|
||||||
|
COALESCE(SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END), 0) AS stock_positions,
|
||||||
|
COALESCE(SUM(CASE WHEN track_stock = 1 AND ((amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0)) THEN 1 ELSE 0 END), 0) AS stock_booked_positions,
|
||||||
|
COALESCE(SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END), 0) AS stock_quantity,
|
||||||
|
COALESCE(SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END), 0) AS stock_booked_quantity,
|
||||||
|
COALESCE(SUM(CASE WHEN (amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0) THEN 1 ELSE 0 END), 0) AS all_booked_positions,
|
||||||
|
COALESCE(SUM(booked_amount), 0) AS all_booked_quantity
|
||||||
|
FROM line_data
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_RETURN_TOTALS_MSSQL = `
|
||||||
|
WITH line_ledger AS (
|
||||||
|
SELECT
|
||||||
|
manifestarticle_id,
|
||||||
|
SUM(amount) AS booked_amount
|
||||||
|
FROM ledger
|
||||||
|
WHERE warehouse_id IS NULL
|
||||||
|
AND UPPER(transaction_type) IN ('RETURN', 'ACCEPTANCE', 'DELIVERY', 'GOODS_RECEIPT')
|
||||||
|
GROUP BY manifestarticle_id
|
||||||
|
),
|
||||||
|
line_data AS (
|
||||||
|
SELECT
|
||||||
|
ma.id,
|
||||||
|
ma.amount,
|
||||||
|
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0) AS line_gross,
|
||||||
|
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) AS line_net,
|
||||||
|
CASE WHEN COALESCE(a.track_stock, 1) = 0 THEN 0 ELSE 1 END AS track_stock,
|
||||||
|
COALESCE(ll.booked_amount, 0) AS booked_amount
|
||||||
|
FROM manifestarticle ma
|
||||||
|
LEFT JOIN article a ON a.company_id = @companyId AND (a.sku = ma.sku OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = ma.sku))
|
||||||
|
LEFT JOIN line_ledger ll ON ll.manifestarticle_id = ma.id
|
||||||
|
WHERE ma.manifest_id = @manifestId
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS positions,
|
||||||
|
COALESCE(SUM(line_gross), 0) AS gross_total,
|
||||||
|
COALESCE(SUM(line_net), 0) AS net_total,
|
||||||
|
COALESCE(SUM(amount), 0) AS total_quantity,
|
||||||
|
COALESCE(SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END), 0) AS stock_positions,
|
||||||
|
COALESCE(SUM(CASE WHEN track_stock = 1 AND ((amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0)) THEN 1 ELSE 0 END), 0) AS stock_booked_positions,
|
||||||
|
COALESCE(SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END), 0) AS stock_quantity,
|
||||||
|
COALESCE(SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END), 0) AS stock_booked_quantity,
|
||||||
|
COALESCE(SUM(CASE WHEN (amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0) THEN 1 ELSE 0 END), 0) AS all_booked_positions,
|
||||||
|
COALESCE(SUM(booked_amount), 0) AS all_booked_quantity
|
||||||
|
FROM line_data
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_UPSERT_SUMMARY_SQLITE = `
|
||||||
|
INSERT INTO returnsummary (
|
||||||
|
company_id, manifest_id, return_number, external_number, return_date, created_at,
|
||||||
|
supplier_id, supplier_name, positions, gross_total, net_total, total_quantity,
|
||||||
|
stock_positions, booked_positions, unbooked_positions,
|
||||||
|
stock_quantity, booked_quantity, unbooked_quantity, is_booked
|
||||||
|
) VALUES (
|
||||||
|
@companyId, @manifestId, @returnNumber, @externalNumber, @returnDate, @createdAt,
|
||||||
|
@supplierId, @supplierName, @positions, @grossTotal, @netTotal, @totalQuantity,
|
||||||
|
@stockPositions, @bookedPositions, @unbookedPositions,
|
||||||
|
@stockQuantity, @bookedQuantity, @unbookedQuantity, @isBooked
|
||||||
|
)
|
||||||
|
ON CONFLICT(manifest_id) DO UPDATE SET
|
||||||
|
return_number = excluded.return_number,
|
||||||
|
external_number = excluded.external_number,
|
||||||
|
return_date = excluded.return_date,
|
||||||
|
created_at = excluded.created_at,
|
||||||
|
supplier_id = excluded.supplier_id,
|
||||||
|
supplier_name = excluded.supplier_name,
|
||||||
|
positions = excluded.positions,
|
||||||
|
gross_total = excluded.gross_total,
|
||||||
|
net_total = excluded.net_total,
|
||||||
|
total_quantity = excluded.total_quantity,
|
||||||
|
stock_positions = excluded.stock_positions,
|
||||||
|
booked_positions = excluded.booked_positions,
|
||||||
|
unbooked_positions = excluded.unbooked_positions,
|
||||||
|
stock_quantity = excluded.stock_quantity,
|
||||||
|
booked_quantity = excluded.booked_quantity,
|
||||||
|
unbooked_quantity = excluded.unbooked_quantity,
|
||||||
|
is_booked = excluded.is_booked
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SQL_UPSERT_SUMMARY_MSSQL = `
|
||||||
|
MERGE INTO returnsummary AS target
|
||||||
|
USING (SELECT @manifestId AS manifest_id) AS source
|
||||||
|
ON (target.manifest_id = source.manifest_id)
|
||||||
|
WHEN MATCHED THEN
|
||||||
|
UPDATE SET
|
||||||
|
return_number = @returnNumber,
|
||||||
|
external_number = @externalNumber,
|
||||||
|
return_date = @returnDate,
|
||||||
|
created_at = @createdAt,
|
||||||
|
supplier_id = @supplierId,
|
||||||
|
supplier_name = @supplierName,
|
||||||
|
positions = @positions,
|
||||||
|
gross_total = @grossTotal,
|
||||||
|
net_total = @netTotal,
|
||||||
|
total_quantity = @totalQuantity,
|
||||||
|
stock_positions = @stockPositions,
|
||||||
|
booked_positions = @bookedPositions,
|
||||||
|
unbooked_positions = @unbookedPositions,
|
||||||
|
stock_quantity = @stockQuantity,
|
||||||
|
booked_quantity = @bookedQuantity,
|
||||||
|
unbooked_quantity = @unbookedQuantity,
|
||||||
|
is_booked = @isBooked
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT (
|
||||||
|
company_id, manifest_id, return_number, external_number, return_date, created_at,
|
||||||
|
supplier_id, supplier_name, positions, gross_total, net_total, total_quantity,
|
||||||
|
stock_positions, booked_positions, unbooked_positions,
|
||||||
|
stock_quantity, booked_quantity, unbooked_quantity, is_booked
|
||||||
|
) VALUES (
|
||||||
|
@companyId, @manifestId, @returnNumber, @externalNumber, @returnDate, @createdAt,
|
||||||
|
@supplierId, @supplierName, @positions, @grossTotal, @netTotal, @totalQuantity,
|
||||||
|
@stockPositions, @bookedPositions, @unbookedPositions,
|
||||||
|
@stockQuantity, @bookedQuantity, @unbookedQuantity, @isBooked
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculates and upserts a single return summary into `returnsummary`.
|
||||||
|
*
|
||||||
|
* @param {object} runner - Database runner.
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {number} opts.companyId
|
||||||
|
* @param {number} [opts.manifestId] - Return manifest id.
|
||||||
|
* @param {string} [opts.returnNumber] - Return number (if manifestId not given).
|
||||||
|
*/
|
||||||
|
export async function syncReturnSummary(runner, { companyId, manifestId, returnNumber }) {
|
||||||
|
const isSqlite = runner.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
let returnRow;
|
||||||
|
if (manifestId) {
|
||||||
|
const res = await runner.query(
|
||||||
|
isSqlite ? SQL_GET_RETURN_BY_ID_SQLITE : SQL_GET_RETURN_BY_ID_MSSQL,
|
||||||
|
{ manifestId, companyId },
|
||||||
|
);
|
||||||
|
returnRow = res.rows[0];
|
||||||
|
} else if (returnNumber) {
|
||||||
|
const res = await runner.query(
|
||||||
|
isSqlite ? SQL_GET_RETURN_BY_NUMBER_SQLITE : SQL_GET_RETURN_BY_NUMBER_MSSQL,
|
||||||
|
{ number: returnNumber, companyId },
|
||||||
|
);
|
||||||
|
returnRow = res.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!returnRow) {
|
||||||
|
if (manifestId) {
|
||||||
|
await runner.query('DELETE FROM returnsummary WHERE manifest_id = @manifestId', { manifestId });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnId = returnRow.id;
|
||||||
|
const num = returnRow.number;
|
||||||
|
|
||||||
|
const totalsRes = await runner.query(
|
||||||
|
isSqlite ? SQL_RETURN_TOTALS_SQLITE : SQL_RETURN_TOTALS_MSSQL,
|
||||||
|
{ manifestId: returnId, companyId },
|
||||||
|
);
|
||||||
|
const totals = totalsRes.rows[0] ?? {};
|
||||||
|
|
||||||
|
let supplierName = null;
|
||||||
|
if (returnRow.entity_id) {
|
||||||
|
const suppRes = await runner.query(
|
||||||
|
isSqlite ? SQL_SUPPLIER_NAME_SQLITE : SQL_SUPPLIER_NAME_MSSQL,
|
||||||
|
{ entityId: returnRow.entity_id },
|
||||||
|
);
|
||||||
|
supplierName = suppRes.rows[0]?.supplier_name || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = Number(totals.positions || 0);
|
||||||
|
const grossTotal = Math.round(Number(totals.gross_total || 0) * 100) / 100;
|
||||||
|
const netTotal = Math.round(Number(totals.net_total || 0) * 100) / 100;
|
||||||
|
const totalQuantity = Math.round(Number(totals.total_quantity || 0) * 1000) / 1000;
|
||||||
|
|
||||||
|
const stockPositions = Number(totals.stock_positions || 0);
|
||||||
|
const stockQuantity = Math.round(Number(totals.stock_quantity || 0) * 1000) / 1000;
|
||||||
|
|
||||||
|
let bookedPositions;
|
||||||
|
let bookedQuantity;
|
||||||
|
let unbookedPositions;
|
||||||
|
let unbookedQuantity;
|
||||||
|
|
||||||
|
if (stockPositions > 0) {
|
||||||
|
bookedPositions = Number(totals.stock_booked_positions || 0);
|
||||||
|
bookedQuantity = Math.round(Number(totals.stock_booked_quantity || 0) * 1000) / 1000;
|
||||||
|
unbookedPositions = Math.max(0, stockPositions - bookedPositions);
|
||||||
|
unbookedQuantity = Math.round(Math.max(0, stockQuantity - bookedQuantity) * 1000) / 1000;
|
||||||
|
} else {
|
||||||
|
bookedPositions = Number(totals.all_booked_positions || 0);
|
||||||
|
bookedQuantity = Math.round(Number(totals.all_booked_quantity || 0) * 1000) / 1000;
|
||||||
|
unbookedPositions = Math.max(0, positions - bookedPositions);
|
||||||
|
unbookedQuantity = Math.round(Math.max(0, totalQuantity - bookedQuantity) * 1000) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBooked = positions > 0 && unbookedPositions === 0 ? 1 : 0;
|
||||||
|
|
||||||
|
await runner.query(isSqlite ? SQL_UPSERT_SUMMARY_SQLITE : SQL_UPSERT_SUMMARY_MSSQL, {
|
||||||
|
companyId,
|
||||||
|
manifestId: returnId,
|
||||||
|
returnNumber: num,
|
||||||
|
externalNumber: returnRow.external_number ?? null,
|
||||||
|
returnDate: returnRow.date,
|
||||||
|
createdAt: returnRow.created_at,
|
||||||
|
supplierId: returnRow.entity_id ?? null,
|
||||||
|
supplierName,
|
||||||
|
positions,
|
||||||
|
grossTotal,
|
||||||
|
netTotal,
|
||||||
|
totalQuantity,
|
||||||
|
stockPositions,
|
||||||
|
bookedPositions,
|
||||||
|
unbookedPositions,
|
||||||
|
stockQuantity,
|
||||||
|
bookedQuantity,
|
||||||
|
unbookedQuantity,
|
||||||
|
isBooked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full rebuild of all returns in `returnsummary` for a given company.
|
||||||
|
*
|
||||||
|
* @param {object} runner
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {number} opts.companyId
|
||||||
|
*/
|
||||||
|
export async function rebuildReturnSummaries(runner, { companyId }) {
|
||||||
|
const isSqlite = runner.dialect === 'sqlite3';
|
||||||
|
await runner.query('DELETE FROM returnsummary WHERE company_id = @companyId', { companyId });
|
||||||
|
|
||||||
|
const listReturnsSql = isSqlite
|
||||||
|
? 'SELECT id FROM manifest WHERE company_id = @companyId AND "type" = \'return\''
|
||||||
|
: 'SELECT id FROM manifest WHERE company_id = @companyId AND [type] = \'return\'';
|
||||||
|
|
||||||
|
const returns = (await runner.query(listReturnsSql, { companyId })).rows;
|
||||||
|
for (const r of returns) {
|
||||||
|
await syncReturnSummary(runner, { companyId, manifestId: r.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
148
src/store/return/update.js
Normal file
148
src/store/return/update.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { syncReturnSummary } from './syncSummary.js';
|
||||||
|
import * as mssqlQueries from '../../providers/mssql/queries/delivery.js';
|
||||||
|
import * as sqliteQueries from '../../providers/sqlite3/queries/delivery.js';
|
||||||
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import taxEnsure from '../tax/ensure.js';
|
||||||
|
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||||
|
import { loadReturn, shapeSummary, RETURN_TYPE } from './helpers.js';
|
||||||
|
import { assertSupplier } from '../delivery/writeLines.js';
|
||||||
|
import { assertUniqueManifestNumber } from '../manifestDoc/helpers.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function update(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const ensureTax = taxEnsure(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
|
return async (googleAuthId, id, patch = {}) => {
|
||||||
|
if (!Number.isFinite(id)) {
|
||||||
|
throw new Error('Return not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number');
|
||||||
|
const hasDate = Object.prototype.hasOwnProperty.call(patch, 'date');
|
||||||
|
const hasCreatedAt = Object.prototype.hasOwnProperty.call(patch, 'createdAt');
|
||||||
|
const hasSupplierId = Object.prototype.hasOwnProperty.call(patch, 'supplierId');
|
||||||
|
const hasEntityId = Object.prototype.hasOwnProperty.call(patch, 'entityId');
|
||||||
|
const hasExternalNumber = Object.prototype.hasOwnProperty.call(patch, 'externalNumber');
|
||||||
|
const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines');
|
||||||
|
|
||||||
|
if (!hasNumber && !hasDate && !hasCreatedAt && !hasSupplierId && !hasEntityId && !hasExternalNumber && !hasLines) {
|
||||||
|
throw new Error('Invalid return');
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalizedLines;
|
||||||
|
if (hasLines) {
|
||||||
|
normalizedLines = normalizeLines(patch.lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { companyId, companyuserId } = await access(googleAuthId);
|
||||||
|
|
||||||
|
const run = async (runner, tx) => {
|
||||||
|
const existing = await runner.query(
|
||||||
|
isSqlite
|
||||||
|
? 'SELECT m.id, m."number", m.external_number AS externalNumber, m."date", m.created_at AS createdAt, m.entity_id AS supplierId FROM manifest m WHERE m.id = @id AND m.company_id = @companyId AND m."type" = @type'
|
||||||
|
: 'SELECT m.id, m.[number], m.external_number AS externalNumber, m.[date], m.created_at AS createdAt, m.entity_id AS supplierId FROM manifest m WHERE m.id = @id AND m.company_id = @companyId AND m.[type] = @type',
|
||||||
|
{ id, companyId, type: RETURN_TYPE },
|
||||||
|
);
|
||||||
|
|
||||||
|
const current = existing.rows[0];
|
||||||
|
if (!current) {
|
||||||
|
throw new Error('Return not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
let number = current.number;
|
||||||
|
if (hasNumber) {
|
||||||
|
number = patch.number?.trim?.() ? patch.number.trim() : null;
|
||||||
|
if (!number) {
|
||||||
|
throw new Error('Invalid return');
|
||||||
|
}
|
||||||
|
await assertUniqueManifestNumber(provider, tx, {
|
||||||
|
companyId,
|
||||||
|
type: RETURN_TYPE,
|
||||||
|
number,
|
||||||
|
excludeId: id,
|
||||||
|
errorMessage: 'Return number already exists',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let date = current.date;
|
||||||
|
if (hasDate) {
|
||||||
|
date = parseDate(patch.date);
|
||||||
|
if (!date) {
|
||||||
|
throw new Error('Invalid return');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let createdAt = current.createdAt ? parseDate(current.createdAt) : null;
|
||||||
|
if (hasCreatedAt) {
|
||||||
|
createdAt = parseDate(patch.createdAt);
|
||||||
|
if (!createdAt) {
|
||||||
|
throw new Error('Invalid return');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let supplierId = current.supplierId;
|
||||||
|
if (hasSupplierId || hasEntityId) {
|
||||||
|
const val = hasSupplierId ? patch.supplierId : patch.entityId;
|
||||||
|
if (val == null) {
|
||||||
|
throw new Error('No supplier selected');
|
||||||
|
}
|
||||||
|
supplierId = await assertSupplier(
|
||||||
|
provider,
|
||||||
|
tx,
|
||||||
|
companyId,
|
||||||
|
val,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let externalNumber = current.externalNumber;
|
||||||
|
if (hasExternalNumber) {
|
||||||
|
externalNumber = patch.externalNumber?.trim?.()
|
||||||
|
? patch.externalNumber.trim()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runner.query(queries.UPDATE_MANIFEST, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: RETURN_TYPE,
|
||||||
|
number,
|
||||||
|
entityId: supplierId,
|
||||||
|
externalNumber,
|
||||||
|
date,
|
||||||
|
createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasLines) {
|
||||||
|
try {
|
||||||
|
await runner.query(queries.DELETE_LINES_BY_MANIFEST, {
|
||||||
|
manifestId: id,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isForeignKeyConstraintError(err)) {
|
||||||
|
throw new Error('Return lines already booked on ledger');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
await insertLines(provider, tx, {
|
||||||
|
manifestId: id,
|
||||||
|
lines: normalizedLines,
|
||||||
|
ensureTax,
|
||||||
|
companyuserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncReturnSummary(runner, { companyId, manifestId: id });
|
||||||
|
return loadReturn(provider, tx, companyId, id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction((runner, tx) => run(runner, tx));
|
||||||
|
}
|
||||||
|
return run(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import makeRunner from '../runner.js';
|
|||||||
import shippingCreate from '../shipping/create.js';
|
import shippingCreate from '../shipping/create.js';
|
||||||
import { loadShipment, queriesFor } from './helpers.js';
|
import { loadShipment, queriesFor } from './helpers.js';
|
||||||
import { syncOrderSummary } from '../order/syncSummary.js';
|
import { syncOrderSummary } from '../order/syncSummary.js';
|
||||||
|
import { syncRetoureSummary } from '../retoure/syncSummary.js';
|
||||||
import { syncShipmentSummary } from './syncSummary.js';
|
import { syncShipmentSummary } from './syncSummary.js';
|
||||||
|
|
||||||
function requireId(val, name) {
|
function requireId(val, name) {
|
||||||
@@ -293,8 +294,12 @@ export default function create(provider) {
|
|||||||
// summary state ('delivered') is written, as that would block shipping creation.
|
// summary state ('delivered') is written, as that would block shipping creation.
|
||||||
await createShipping(googleAuthId, { ...input.shipping, manifestId }, innerTx);
|
await createShipping(googleAuthId, { ...input.shipping, manifestId }, innerTx);
|
||||||
|
|
||||||
// 9. Synchronize order summary row and shipment summary row
|
// 9. Synchronize order/retoure summary row and shipment summary row
|
||||||
|
if (order.type === 'retoure') {
|
||||||
|
await syncRetoureSummary(runner, { companyId, manifestId: orderId });
|
||||||
|
} else {
|
||||||
await syncOrderSummary(runner, { companyId, manifestId: orderId });
|
await syncOrderSummary(runner, { companyId, manifestId: orderId });
|
||||||
|
}
|
||||||
const validState = input.state && ['new', 'delivered'].includes(String(input.state).trim().toLowerCase())
|
const validState = input.state && ['new', 'delivered'].includes(String(input.state).trim().toLowerCase())
|
||||||
? String(input.state).trim().toLowerCase()
|
? String(input.state).trim().toLowerCase()
|
||||||
: 'new';
|
: 'new';
|
||||||
|
|||||||
Reference in New Issue
Block a user