Compare commits

...

4 Commits

Author SHA1 Message Date
850c721867 Refactor allowed filters and sorters across various routes to enhance data handling
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit updates the allowed filters and sorters in multiple routes, including invoices, payment policies, inventory, management, and sales. It removes unnecessary filters, adds new ones for improved categorization, and ensures consistency in the data structure. These changes aim to streamline data retrieval and enhance the overall functionality of the application.
2026-09-01 19:54:44 +01:00
a7b613c88b Implement model prefix stripping and leading operator handling in utility functions
This commit introduces new functions to handle model prefix stripping and leading operator detection in the `utils.js` file. The `stripModelPrefixFromExpression` and `splitLeadingOperator` functions are added to enhance expression parsing. Additionally, existing functions are updated to utilize these new utilities, improving the handling of expressions in filters. Corresponding tests are added to ensure the correctness of the new functionality, particularly for equality and not-equal expressions.
2026-09-01 18:44:38 +01:00
1f679825df Add tax calculation helpers and integrate into existing schemas
This commit introduces a new `tax.js` module containing functions for tax rate resolution and amount calculation with tax. The `amountWithTax` and `resolveTaxRate` functions are integrated into various schemas, including `invoice`, `orderitem`, `shipment`, `courierservice`, `filament`, `part`, and `product`, enhancing their ability to compute amounts with applicable taxes. Additionally, corresponding tests are added to ensure the correctness of the new tax-related functionalities.
2026-09-01 17:39:12 +01:00
edb8282c41 Enhance filament stock management by adding default state and posting functionality
This commit updates the filament stock schema to set a default state of 'draft' and introduces a new route handler for posting filament stocks. The posting process includes validation to ensure stocks are in the draft state before transitioning to 'unconsumed', along with the creation of an initial stock event. Additionally, the service layer is updated to handle these changes, and tests are added to verify the new functionality.
2026-09-01 17:26:10 +01:00
36 changed files with 723 additions and 120 deletions

View File

@ -38,4 +38,16 @@ describe('parseFilter number fields', () => {
const result = await parseFilter('_reference', 'JOB*', jobModel); const result = await parseFilter('_reference', 'JOB*', jobModel);
expect(result._reference).toEqual({ $regex: '^JOB.*$', $options: 'i' }); expect(result._reference).toEqual({ $regex: '^JOB.*$', $options: 'i' });
}); });
it('strips model prefix from equality expressions', async () => {
await expect(parseFilter('_reference', 'GCF:EJX3EXUZYW2U', jobModel)).resolves.toEqual({
_reference: { $regex: '^EJX3EXUZYW2U$', $options: 'i' },
});
});
it('strips model prefix from not-equal expressions', async () => {
await expect(parseFilter('_reference', '<>GCF:EJX3EXUZYW2U', jobModel)).resolves.toEqual({
_reference: { $ne: 'EJX3EXUZYW2U' },
});
});
}); });

View File

@ -0,0 +1,37 @@
import { amountWithTax, effectiveMarginPrice } from '../tax.js';
describe('amountWithTax', () => {
it('returns 0 for zero amount', () => {
expect(amountWithTax(0, { rateType: 'percentage', rate: 20 })).toBe(0);
});
it('applies percentage tax', () => {
expect(amountWithTax(100, { rateType: 'percentage', rate: 20 })).toBe(120);
});
it('applies fixed amount tax', () => {
expect(amountWithTax(100, { rateType: 'amount', rate: 5 })).toBe(105);
});
it('applies fixed rate type from API schema', () => {
expect(amountWithTax(100, { rateType: 'fixed', rate: 5 })).toBe(105);
});
it('returns base amount when tax rate is missing', () => {
expect(amountWithTax(100, null)).toBe(100);
});
});
describe('effectiveMarginPrice', () => {
it('computes price from margin mode', () => {
expect(
effectiveMarginPrice({ priceMode: 'margin', price: 50, cost: 100, margin: 20 })
).toBe(120);
});
it('returns explicit price for amount mode', () => {
expect(
effectiveMarginPrice({ priceMode: 'amount', price: 150, cost: 100, margin: 20 })
).toBe(150);
});
});

View File

