From c94dc058efba7436b58d84aa3ea5684fb73b526d Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Wed, 2 Sep 2026 20:55:38 +0100 Subject: [PATCH] Add ensure-references script and update package.json - Introduced a new script `ensure-references.js` to ensure all documents have a `_reference` field, enhancing data integrity across models. - Updated `package.json` to include the new script in the npm scripts section for easy execution. - Refactored database utility functions to exclude additional models from audit logging, improving data privacy. - Enhanced various schemas with new fields and methods for better tax calculations and inventory management. --- package.json | 3 +- scripts/ensure-references.js | 104 ++++++++++++++++++ src/database/database.js | 3 +- .../stockQuantity.recalculate.test.js | 5 +- .../schemas/finance/invoice.schema.js | 32 +++++- .../schemas/inventory/filamentstock.schema.js | 10 +- .../schemas/inventory/orderitem.schema.js | 12 +- .../schemas/inventory/shipment.schema.js | 20 +--- .../management/courierservice.schema.js | 28 +++++ .../management/documentprinter.schema.js | 8 ++ .../schemas/management/filament.schema.js | 21 ++++ .../schemas/management/filamentsku.schema.js | 32 ++++++ .../schemas/management/part.schema.js | 25 +++++ .../schemas/management/partsku.schema.js | 56 ++++++++++ .../schemas/management/product.schema.js | 31 ++++++ .../schemas/management/productsku.schema.js | 56 ++++++++++ .../management/stockauditlevel.schema.js | 6 +- .../schemas/misc/objectview.schema.js | 56 ++++++++++ src/database/schemas/models.js | 2 + src/database/tax.js | 54 +++++++++ src/database/utils.js | 2 + 21 files changed, 528 insertions(+), 38 deletions(-) create mode 100644 scripts/ensure-references.js create mode 100644 src/database/schemas/misc/objectview.schema.js create mode 100644 src/database/tax.js diff --git a/package.json b/package.json index 44093f0..a4eb3a0 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "lint:fix": "eslint src/ --fix", "format": "prettier --write \"src/**/*.{js,json}\"", "format:check": "prettier --check \"src/**/*.{js,json}\"", - "fix": "npm run lint:fix && npm run format" + "fix": "npm run lint:fix && npm run format", + "ensure-references": "node scripts/ensure-references.js" }, "author": "Tom Butcher", "license": "ISC", diff --git a/scripts/ensure-references.js b/scripts/ensure-references.js new file mode 100644 index 0000000..c2fcdc3 --- /dev/null +++ b/scripts/ensure-references.js @@ -0,0 +1,104 @@ +import { editObject } from '../src/database/database.js'; +import { generateId } from '../src/database/utils.js'; +import { models } from '../src/database/schemas/models.js'; +import { mongoServer } from '../src/database/mongo.js'; +import { natsServer } from '../src/database/nats.js'; +import { redisServer } from '../src/database/redis.js'; + +const MISSING_REFERENCE_FILTER = { + $or: [ + { _reference: { $exists: false } }, + { _reference: null }, + { _reference: '' }, + ], +}; + +const dryRun = process.argv.includes('--dry-run'); + +async function ensureReferences() { + console.log( + dryRun + ? 'Scanning for documents missing _reference (dry run)...' + : 'Ensuring all documents have a _reference...' + ); + + await mongoServer.connect(); + await natsServer.connect(); + await redisServer.connect(); + + let totalMissing = 0; + let totalUpdated = 0; + let totalFailed = 0; + + for (const [prefix, entry] of Object.entries(models)) { + const model = entry.model; + if (!model?.schema?.path('_reference')) { + continue; + } + + const missing = await model.find(MISSING_REFERENCE_FILTER).select('_id').lean(); + if (missing.length === 0) { + continue; + } + + console.log(`[${prefix}] ${entry.label}: ${missing.length} missing`); + totalMissing += missing.length; + + if (dryRun) { + continue; + } + + for (const doc of missing) { + const _reference = generateId()(); + const result = await editObject({ + model, + id: doc._id, + updateData: { _reference }, + auditLog: false, + notify: false, + recalculate: false, + }); + + if (result?.error) { + totalFailed += 1; + console.error( + ` Failed ${doc._id}: ${result.error}${result.code ? ` (${result.code})` : ''}` + ); + continue; + } + + totalUpdated += 1; + console.log(` ${doc._id} -> ${_reference}`); + } + } + + console.log( + dryRun + ? `Done. ${totalMissing} document(s) missing _reference.` + : `Done. ${totalUpdated} updated, ${totalFailed} failed, ${totalMissing} found.` + ); +} + +try { + await ensureReferences(); +} catch (error) { + console.error('ensure-references failed:', error); + process.exitCode = 1; +} finally { + try { + await redisServer.disconnect(); + } catch { + // ignore disconnect errors on shutdown + } + try { + await mongoServer.disconnect(); + } catch { + // ignore disconnect errors on shutdown + } + try { + await natsServer.disconnect(); + } catch { + // ignore disconnect errors on shutdown + } + process.exit(process.exitCode ?? 0); +} diff --git a/src/database/database.js b/src/database/database.js index 3585920..995d8eb 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -564,7 +564,8 @@ export const editObject = async ({ ownerType != undefined && parentType !== 'notification' && parentType !== 'auditLog' && - parentType !== 'userNotifier' + parentType !== 'userNotifier' && + parentType !== 'objectView' ) { await editNotification( previousExpandedObject, diff --git a/src/database/schemas/__tests__/stockQuantity.recalculate.test.js b/src/database/schemas/__tests__/stockQuantity.recalculate.test.js index 94bad77..9f808c4 100644 --- a/src/database/schemas/__tests__/stockQuantity.recalculate.test.js +++ b/src/database/schemas/__tests__/stockQuantity.recalculate.test.js @@ -24,11 +24,10 @@ jest.unstable_mockModule('../../utils.js', () => ({ })); jest.unstable_mockModule('../inventory/stockaudit.schema.js', () => ({ - updateDraftStockAuditCurrents: jest.fn(), + updateDraftStockAuditCurrents: jest.fn().mockResolvedValue(), })); const { aggregateRollups, editObject, newObject, deleteObject } = await import('../../database.js'); -const { updateDraftStockAuditCurrents } = await import('../inventory/stockaudit.schema.js'); const { listingModel } = await import('../sales/listing.schema.js'); const { listingVarientModel } = await import('../sales/listingvarient.schema.js'); const { productSkuModel } = await import('../management/productsku.schema.js'); @@ -370,9 +369,7 @@ describe('productStock.recalculate', () => { beforeEach(() => { aggregateRollups.mockReset(); editObject.mockReset(); - updateDraftStockAuditCurrents.mockReset(); jest.restoreAllMocks(); - updateDraftStockAuditCurrents.mockResolvedValue(undefined); }); it('writes the sku/location total onto matching listing varients', async () => { diff --git a/src/database/schemas/finance/invoice.schema.js b/src/database/schemas/finance/invoice.schema.js index bd9eea4..954d09a 100644 --- a/src/database/schemas/finance/invoice.schema.js +++ b/src/database/schemas/finance/invoice.schema.js @@ -1,7 +1,9 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; -import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js'; +import { aggregateRollups, aggregateRollupsHistory, editObject, getObject } from '../../database.js'; +import { taxRateModel } from '../management/taxrate.schema.js'; +import { amountWithTax, resolveTaxRate } from '../../tax.js'; const invoiceOrderItemSchema = new Schema( { @@ -140,23 +142,41 @@ invoiceSchema.statics.recalculate = async function (invoice, user) { return; } + const invoiceOrderItems = []; + for (const item of invoice.invoiceOrderItems || []) { + const taxRate = await resolveTaxRate(item.taxRate, getObject, taxRateModel); + invoiceOrderItems.push({ + ...item, + invoiceAmountWithTax: amountWithTax(item.invoiceAmount, taxRate), + }); + } + + const invoiceShipments = []; + for (const item of invoice.invoiceShipments || []) { + const taxRate = await resolveTaxRate(item.taxRate, getObject, taxRateModel); + invoiceShipments.push({ + ...item, + invoiceAmountWithTax: amountWithTax(item.invoiceAmount, taxRate), + }); + } + // Calculate totals from invoiceOrderItems let totalAmount = 0; - for (const item of invoice.invoiceOrderItems || []) { + for (const item of invoiceOrderItems) { totalAmount += Number.parseFloat(item.invoiceAmount) || 0; } let totalAmountWithTax = 0; - for (const item of invoice.invoiceOrderItems || []) { + for (const item of invoiceOrderItems) { totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0; } // Calculate shipping totals from invoiceShipments let shippingAmount = 0; - for (const item of invoice.invoiceShipments || []) { + for (const item of invoiceShipments) { shippingAmount += Number.parseFloat(item.invoiceAmount) || 0; } let shippingAmountWithTax = 0; - for (const item of invoice.invoiceShipments || []) { + for (const item of invoiceShipments) { shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0; } @@ -168,6 +188,8 @@ invoiceSchema.statics.recalculate = async function (invoice, user) { (parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount)); const updateData = { + invoiceOrderItems, + invoiceShipments, totalAmount: parseFloat(totalAmount).toFixed(2), totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2), shippingAmount: parseFloat(shippingAmount).toFixed(2), diff --git a/src/database/schemas/inventory/filamentstock.schema.js b/src/database/schemas/inventory/filamentstock.schema.js index 1c42010..4033d94 100644 --- a/src/database/schemas/inventory/filamentstock.schema.js +++ b/src/database/schemas/inventory/filamentstock.schema.js @@ -15,9 +15,10 @@ const filamentStockSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, state: { - type: { type: String, required: true }, + type: { type: String, required: true, default: 'draft' }, progress: { type: Number, required: false }, }, + postedAt: { type: Date, required: false }, startingWeight: { net: { type: Number, required: true }, gross: { type: Number, required: true }, @@ -65,6 +66,11 @@ const rollupConfigs = [ filter: {}, rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }], }, + { + name: 'draft', + filter: { 'state.type': 'draft' }, + rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }], + }, { name: 'unconsumed', filter: { 'state.type': 'unconsumed' }, @@ -114,6 +120,8 @@ filamentStockSchema.statics.recalculate = async function (filamentStock, user) { stockLocationId, user, }); + + if (filamentStock.state?.type === 'draft' || filamentStock.state?.type === 'consumed') return; }; // Add virtual id getter diff --git a/src/database/schemas/inventory/orderitem.schema.js b/src/database/schemas/inventory/orderitem.schema.js index 7286d1f..f93d0da 100644 --- a/src/database/schemas/inventory/orderitem.schema.js +++ b/src/database/schemas/inventory/orderitem.schema.js @@ -15,6 +15,7 @@ import { getObject, } from '../../database.js'; import { generateId } from '../../utils.js'; +import { amountWithTax, resolveTaxRate } from '../../tax.js'; const { Schema } = mongoose; const skuModelsByItemType = { @@ -194,17 +195,10 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) { } } - let taxRate = orderItem.taxRate; - if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length === 1) { - taxRate = await getObject({ - model: taxRateModel, - id: orderItem.taxRate._id, - cached: true, - }); - } + const taxRate = await resolveTaxRate(orderItem.taxRate, getObject, taxRateModel); const orderTotalAmount = effectiveItemAmount * orderItem.quantity; - const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100); + const orderTotalAmountWithTax = amountWithTax(orderTotalAmount, taxRate); const orderItemUpdateData = { totalAmount: orderTotalAmount, diff --git a/src/database/schemas/inventory/shipment.schema.js b/src/database/schemas/inventory/shipment.schema.js index 95f9beb..8f63b69 100644 --- a/src/database/schemas/inventory/shipment.schema.js +++ b/src/database/schemas/inventory/shipment.schema.js @@ -10,6 +10,7 @@ import { editObject, getObject, } from '../../database.js'; +import { amountWithTax, resolveTaxRate } from '../../tax.js'; const shipmentSchema = new Schema( { @@ -102,26 +103,17 @@ shipmentSchema.statics.recalculate = async function (shipment, user) { return; } - var taxRate = shipment.taxRate; + const taxRate = await resolveTaxRate(shipment.taxRate, getObject, taxRateModel); - if (shipment.taxRate?._id && Object.keys(shipment.taxRate).length == 1) { - taxRate = await getObject({ - model: taxRateModel, - id: shipment.taxRate._id, - cached: true, - }); - } - - const amountWithTax = parseFloat( - (shipment.amount || 0) * (1 + (taxRate?.rate || 0) / 100) - ).toFixed(2); + const amountWithTaxValue = amountWithTax(shipment.amount || 0, taxRate); await editObject({ model: shipmentModel, id: shipment._id, updateData: { - amountWithTax: amountWithTax, + amountWithTax: amountWithTaxValue, invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0), - invoicedAmountWithTaxRemaining: amountWithTax - (shipment.invoicedAmountWithTax || 0), + invoicedAmountWithTaxRemaining: + amountWithTaxValue - (shipment.invoicedAmountWithTax || 0), }, user, recalculate: false, diff --git a/src/database/schemas/management/courierservice.schema.js b/src/database/schemas/management/courierservice.schema.js index 73e990a..800406d 100644 --- a/src/database/schemas/management/courierservice.schema.js +++ b/src/database/schemas/management/courierservice.schema.js @@ -1,5 +1,8 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; +import { taxRateModel } from './taxrate.schema.js'; +import { editObject, getObject } from '../../database.js'; +import { amountWithTax, resolveTaxRate } from '../../tax.js'; const { Schema } = mongoose; const marketplaceMappingSchema = new mongoose.Schema( @@ -39,4 +42,29 @@ courierServiceSchema.virtual('id').get(function () { courierServiceSchema.set('toJSON', { virtuals: true }); +courierServiceSchema.statics.recalculate = async function (courierService, user) { + const costTaxRate = await resolveTaxRate(courierService.costTaxRate, getObject, taxRateModel); + const updateData = {}; + + if (courierService.cost != null) { + updateData.costWithTax = amountWithTax(courierService.cost, costTaxRate); + } + if (courierService.additionalCost != null) { + updateData.additionalCostWithTax = amountWithTax( + courierService.additionalCost, + costTaxRate + ); + } + + if (Object.keys(updateData).length > 0) { + await editObject({ + model: this, + id: courierService._id, + updateData, + user, + recalculate: false, + }); + } +}; + export const courierServiceModel = mongoose.model('courierService', courierServiceSchema); diff --git a/src/database/schemas/management/documentprinter.schema.js b/src/database/schemas/management/documentprinter.schema.js index 09d9be0..70f1325 100644 --- a/src/database/schemas/management/documentprinter.schema.js +++ b/src/database/schemas/management/documentprinter.schema.js @@ -8,6 +8,8 @@ const connectionSchema = new Schema( protocol: { type: String, required: true }, host: { type: String, required: true }, port: { type: Number, required: false }, + username: { type: String, required: false }, + password: { type: String, required: false }, }, { _id: false } ); @@ -22,6 +24,8 @@ const documentPrinterSchema = new Schema( }, connection: { type: connectionSchema, required: true }, currentDocumentSize: { type: Schema.Types.ObjectId, ref: 'documentSize', required: false }, + supportedDocumentSizes: [{ type: Schema.Types.ObjectId, ref: 'documentSize', required: false }], + rotateOrientation: { type: Boolean, required: false, default: false }, tags: [{ type: String }], online: { type: Boolean, required: true, default: false }, active: { type: Boolean, required: true, default: true }, @@ -30,6 +34,10 @@ const documentPrinterSchema = new Schema( message: { type: String, required: false }, progress: { type: Number, required: false }, }, + paperState: { + type: { type: String, required: true, default: 'unknown' }, + message: { type: String, required: false }, + }, connectedAt: { type: Date, default: null }, host: { type: Schema.Types.ObjectId, ref: 'host', required: true }, vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false }, diff --git a/src/database/schemas/management/filament.schema.js b/src/database/schemas/management/filament.schema.js index ea53d0b..8a66537 100644 --- a/src/database/schemas/management/filament.schema.js +++ b/src/database/schemas/management/filament.schema.js @@ -1,5 +1,8 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; +import { taxRateModel } from '../management/taxrate.schema.js'; +import { editObject, getObject } from '../../database.js'; +import { amountWithTax, resolveTaxRate } from '../../tax.js'; const { Schema } = mongoose; // Filament base - cost and tax; color and cost override at FilamentSKU @@ -28,6 +31,24 @@ filamentSchema.virtual('id').get(function () { filamentSchema.set('toJSON', { virtuals: true }); filamentSchema.statics.recalculate = async function (filament, user) { + const costTaxRate = await resolveTaxRate(filament.costTaxRate, getObject, taxRateModel); + const taxUpdateData = {}; + + if (filament.cost != null) { + taxUpdateData.costWithTax = amountWithTax(filament.cost, costTaxRate); + } + + if (Object.keys(taxUpdateData).length > 0) { + await editObject({ + model: this, + id: filament._id, + updateData: taxUpdateData, + user, + recalculate: false, + }); + Object.assign(filament, taxUpdateData); + } + const filamentSkuModel = mongoose.model('filamentSku'); const skus = await filamentSkuModel.find({ filament: filament._id }).select('_id').lean(); for (const sku of skus) { diff --git a/src/database/schemas/management/filamentsku.schema.js b/src/database/schemas/management/filamentsku.schema.js index 97e995d..60646c1 100644 --- a/src/database/schemas/management/filamentsku.schema.js +++ b/src/database/schemas/management/filamentsku.schema.js @@ -1,5 +1,9 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; +import { filamentModel } from './filament.schema.js'; +import { taxRateModel } from './taxrate.schema.js'; +import { editObject, getObject } from '../../database.js'; +import { amountWithTax, resolveTaxRate } from '../../tax.js'; const { Schema } = mongoose; // Define the main filament SKU schema - color and cost live at SKU level @@ -30,6 +34,34 @@ filamentSkuSchema.virtual('id').get(function () { filamentSkuSchema.set('toJSON', { virtuals: true }); filamentSkuSchema.statics.recalculate = async function (filamentSku, user) { + const parent = await getObject({ + model: filamentModel, + id: filamentSku.filament?._id || filamentSku.filament, + cached: true, + }); + + const taxUpdateData = {}; + + if (filamentSku.overrideCost) { + const costTaxRate = await resolveTaxRate(filamentSku.costTaxRate, getObject, taxRateModel); + if (filamentSku.cost != null) { + taxUpdateData.costWithTax = amountWithTax(filamentSku.cost, costTaxRate); + } + } else if (parent?.costWithTax != null) { + taxUpdateData.costWithTax = parent.costWithTax; + } + + if (Object.keys(taxUpdateData).length > 0) { + await editObject({ + model: this, + id: filamentSku._id, + updateData: taxUpdateData, + user, + recalculate: false, + }); + Object.assign(filamentSku, taxUpdateData); + } + const orderItemModel = mongoose.model('orderItem'); const skuId = filamentSku._id; const draftOrderItems = await orderItemModel diff --git a/src/database/schemas/management/part.schema.js b/src/database/schemas/management/part.schema.js index 682f6e7..1456228 100644 --- a/src/database/schemas/management/part.schema.js +++ b/src/database/schemas/management/part.schema.js @@ -1,5 +1,8 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; +import { taxRateModel } from '../management/taxrate.schema.js'; +import { editObject, getObject } from '../../database.js'; +import { amountWithTax, resolveTaxRate } from '../../tax.js'; const { Schema } = mongoose; // Define the main part schema - cost/price and tax; override at PartSku @@ -32,6 +35,28 @@ partSchema.virtual('id').get(function () { partSchema.set('toJSON', { virtuals: true }); partSchema.statics.recalculate = async function (part, user) { + const costTaxRate = await resolveTaxRate(part.costTaxRate, getObject, taxRateModel); + const priceTaxRate = await resolveTaxRate(part.priceTaxRate, getObject, taxRateModel); + const taxUpdateData = {}; + + if (part.cost != null) { + taxUpdateData.costWithTax = amountWithTax(part.cost, costTaxRate); + } + if (part.price != null) { + taxUpdateData.priceWithTax = amountWithTax(part.price, priceTaxRate); + } + + if (Object.keys(taxUpdateData).length > 0) { + await editObject({ + model: this, + id: part._id, + updateData: taxUpdateData, + user, + recalculate: false, + }); + Object.assign(part, taxUpdateData); + } + const partSkuModel = mongoose.model('partSku'); const skus = await partSkuModel.find({ part: part._id }).select('_id').lean(); for (const sku of skus) { diff --git a/src/database/schemas/management/partsku.schema.js b/src/database/schemas/management/partsku.schema.js index 02c443e..ed7a1fd 100644 --- a/src/database/schemas/management/partsku.schema.js +++ b/src/database/schemas/management/partsku.schema.js @@ -1,5 +1,13 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; +import { partModel } from './part.schema.js'; +import { taxRateModel } from './taxrate.schema.js'; +import { editObject, getObject } from '../../database.js'; +import { + amountWithTax, + effectiveMarginPrice, + resolveTaxRate, +} from '../../tax.js'; const { Schema } = mongoose; // Define the main part SKU schema - pricing lives at SKU level @@ -36,6 +44,54 @@ partSkuSchema.virtual('id').get(function () { partSkuSchema.set('toJSON', { virtuals: true }); partSkuSchema.statics.recalculate = async function (partSku, user) { + const parent = await getObject({ + model: partModel, + id: partSku.part?._id || partSku.part, + cached: true, + }); + + const taxUpdateData = {}; + + if (partSku.overrideCost) { + const costTaxRate = await resolveTaxRate(partSku.costTaxRate, getObject, taxRateModel); + if (partSku.cost != null) { + taxUpdateData.costWithTax = amountWithTax(partSku.cost, costTaxRate); + } + } else if (parent?.costWithTax != null) { + taxUpdateData.costWithTax = parent.costWithTax; + } + + if (partSku.overridePrice) { + const priceTaxRate = await resolveTaxRate( + partSku.priceTaxRate ?? parent?.priceTaxRate, + getObject, + taxRateModel + ); + const cost = partSku.overrideCost ? partSku.cost : parent?.cost; + const price = effectiveMarginPrice({ + priceMode: partSku.priceMode ?? parent?.priceMode, + price: partSku.price, + cost, + margin: partSku.margin ?? parent?.margin, + }); + if (price != null) { + taxUpdateData.priceWithTax = amountWithTax(price, priceTaxRate); + } + } else if (parent?.priceWithTax != null) { + taxUpdateData.priceWithTax = parent.priceWithTax; + } + + if (Object.keys(taxUpdateData).length > 0) { + await editObject({ + model: this, + id: partSku._id, + updateData: taxUpdateData, + user, + recalculate: false, + }); + Object.assign(partSku, taxUpdateData); + } + const orderItemModel = mongoose.model('orderItem'); const skuId = partSku._id; const draftOrderItems = await orderItemModel diff --git a/src/database/schemas/management/product.schema.js b/src/database/schemas/management/product.schema.js index 09e9813..9e1cb9d 100644 --- a/src/database/schemas/management/product.schema.js +++ b/src/database/schemas/management/product.schema.js @@ -1,5 +1,8 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; +import { taxRateModel } from '../management/taxrate.schema.js'; +import { editObject, getObject } from '../../database.js'; +import { amountWithTax, resolveTaxRate } from '../../tax.js'; const { Schema } = mongoose; // Define the main product schema @@ -35,6 +38,34 @@ productSchema.virtual('id').get(function () { productSchema.set('toJSON', { virtuals: true }); productSchema.statics.recalculate = async function (product, user) { + const costTaxRate = await resolveTaxRate(product.costTaxRate, getObject, taxRateModel); + const priceTaxRate = await resolveTaxRate(product.priceTaxRate, getObject, taxRateModel); + const taxUpdateData = {}; + + if (product.cost != null) { + taxUpdateData.costWithTax = amountWithTax(product.cost, costTaxRate); + } + if (product.price != null) { + taxUpdateData.priceWithTax = amountWithTax(product.price, priceTaxRate); + } + + if (Object.keys(taxUpdateData).length > 0) { + await editObject({ + model: this, + id: product._id, + updateData: taxUpdateData, + user, + recalculate: false, + }); + Object.assign(product, taxUpdateData); + } + + const productSkuModel = mongoose.model('productSku'); + const skus = await productSkuModel.find({ product: product._id }).select('_id').lean(); + for (const sku of skus) { + await productSkuModel.recalculate(sku, user); + } + const orderItemModel = mongoose.model('orderItem'); const itemId = product._id; const draftOrderItems = await orderItemModel diff --git a/src/database/schemas/management/productsku.schema.js b/src/database/schemas/management/productsku.schema.js index 59ac71e..49c2696 100644 --- a/src/database/schemas/management/productsku.schema.js +++ b/src/database/schemas/management/productsku.schema.js @@ -1,5 +1,13 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; +import { productModel } from './product.schema.js'; +import { taxRateModel } from './taxrate.schema.js'; +import { editObject, getObject } from '../../database.js'; +import { + amountWithTax, + effectiveMarginPrice, + resolveTaxRate, +} from '../../tax.js'; const { Schema } = mongoose; const partSkuUsageSchema = new Schema({ @@ -43,6 +51,54 @@ productSkuSchema.virtual('id').get(function () { productSkuSchema.set('toJSON', { virtuals: true }); productSkuSchema.statics.recalculate = async function (productSku, user) { + const parent = await getObject({ + model: productModel, + id: productSku.product?._id || productSku.product, + cached: true, + }); + + const taxUpdateData = {}; + + if (productSku.overrideCost) { + const costTaxRate = await resolveTaxRate(productSku.costTaxRate, getObject, taxRateModel); + if (productSku.cost != null) { + taxUpdateData.costWithTax = amountWithTax(productSku.cost, costTaxRate); + } + } else if (parent?.costWithTax != null) { + taxUpdateData.costWithTax = parent.costWithTax; + } + + if (productSku.overridePrice) { + const priceTaxRate = await resolveTaxRate( + productSku.priceTaxRate ?? parent?.priceTaxRate, + getObject, + taxRateModel + ); + const cost = productSku.overrideCost ? productSku.cost : parent?.cost; + const price = effectiveMarginPrice({ + priceMode: productSku.priceMode ?? parent?.priceMode, + price: productSku.price, + cost, + margin: productSku.margin ?? parent?.margin, + }); + if (price != null) { + taxUpdateData.priceWithTax = amountWithTax(price, priceTaxRate); + } + } else if (parent?.priceWithTax != null) { + taxUpdateData.priceWithTax = parent.priceWithTax; + } + + if (Object.keys(taxUpdateData).length > 0) { + await editObject({ + model: this, + id: productSku._id, + updateData: taxUpdateData, + user, + recalculate: false, + }); + Object.assign(productSku, taxUpdateData); + } + const orderItemModel = mongoose.model('orderItem'); const skuId = productSku._id; const draftOrderItems = await orderItemModel diff --git a/src/database/schemas/management/stockauditlevel.schema.js b/src/database/schemas/management/stockauditlevel.schema.js index 5fe3c06..91f647b 100644 --- a/src/database/schemas/management/stockauditlevel.schema.js +++ b/src/database/schemas/management/stockauditlevel.schema.js @@ -13,8 +13,8 @@ const toId = (value) => { export function normalizeAuditLevelLine(line) { const allItems = Boolean(line?.allItems); const allSkus = allItems ? true : Boolean(line?.allSkus); - const item = allItems ? null : line?.item?._id ?? line?.item ?? null; - const itemSku = allItems || allSkus ? null : line?.itemSku?._id ?? line?.itemSku ?? null; + const item = allItems ? null : (line?.item?._id ?? line?.item ?? null); + const itemSku = allItems || allSkus ? null : (line?.itemSku?._id ?? line?.itemSku ?? null); return { _id: line?._id, @@ -62,7 +62,7 @@ const stockAuditLevelSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, name: { type: String, required: true }, - tags: [{ type: String }], + tags: [{ type: String, required: true }], auditLines: { type: [stockAuditLevelLineSchema], default: [] }, }, { timestamps: true } diff --git a/src/database/schemas/misc/objectview.schema.js b/src/database/schemas/misc/objectview.schema.js new file mode 100644 index 0000000..af64771 --- /dev/null +++ b/src/database/schemas/misc/objectview.schema.js @@ -0,0 +1,56 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +const { Schema } = mongoose; + +const objectViewSchema = new mongoose.Schema({ + _reference: { type: String, default: () => generateId()() }, + user: { + type: Schema.Types.ObjectId, + ref: 'user', + required: true, + }, + objectType: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + }, + color: { + type: String, + required: true, + default: '#3498DB', + }, + private: { + type: Boolean, + required: true, + default: true, + }, + filter: { + type: Schema.Types.Mixed, + default: () => ({}), + }, + sort: { + type: Schema.Types.Mixed, + default: () => ({}), + }, + createdAt: { + type: Date, + required: true, + default: Date.now, + }, + updatedAt: { + type: Date, + required: true, + default: Date.now, + }, +}); + +objectViewSchema.virtual('id').get(function () { + return this._id; +}); + +objectViewSchema.set('toJSON', { virtuals: true }); + +export const objectViewModel = mongoose.model('objectView', objectViewSchema); diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 2cb8d25..9c67b04 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -32,6 +32,7 @@ import { noteTypeModel } from './management/notetype.schema.js'; import { noteModel } from './misc/note.schema.js'; import { notificationModel } from './misc/notification.schema.js'; import { userNotifierModel } from './misc/usernotifier.schema.js'; +import { objectViewModel } from './misc/objectview.schema.js'; import { documentSizeModel } from './management/documentsize.schema.js'; import { documentTemplateModel } from './management/documenttemplate.schema.js'; import { hostModel } from './management/host.schema.js'; @@ -103,6 +104,7 @@ export const models = { NTE: modelEntry(() => noteModel, 'note', 'Note'), NTF: modelEntry(() => notificationModel, 'notification', 'Notification'), ONF: modelEntry(() => userNotifierModel, 'userNotifier', 'User Notifier'), + OVW: modelEntry(() => objectViewModel, 'objectView', 'Object View'), DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'), DTP: modelEntry(() => documentTemplateModel, 'documentTemplate', 'Document Template'), DPR: modelEntry(() => documentPrinterModel, 'documentPrinter', 'Document Printer'), diff --git a/src/database/tax.js b/src/database/tax.js new file mode 100644 index 0000000..078094b --- /dev/null +++ b/src/database/tax.js @@ -0,0 +1,54 @@ +/** + * Tax calculation helpers mirroring farmcontrol-ui model value functions. + */ + +export function isPopulatedTaxRate(taxRate) { + return taxRate != null && taxRate.rateType != null; +} + +export async function resolveTaxRate(taxRateRef, getObject, taxRateModel) { + if (!taxRateRef) { + return null; + } + if (isPopulatedTaxRate(taxRateRef)) { + return taxRateRef; + } + const id = taxRateRef._id ?? taxRateRef; + if (!id) { + return null; + } + if ( + typeof taxRateRef === 'object' && + taxRateRef._id && + Object.keys(taxRateRef).length === 1 + ) { + return await getObject({ model: taxRateModel, id, cached: true }); + } + return await getObject({ model: taxRateModel, id, cached: true }); +} + +export function amountWithTax(amount, taxRate) { + const base = Number.parseFloat(amount) || 0; + if (!base) { + return 0; + } + if (!taxRate) { + return Number.parseFloat(base.toFixed(2)); + } + + const rate = Number.parseFloat(taxRate.rate) || 0; + if (taxRate.rateType === 'percentage') { + return Number.parseFloat((base * (1 + rate / 100)).toFixed(2)); + } + if (taxRate.rateType === 'amount' || taxRate.rateType === 'fixed') { + return Number.parseFloat((base + rate).toFixed(2)); + } + return Number.parseFloat(base.toFixed(2)); +} + +export function effectiveMarginPrice({ priceMode, price, cost, margin }) { + if (priceMode === 'margin' && margin != null && cost != null) { + return cost * (1 + margin / 100); + } + return price; +} diff --git a/src/database/utils.js b/src/database/utils.js index 927c201..4254d3c 100644 --- a/src/database/utils.js +++ b/src/database/utils.js @@ -9,11 +9,13 @@ import { customAlphabet } from 'nanoid'; const NOTIFICATION_EXCLUDED_MODELS = [ 'notification', 'userNotifier', + 'objectView', 'auditLog' ]; const AUDIT_EXCLUDED_MODELS = [ 'notification', 'userNotifier', + 'objectView', 'marketplaceEvent' ]; const AUDIT_EXCLUDED_CHANGES = ['state.message'];