@ -1,7 +1,9 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; 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( const invoiceOrderItemSchema = new Schema(
{ {
@ -140,23 +142,41 @@ invoiceSchema.statics.recalculate = async function (invoice, user) {
return; 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 // Calculate totals from invoiceOrderItems
let totalAmount = 0; let totalAmount = 0;
for (const item of invoice.invoiceOrderItems || []) { for (const item of invoiceOrderItems) {
totalAmount += Number.parseFloat(item.invoiceAmount) || 0; totalAmount += Number.parseFloat(item.invoiceAmount) || 0;
} }
let totalAmountWithTax = 0; let totalAmountWithTax = 0;
for (const item of invoice.invoiceOrderItems || []) { for (const item of invoiceOrderItems) {
totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0; totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
} }
// Calculate shipping totals from invoiceShipments // Calculate shipping totals from invoiceShipments
let shippingAmount = 0; let shippingAmount = 0;
for (const item of invoice.invoiceShipments || []) { for (const item of invoiceShipments) {
shippingAmount += Number.parseFloat(item.invoiceAmount) || 0; shippingAmount += Number.parseFloat(item.invoiceAmount) || 0;
} }
let shippingAmountWithTax = 0; let shippingAmountWithTax = 0;
for (const item of invoice.invoiceShipments || []) { for (const item of invoiceShipments) {
shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0; shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
} }
@ -168,6 +188,8 @@ invoiceSchema.statics.recalculate = async function (invoice, user) {
(parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount)); (parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount));
const updateData = { const updateData = {
invoiceOrderItems,
invoiceShipments,
totalAmount: parseFloat(totalAmount).toFixed(2), totalAmount: parseFloat(totalAmount).toFixed(2),
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2), totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
shippingAmount: parseFloat(shippingAmount).toFixed(2), shippingAmount: parseFloat(shippingAmount).toFixed(2),

View File

@ -15,9 +15,10 @@ const filamentStockSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
state: { state: {
type: { type: String, required: true }, type: { type: String, required: true, default: 'draft' },
progress: { type: Number, required: false }, progress: { type: Number, required: false },
}, },
postedAt: { type: Date, required: false },
startingWeight: { startingWeight: {
net: { type: Number, required: true }, net: { type: Number, required: true },
gross: { type: Number, required: true }, gross: { type: Number, required: true },
@ -65,6 +66,11 @@ const rollupConfigs = [
filter: {}, filter: {},
rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }], rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }],
}, },
{
name: 'draft',
filter: { 'state.type': 'draft' },
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
},
{ {
name: 'unconsumed', name: 'unconsumed',
filter: { 'state.type': 'unconsumed' }, filter: { 'state.type': 'unconsumed' },
@ -114,6 +120,8 @@ filamentStockSchema.statics.recalculate = async function (filamentStock, user) {
stockLocationId, stockLocationId,
user, user,
}); });
if (filamentStock.state?.type === 'draft' || filamentStock.state?.type === 'consumed') return;
}; };
// Add virtual id getter // Add virtual id getter

View File

@ -15,6 +15,7 @@ import {
getObject, getObject,
} from '../../database.js'; } from '../../database.js';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
const skuModelsByItemType = { const skuModelsByItemType = {
@ -194,17 +195,10 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
} }
} }
let taxRate = orderItem.taxRate; const taxRate = await resolveTaxRate(orderItem.taxRate, getObject, taxRateModel);
if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length === 1) {
taxRate = await getObject({
model: taxRateModel,
id: orderItem.taxRate._id,
cached: true,
});
}
const orderTotalAmount = effectiveItemAmount * orderItem.quantity; const orderTotalAmount = effectiveItemAmount * orderItem.quantity;
const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100); const orderTotalAmountWithTax = amountWithTax(orderTotalAmount, taxRate);
const orderItemUpdateData = { const orderItemUpdateData = {
totalAmount: orderTotalAmount, totalAmount: orderTotalAmount,

View File

@ -10,6 +10,7 @@ import {
editObject, editObject,
getObject, getObject,
} from '../../database.js'; } from '../../database.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const shipmentSchema = new Schema( const shipmentSchema = new Schema(
{ {
@ -102,26 +103,17 @@ shipmentSchema.statics.recalculate = async function (shipment, user) {
return; return;
} }
var taxRate = shipment.taxRate; const taxRate = await resolveTaxRate(shipment.taxRate, getObject, taxRateModel);
if (shipment.taxRate?._id && Object.keys(shipment.taxRate).length == 1) { const amountWithTaxValue = amountWithTax(shipment.amount || 0, taxRate);
taxRate = await getObject({
model: taxRateModel,
id: shipment.taxRate._id,
cached: true,
});
}
const amountWithTax = parseFloat(
(shipment.amount || 0) * (1 + (taxRate?.rate || 0) / 100)
).toFixed(2);
await editObject({ await editObject({
model: shipmentModel, model: shipmentModel,
id: shipment._id, id: shipment._id,
updateData: { updateData: {
amountWithTax: amountWithTax, amountWithTax: amountWithTaxValue,
invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0), invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0),
invoicedAmountWithTaxRemaining: amountWithTax - (shipment.invoicedAmountWithTax || 0), invoicedAmountWithTaxRemaining:
amountWithTaxValue - (shipment.invoicedAmountWithTax || 0),
}, },
user, user,
recalculate: false, recalculate: false,

View File

@ -1,5 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; 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 { Schema } = mongoose;
const marketplaceMappingSchema = new mongoose.Schema( const marketplaceMappingSchema = new mongoose.Schema(
@ -39,4 +42,29 @@ courierServiceSchema.virtual('id').get(function () {
courierServiceSchema.set('toJSON', { virtuals: true }); 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); export const courierServiceModel = mongoose.model('courierService', courierServiceSchema);

View File

@ -1,5 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; 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; const { Schema } = mongoose;
// Filament base - cost and tax; color and cost override at FilamentSKU // 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.set('toJSON', { virtuals: true });
filamentSchema.statics.recalculate = async function (filament, user) { 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 filamentSkuModel = mongoose.model('filamentSku');
const skus = await filamentSkuModel.find({ filament: filament._id }).select('_id').lean(); const skus = await filamentSkuModel.find({ filament: filament._id }).select('_id').lean();
for (const sku of skus) { for (const sku of skus) {

View File

@ -1,5 +1,9 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; 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; const { Schema } = mongoose;
// Define the main filament SKU schema - color and cost live at SKU level // 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.set('toJSON', { virtuals: true });
filamentSkuSchema.statics.recalculate = async function (filamentSku, user) { 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 orderItemModel = mongoose.model('orderItem');
const skuId = filamentSku._id; const skuId = filamentSku._id;
const draftOrderItems = await orderItemModel const draftOrderItems = await orderItemModel

View File

@ -1,5 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; 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; const { Schema } = mongoose;
// Define the main part schema - cost/price and tax; override at PartSku // 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.set('toJSON', { virtuals: true });
partSchema.statics.recalculate = async function (part, user) { 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 partSkuModel = mongoose.model('partSku');
const skus = await partSkuModel.find({ part: part._id }).select('_id').lean(); const skus = await partSkuModel.find({ part: part._id }).select('_id').lean();
for (const sku of skus) { for (const sku of skus) {

View File

@ -1,5 +1,13 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; 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; const { Schema } = mongoose;
// Define the main part SKU schema - pricing lives at SKU level // 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.set('toJSON', { virtuals: true });
partSkuSchema.statics.recalculate = async function (partSku, user) { 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 orderItemModel = mongoose.model('orderItem');
const skuId = partSku._id; const skuId = partSku._id;
const draftOrderItems = await orderItemModel const draftOrderItems = await orderItemModel

View File

@ -1,5 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; 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; const { Schema } = mongoose;
// Define the main product schema // Define the main product schema
@ -35,6 +38,34 @@ productSchema.virtual('id').get(function () {
productSchema.set('toJSON', { virtuals: true }); productSchema.set('toJSON', { virtuals: true });
productSchema.statics.recalculate = async function (product, user) { 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 orderItemModel = mongoose.model('orderItem');
const itemId = product._id; const itemId = product._id;
const draftOrderItems = await orderItemModel const draftOrderItems = await orderItemModel

View File

@ -1,5 +1,13 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; 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 { Schema } = mongoose;
const partSkuUsageSchema = new Schema({ const partSkuUsageSchema = new Schema({
@ -43,6 +51,54 @@ productSkuSchema.virtual('id').get(function () {
productSkuSchema.set('toJSON', { virtuals: true }); productSkuSchema.set('toJSON', { virtuals: true });
productSkuSchema.statics.recalculate = async function (productSku, user) { 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 orderItemModel = mongoose.model('orderItem');
const skuId = productSku._id; const skuId = productSku._id;
const draftOrderItems = await orderItemModel const draftOrderItems = await orderItemModel

54
src/database/tax.js Normal file
View File

@ -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;
}

View File

@ -12,10 +12,6 @@ const listAllowedFilters = [
'state', 'state',
'vendor._id', 'vendor._id',
'client._id', 'client._id',
'from',
'from._id',
'to',
'to._id',
'order', 'order',
'order._id', 'order._id',
'orderType', 'orderType',
@ -27,10 +23,6 @@ const listAllowedSorters = ['createdAt', 'state', 'updatedAt', 'invoiceDate', 'd
const propertiesAllowedFilters = [ const propertiesAllowedFilters = [
'vendor', 'vendor',
'client', 'client',
'from',
'from._id',
'to',
'to._id',
'orderType', 'orderType',
'order', 'order',
'order._id', 'order._id',

View File

@ -21,13 +21,12 @@ const router = express.Router();
const listAllowedFilters = [ const listAllowedFilters = [
'name', 'name',
'immediatePay', 'immediatePay',
'marketplaces.marketplace',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
]; ];
const listAllowedSorters = ['name', 'immediatePay', 'createdAt', '_id', 'updatedAt']; const listAllowedSorters = ['name', 'immediatePay', 'createdAt', '_id', 'updatedAt'];
const propertiesAllowedFilters = ['name', 'immediatePay', 'marketplaces.marketplace']; const propertiesAllowedFilters = ['name', 'immediatePay'];
router.get('/', isAuthenticated, checkPermissions('paymentPolicy', 'list'), async (req, res) => { router.get('/', isAuthenticated, checkPermissions('paymentPolicy', 'list'), async (req, res) => {
const { page, limit, property, search, sortProperty, sortOrder } = req.query; const { page, limit, property, search, sortProperty, sortOrder } = req.query;

View File

@ -34,6 +34,7 @@ import {
editMultipleFilamentStocksRouteHandler, editMultipleFilamentStocksRouteHandler,
newFilamentStockRouteHandler, newFilamentStockRouteHandler,
deleteFilamentStockRouteHandler, deleteFilamentStockRouteHandler,
postFilamentStockRouteHandler,
listFilamentStocksByPropertiesRouteHandler, listFilamentStocksByPropertiesRouteHandler,
getFilamentStockStatsRouteHandler, getFilamentStockStatsRouteHandler,
getFilamentStockHistoryRouteHandler, getFilamentStockHistoryRouteHandler,
@ -121,4 +122,8 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
deleteFilamentStockRouteHandler(req, res); deleteFilamentStockRouteHandler(req, res);
}); });
router.post('/:id/post', isAuthenticated, checkPermissions('filamentStock', 'post'), async (req, res) => {
postFilamentStockRouteHandler(req, res);
});
export default router; export default router;

View File

@ -6,6 +6,8 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
const router = express.Router(); const router = express.Router();
const listAllowedFilters = [ const listAllowedFilters = [
'part',
'part._id',
'partSku', 'partSku',
'partSku._id', 'partSku._id',
'state', 'state',
@ -16,7 +18,14 @@ const listAllowedFilters = [
'updatedAt', 'updatedAt',
'_reference', '_reference',
]; ];
const listAllowedSorters = ['partSku', 'currentQuantity', 'state', 'createdAt', 'updatedAt']; const listAllowedSorters = [
'part',
'partSku',
'currentQuantity',
'state',
'createdAt',
'updatedAt',
];
const propertiesAllowedFilters = ['part', 'state.type']; const propertiesAllowedFilters = ['part', 'state.type'];
import { import {
listPartStocksRouteHandler, listPartStocksRouteHandler,

View File

@ -6,6 +6,8 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
const router = express.Router(); const router = express.Router();
const listAllowedFilters = [ const listAllowedFilters = [
'product',
'product._id',
'productSku', 'productSku',
'productSku._id', 'productSku._id',
'state', 'state',
@ -16,8 +18,16 @@ const listAllowedFilters = [
'updatedAt', 'updatedAt',
'_reference', '_reference',
]; ];
const listAllowedSorters = ['productSku', 'currentQuantity', 'state', 'createdAt', 'updatedAt']; const listAllowedSorters = [
const propertiesAllowedFilters = ['productSku', 'state.type']; 'product',
'productSku',
'currentQuantity',
'state',
'createdAt',
'updatedAt',
'stockLocation',
];
const propertiesAllowedFilters = ['product', 'productSku', 'state.type'];
import { import {
listProductStocksRouteHandler, listProductStocksRouteHandler,
getProductStockRouteHandler, getProductStockRouteHandler,

View File

@ -8,6 +8,8 @@ const router = express.Router();
const listAllowedFilters = [ const listAllowedFilters = [
'state', 'state',
'state.type', 'state.type',
'fromLocation',
'toLocation',
'postedAt', 'postedAt',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',

View File

@ -15,7 +15,8 @@ const listAllowedFilters = [
'deliveryTime', 'deliveryTime',
'cost', 'cost',
'costWithTax', 'costWithTax',
'marketplaces.marketplace', 'additionalCost',
'additionalCostWithTax',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
@ -25,8 +26,11 @@ const listAllowedSorters = [
'courier', 'courier',
'active', 'active',
'tracked', 'tracked',
'deliveryTime',
'cost', 'cost',
'costWithTax', 'costWithTax',
'additionalCost',
'additionalCostWithTax',
'estimatedDeliveryTime', 'estimatedDeliveryTime',
'createdAt', 'createdAt',
'_id', '_id',
@ -42,7 +46,8 @@ const propertiesAllowedFilters = [
'deliveryTime', 'deliveryTime',
'cost', 'cost',
'costWithTax', 'costWithTax',
'marketplaces.marketplace', 'additionalCost',
'additionalCostWithTax',
]; ];
import { import {
listCourierServicesRouteHandler, listCourierServicesRouteHandler,

View File

@ -11,11 +11,20 @@ const listAllowedFilters = [
'active', 'active',
'isGlobal', 'isGlobal',
'state', 'state',
'connection.port',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
]; ];
const listAllowedSorters = ['name', 'documentSize', 'connectedAt', 'updatedAt', 'state', 'createdAt']; const listAllowedSorters = [
'name',
'documentSize',
'connectedAt',
'connection.port',
'updatedAt',
'state',
'createdAt',
];
const propertiesAllowedFilters = ['tags']; const propertiesAllowedFilters = ['tags'];
import { import {
listDocumentPrintersRouteHandler, listDocumentPrintersRouteHandler,

View File

@ -11,7 +11,10 @@ const listAllowedFilters = [
'material._id', 'material._id',
'diameter', 'diameter',
'name', 'name',
'density',
'emptySpoolWeight',
'cost', 'cost',
'costWithTax',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
@ -21,6 +24,9 @@ const listAllowedSorters = [
'createdAt', 'createdAt',
'vendor', 'vendor',
'material', 'material',
'diameter',
'density',
'emptySpoolWeight',
'cost', 'cost',
'costWithTax', 'costWithTax',
'updatedAt', 'updatedAt',

View File

@ -5,13 +5,26 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
const router = express.Router(); const router = express.Router();
const listAllowedFilters = ['product._id', '_id', 'name', 'createdAt', 'updatedAt', '_reference']; const listAllowedFilters = [
'product._id',
'_id',
'name',
'cost',
'costWithTax',
'price',
'margin',
'priceWithTax',
'createdAt',
'updatedAt',
'_reference',
];
const listAllowedSorters = [ const listAllowedSorters = [
'name', 'name',
'priceMode', 'priceMode',
'cost', 'cost',
'costWithTax', 'costWithTax',
'price', 'price',
'margin',
'priceWithTax', 'priceWithTax',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
@ -59,24 +72,15 @@ router.get('/properties', checkPermissions('part', 'list'), isAuthenticated, asy
listPartsByPropertiesRouteHandler(req, res, properties, filter, masterFilter); listPartsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
}); });
router.get( router.get('/values', checkPermissions('part', 'list'), isAuthenticated, async (req, res) => {
'/values',
checkPermissions('part', 'list'),
isAuthenticated,
async (req, res) => {
const { property } = req.query; const { property } = req.query;
const filter = await getFilter(req.query, listAllowedFilters, true); const filter = await getFilter(req.query, listAllowedFilters, true);
var masterFilter = {}; var masterFilter = {};
if (req.query.masterFilter) { if (req.query.masterFilter) {
masterFilter = await getFilter( masterFilter = await getFilter(JSON.parse(req.query.masterFilter), listAllowedFilters, true);
JSON.parse(req.query.masterFilter),
listAllowedFilters,
true
);
} }
getPartPropertyValuesRouteHandler(req, res, property, filter, masterFilter); getPartPropertyValuesRouteHandler(req, res, property, filter, masterFilter);
} });
);
router.get('/search', checkPermissions('part', 'list'), isAuthenticated, async (req, res) => { router.get('/search', checkPermissions('part', 'list'), isAuthenticated, async (req, res) => {
const { search } = req.query; const { search } = req.query;

View File

@ -12,7 +12,10 @@ const listAllowedFilters = [
'part._id', 'part._id',
'name', 'name',
'cost', 'cost',
'costWithTax',
'price', 'price',
'priceWithTax',
'margin',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
@ -25,6 +28,7 @@ const listAllowedSorters = [
'costWithTax', 'costWithTax',
'price', 'price',
'priceWithTax', 'priceWithTax',
'margin',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
]; ];

View File

@ -11,6 +11,11 @@ const listAllowedFilters = [
'globalPrice', 'globalPrice',
'productCategory', 'productCategory',
'productCategory._id', 'productCategory._id',
'cost',
'costWithTax',
'price',
'margin',
'priceWithTax',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
@ -24,6 +29,7 @@ const listAllowedSorters = [
'cost', 'cost',
'costWithTax', 'costWithTax',
'price', 'price',
'margin',
'priceWithTax', 'priceWithTax',
'updatedAt', 'updatedAt',
]; ];

View File

@ -12,7 +12,10 @@ const listAllowedFilters = [
'product._id', 'product._id',
'name', 'name',
'cost', 'cost',
'costWithTax',
'price', 'price',
'priceWithTax',
'margin',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
@ -25,6 +28,7 @@ const listAllowedSorters = [
'costWithTax', 'costWithTax',
'price', 'price',
'priceWithTax', 'priceWithTax',
'margin',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
]; ];

View File

@ -6,7 +6,7 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
const router = express.Router(); const router = express.Router();
const listAllowedFilters = ['name', 'createdAt', 'updatedAt', '_reference', 'tags']; const listAllowedFilters = ['name', 'createdAt', 'updatedAt', '_reference', 'tags'];
const listAllowedSorters = ['name', 'createdAt', '_id', 'updatedAt', 'tags']; const listAllowedSorters = ['name', 'createdAt', 'updatedAt'];
const propertiesAllowedFilters = ['tags']; const propertiesAllowedFilters = ['tags'];
import { import {

View File

@ -12,7 +12,6 @@ const listAllowedFilters = [
'active', 'active',
'country', 'country',
'jurisdiction', 'jurisdiction',
'marketplaces.marketplace',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',

View File

@ -21,12 +21,8 @@ const router = express.Router();
const listAllowedFilters = [ const listAllowedFilters = [
'name', 'name',
'handlingTime', 'handlingTime',
'localPickup',
'globalShipping',
'freightShipping',
'pickupDropOff',
'courierServices', 'courierServices',
'marketplaces.marketplace', 'localPickup',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
@ -35,9 +31,8 @@ const listAllowedSorters = ['name', 'handlingTime', 'createdAt', '_id', 'updated
const propertiesAllowedFilters = [ const propertiesAllowedFilters = [
'name', 'name',
'handlingTime', 'handlingTime',
'localPickup',
'courierServices', 'courierServices',
'marketplaces.marketplace', 'localPickup',
]; ];
router.get('/', isAuthenticated, checkPermissions('fulfillmentPolicy', 'list'), async (req, res) => { router.get('/', isAuthenticated, checkPermissions('fulfillmentPolicy', 'list'), async (req, res) => {

View File

@ -21,6 +21,7 @@ const listAllowedFilters = [
'returnPolicy', 'returnPolicy',
'state', 'state',
'state.type', 'state.type',
'condition',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
@ -48,6 +49,7 @@ const propertiesAllowedFilters = [
'returnPolicy', 'returnPolicy',
'state', 'state',
'state.type', 'state.type',
'condition',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
]; ];

View File

@ -24,7 +24,6 @@ const listAllowedFilters = [
'returnPeriodDays', 'returnPeriodDays',
'returnShippingCostPayer', 'returnShippingCostPayer',
'refundMethod', 'refundMethod',
'marketplaces.marketplace',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference', '_reference',
@ -42,7 +41,6 @@ const propertiesAllowedFilters = [
'returnsAccepted', 'returnsAccepted',
'returnShippingCostPayer', 'returnShippingCostPayer',
'refundMethod', 'refundMethod',
'marketplaces.marketplace',
]; ];
router.get('/', isAuthenticated, checkPermissions('returnPolicy', 'list'), async (req, res) => { router.get('/', isAuthenticated, checkPermissions('returnPolicy', 'list'), async (req, res) => {

View File

@ -6,6 +6,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(), listObjects: jest.fn(),
getObject: jest.fn(), getObject: jest.fn(),
editObject: jest.fn(), editObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(), editObjects: jest.fn(),
newObject: jest.fn(), newObject: jest.fn(),
deleteObject: jest.fn(), deleteObject: jest.fn(),
@ -13,6 +14,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
getModelStats: jest.fn(), getModelStats: jest.fn(),
getModelHistory: jest.fn(), getModelHistory: jest.fn(),
getObjectNeighbors: jest.fn(), getObjectNeighbors: jest.fn(),
checkStates: jest.fn(),
aggregateRollups: jest.fn(), aggregateRollups: jest.fn(),
aggregateRollupsHistory: jest.fn(), aggregateRollupsHistory: jest.fn(),
})); }));
@ -41,9 +43,12 @@ const {
listFilamentStocksRouteHandler, listFilamentStocksRouteHandler,
getFilamentStockRouteHandler, getFilamentStockRouteHandler,
newFilamentStockRouteHandler, newFilamentStockRouteHandler,
postFilamentStockRouteHandler,
} = await import('../filamentstocks.js'); } = await import('../filamentstocks.js');
const { listObjects, getObject, newObject } = await import('../../../database/database.js'); const { listObjects, getObject, newObject, checkStates, editObject } = await import(
'../../../database/database.js'
);
const { filamentStockModel } = await import( const { filamentStockModel } = await import(
'../../../database/schemas/inventory/filamentstock.schema.js' '../../../database/schemas/inventory/filamentstock.schema.js'
); );
@ -80,21 +85,79 @@ describe('Filament Stock Service Route Handlers', () => {
}); });
describe('newFilamentStockRouteHandler', () => { describe('newFilamentStockRouteHandler', () => {
it('should create a new filament stock', async () => { it('should create a new draft filament stock without a stock event', async () => {
req.body = { req.body = {
filament: 'filament123', filament: 'filament123',
startingWeight: { net: 1000, gross: 1100 }, startingWeight: { net: 1000, gross: 1100 },
currentWeight: { net: 1000, gross: 1100 }, currentWeight: { net: 1000, gross: 1100 },
}; };
const mockStock = { _id: '456', ...req.body }; const mockStock = { _id: '456', ...req.body, state: { type: 'draft' } };
const mockStockEvent = { _id: '789' }; newObject.mockResolvedValueOnce(mockStock);
newObject.mockResolvedValueOnce(mockStock).mockResolvedValueOnce(mockStockEvent);
await newFilamentStockRouteHandler(req, res); await newFilamentStockRouteHandler(req, res);
expect(newObject).toHaveBeenCalledTimes(2); expect(newObject).toHaveBeenCalledTimes(1);
expect(newObject).toHaveBeenCalledWith(
expect.objectContaining({
newData: expect.objectContaining({ state: { type: 'draft' } }),
})
);
expect(res.send).toHaveBeenCalledWith(mockStock); expect(res.send).toHaveBeenCalledWith(mockStock);
}); });
}); });
describe('postFilamentStockRouteHandler', () => {
it('should post a draft filament stock and create an initial stock event', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.user = { _id: 'test-user-id' };
checkStates.mockResolvedValue(true);
getObject.mockResolvedValue({
_id: '507f1f77bcf86cd799439011',
startingWeight: { net: 1000, gross: 1100 },
});
newObject.mockResolvedValue({ _id: '789' });
editObject.mockResolvedValue({
_id: '507f1f77bcf86cd799439011',
state: { type: 'unconsumed' },
postedAt: expect.any(Date),
});
await postFilamentStockRouteHandler(req, res);
expect(checkStates).toHaveBeenCalledWith(
expect.objectContaining({ states: ['draft'] })
);
expect(newObject).toHaveBeenCalledWith(
expect.objectContaining({
newData: expect.objectContaining({
value: 1000,
unit: 'g',
parentType: 'filamentStock',
}),
recalculate: true,
})
);
expect(editObject).toHaveBeenCalledWith(
expect.objectContaining({
updateData: expect.objectContaining({
state: { type: 'unconsumed' },
}),
})
);
expect(res.send).toHaveBeenCalled();
});
it('should fail if filament stock is not in draft state', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(false);
await postFilamentStockRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({ error: 'Filament stock is not in draft state.' })
);
});
});
}); });

View File

@ -12,6 +12,7 @@ import {
listObjectsByProperties, listObjectsByProperties,
getModelStats, getModelStats,
getModelHistory, getModelHistory,
checkStates,
searchObjects, searchObjects,
getPropertyValues, getPropertyValues,
getObjectNeighbors, getObjectNeighbors,
@ -20,6 +21,12 @@ import { stockEventModel } from '../../database/schemas/inventory/stockevent.sch
const logger = log4js.getLogger('Filament Stocks'); const logger = log4js.getLogger('Filament Stocks');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
const FILAMENT_STOCK_POPULATE = [
{ path: 'filament' },
{ path: 'filamentSku', populate: 'filament' },
{ path: 'stockLocation' },
];
export const listFilamentStocksRouteHandler = async ( export const listFilamentStocksRouteHandler = async (
req, req,
res, res,
@ -130,19 +137,33 @@ export const editFilamentStockRouteHandler = async (req, res) => {
logger.trace(`Filament Stock with ID: ${id}`); logger.trace(`Filament Stock with ID: ${id}`);
const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking filament stock states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Filament stock is not in draft state.');
res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 });
return;
}
const updateData = { const updateData = {
stockLocation: req.body.stockLocation, filament: req.body?.filament,
filamentSku: req.body?.filamentSku,
stockLocation: req.body?.stockLocation,
startingWeight: req.body?.startingWeight,
currentWeight: req.body?.currentWeight ?? req.body?.startingWeight,
}; };
const result = await editObject({ const result = await editObject({
model: filamentStockModel, model: filamentStockModel,
id, id,
updateData, updateData,
user: req.user, user: req.user,
populate: [ populate: FILAMENT_STOCK_POPULATE,
{ path: 'filament' },
{ path: 'filamentSku', populate: 'filament' },
{ path: 'stockLocation' },
],
}); });
if (result.error) { if (result.error) {
@ -183,13 +204,14 @@ export const editMultipleFilamentStocksRouteHandler = async (req, res) => {
}; };
export const newFilamentStockRouteHandler = async (req, res) => { export const newFilamentStockRouteHandler = async (req, res) => {
const startingWeight = req.body.startingWeight;
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),
startingWeight: req.body.startingWeight, startingWeight,
currentWeight: req.body.currentWeight, currentWeight: req.body.currentWeight ?? startingWeight,
filament: req.body.filament, filament: req.body.filament,
filamentSku: req.body.filamentSku, filamentSku: req.body.filamentSku,
state: req.body.state, state: req.body.state ?? { type: 'draft' },
stockLocation: req.body.stockLocation, stockLocation: req.body.stockLocation,
}; };
const result = await newObject({ const result = await newObject({
@ -204,28 +226,6 @@ export const newFilamentStockRouteHandler = async (req, res) => {
logger.debug(`New filament stock with ID: ${result._id}`); logger.debug(`New filament stock with ID: ${result._id}`);
const netStockEventData = {
updatedAt: new Date(),
value: req.body.startingWeight.net,
owner: req.user,
ownerType: 'user',
parent: result._id,
parentType: 'filamentStock',
unit: 'g',
};
const stockEventResult = await newObject({
model: stockEventModel,
newData: netStockEventData,
user: req.user,
});
if (stockEventResult.error) {
logger.error('No stock event created:', stockEventResult.error);
return res.status(stockEventResult.code).send(stockEventResult);
}
logger.debug(`New stock event with ID: ${stockEventResult._id}`);
res.send(result); res.send(result);
}; };
@ -235,6 +235,20 @@ export const deleteFilamentStockRouteHandler = async (req, res) => {
logger.trace(`Filament Stock with ID: ${id}`); logger.trace(`Filament Stock with ID: ${id}`);
const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking filament stock states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Filament stock is not in draft state.');
res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 });
return;
}
const result = await deleteObject({ const result = await deleteObject({
model: filamentStockModel, model: filamentStockModel,
id, id,
@ -250,6 +264,79 @@ export const deleteFilamentStockRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
export const postFilamentStockRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Filament Stock with ID: ${id}`);
const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking filament stock states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Filament stock is not in draft state.');
res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 });
return;
}
const filamentStock = await getObject({
model: filamentStockModel,
id,
populate: FILAMENT_STOCK_POPULATE,
});
if (filamentStock?.error) {
logger.error('Error loading filament stock to post:', filamentStock.error);
res.status(filamentStock.code || 500).send(filamentStock);
return;
}
const initialStockEventResult = await newObject({
model: stockEventModel,
newData: {
value: filamentStock.startingWeight.net,
unit: 'g',
parent: { _id: id },
parentType: 'filamentStock',
owner: { _id: req.user._id },
ownerType: 'user',
},
recalculate: true,
user: req.user,
});
if (initialStockEventResult?.error) {
logger.error('Error creating initial stock event:', initialStockEventResult.error);
res.status(initialStockEventResult.code || 500).send(initialStockEventResult);
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'unconsumed' },
postedAt: new Date(),
currentWeight: filamentStock.startingWeight,
};
const result = await editObject({
model: filamentStockModel,
id,
updateData,
user: req.user,
populate: FILAMENT_STOCK_POPULATE,
});
if (result.error) {
logger.error('Error posting filament stock:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Posted filament stock with ID: ${id}`);
res.send(result);
};
export const getFilamentStockStatsRouteHandler = async (req, res) => { export const getFilamentStockStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: filamentStockModel }); const result = await getModelStats({ model: filamentStockModel });
if (result?.error) { if (result?.error) {

View File

@ -51,6 +51,15 @@ const trimSpotlightObject = (object, objectType) => {
online: object.online || undefined, online: object.online || undefined,
amount: object.amount || undefined, amount: object.amount || undefined,
unit: object.unit || undefined, unit: object.unit || undefined,
currentWeight: object.currentWeight || undefined,
currentQuantity: object.currentQuantity || undefined,
grandTotalAmount: object.grandTotalAmount || undefined,
totalAmount: object.totalAmount || undefined,
totalAmountWithTax: object.totalAmountWithTax || undefined,
cost: object.cost || undefined,
costWithTax: object.costWithTax || undefined,
price: object.price || undefined,
priceWithTax: object.priceWithTax || undefined,
}; };
}; };

View File

@ -198,6 +198,26 @@ function parsePrefixedValue(value) {
return { prefix: null, suffix: trimmed, hadPrefix: false }; return { prefix: null, suffix: trimmed, hadPrefix: false };
} }
const FILTER_OPERATOR_PREFIXES = ['<>', '>=', '<=', '>', '<', '='];
function splitLeadingOperator(str) {
const text = String(str).trim();
for (const op of FILTER_OPERATOR_PREFIXES) {
if (text.startsWith(op)) {
return { operator: op, operand: text.slice(op.length).trim() };
}
}
return { operator: '', operand: text };
}
// Strips a model prefix (e.g. GCF:) from an expression, including after a leading operator.
function stripModelPrefixFromExpression(expression) {
const { operator, operand } = splitLeadingOperator(expression);
const { suffix, hadPrefix } = parsePrefixedValue(operand);
if (!hadPrefix) return expression;
return operator + suffix;
}
function buildRegexOp(pattern, useOptions = true) { function buildRegexOp(pattern, useOptions = true) {
const op = { $regex: pattern }; const op = { $regex: pattern };
if (useOptions) op.$options = 'i'; if (useOptions) op.$options = 'i';
@ -242,12 +262,12 @@ function getRefModelEntryFromPrefix(prefix) {
} }
function stripPrefixFromOperand(operand) { function stripPrefixFromOperand(operand) {
const { suffix, hadPrefix } = parsePrefixedValue(String(operand).trim()); return stripModelPrefixFromExpression(operand);
return hadPrefix ? suffix : String(operand).trim();
} }
function getRefModelEntryForToken(token, fallbackRefName) { function getRefModelEntryForToken(token, fallbackRefName) {
const { prefix, hadPrefix } = parsePrefixedValue(String(token).trim()); const { operand } = splitLeadingOperator(token);
const { prefix, hadPrefix } = parsePrefixedValue(operand);
if (hadPrefix) { if (hadPrefix) {
const entry = getRefModelEntryFromPrefix(prefix); const entry = getRefModelEntryFromPrefix(prefix);
if (entry) return entry; if (entry) return entry;
@ -741,9 +761,15 @@ function combineAndRefIds(children) {
} }
async function resolveRefOperand(operand, refModelEntry, operator = 'eq') { async function resolveRefOperand(operand, refModelEntry, operator = 'eq') {
const lookupOperand = String(operand).trim();
const { prefix, suffix, hadPrefix } = parsePrefixedValue(lookupOperand);
if (hadPrefix) {
const prefixEntry = getRefModelEntryFromPrefix(prefix);
if (prefixEntry) refModelEntry = prefixEntry;
}
if (!refModelEntry?.model) return NO_MATCH_CONDITION; if (!refModelEntry?.model) return NO_MATCH_CONDITION;
const lookupOperand = String(operand).trim();
const objectId = extractObjectIdFromOperand(lookupOperand); const objectId = extractObjectIdFromOperand(lookupOperand);
if (objectId) { if (objectId) {
@ -752,14 +778,16 @@ async function resolveRefOperand(operand, refModelEntry, operator = 'eq') {
return NO_MATCH_CONDITION; return NO_MATCH_CONDITION;
} }
const expression = buildRefListExpression(operator, lookupOperand); const expressionOperand = hadPrefix ? suffix : lookupOperand;
const ids = await listRefModelIds(refModelEntry, expression);
if (operator === 'ne') { if (operator === 'ne') {
const ids = await listRefModelIds(refModelEntry, expressionOperand);
if (ids.length === 0) return { query: {} }; if (ids.length === 0) return { query: {} };
return { op: { $nin: ids } }; return { op: { $nin: ids } };
} }
const expression = buildRefListExpression(operator, expressionOperand);
const ids = await listRefModelIds(refModelEntry, expression);
return idsToCondition(ids); return idsToCondition(ids);
} }
@ -850,9 +878,8 @@ async function parseFilter(property, value, model = null) {
} }
let expression = trimmed; let expression = trimmed;
if (fieldKind.kind === 'default' && expression.charAt(3) === ':') { if (fieldKind.kind === 'default') {
const afterColon = value.split(':')[1]; expression = stripModelPrefixFromExpression(expression);
expression = afterColon != null ? afterColon.trim() : '';
} }
const isDateField = looksLikeDateField(property); const isDateField = looksLikeDateField(property);