Repository navigation
add(order): measured quantities - #33
Merged
Merged
Conversation
…n whole base units, price per unit or per 100g or 1k items), actual quantities of cut or weighed goods
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(by weight, length, volume or count in whole base units, price per unit or per 100g or 1k items), actual quantities of cut or weighed goods.
Measured quantities (weight, length, volume, count) and actual quantities
Shops can now sell cable by the metre, paint by the litre, sand by the kilogram and services per 1,000 or 1,000,000
units, without decimal quantities. A measured quantity is a whole number of base units: grams, millimetres,
millilitres, or items for
count. The price refers to a price unit and basis (€1.20 / m,€2.49 / 100 g,€2.50 / 1,000). The line total isround_half_up(price × quantity ÷ factor)in cents, computed in integers from theunrounded price.
A manager or an ERP can then record the actual quantity of a line that was cut or weighed after ordering (2530 mm
ordered, 2553 mm cut). The line is billed, shipped and taken from stock with that quantity, within a per-product
tolerance.
Piece products, old orders and existing themes behave exactly as before. A test compares the piece cart and order JSON
byte for byte with a fixture captured from
master.Feature keys:
measured-quantities,actual-quantities.INTEGRATION_EVENTS_VERSIONstays1.Changes
Data and rules
2026_10_27_000000_add_measured_quantities_to_products. It is additive and guarded byhasColumn, andadds these nullable
s_productscolumns:measure,price_unit,price_per,quantity_step,quantity_min,quantity_tolerance,quantity_rounding.inventorystaysINTand holds base units for measured products.Services\MeasuredQuantity(integer arithmetic, rules, parsing, labels, accounting conversion),QuantityRulesand
MeasuredQuantityException.sProduct::MEASURE_*constants,isMeasured()andquantityRules().HasPhysicalAttributes::physicalUnits()andphysicalLabelLocalized().product.measures, plus rounding to the step per price unit inproduct.quantity_rounding(upforlength and volume,
nearestfor weight). The settings screen edits both. Invalid values are refused and the previousones are kept.
countcan bepriced per 1, 1,000 or 1,000,000.
Storefront
sCart::addProduct()and quick order acceptamountin the price unit. The quantity is raised to the minimum,rounded to the step, capped at the maximum and at the stock (rounded down to a step). When it changes, the response
has
quantityAdjusted: true.sCommerce::quantityInput($product)andsCommerce::unitPrice($product). The cart script postsamountforinputs that have
data-sc-measure.2,53 м).Orders
measure,base_unit,price_unit,price_per,factor,quantity_rounding|step|min|tolerance,quantity_label,unit_price_labelandline_weight. Piece lines have noneof these keys.
OrderLineEditor. Measure rules and actual quantities stay as stored onthe server; values from the browser are ignored. Measured quantities are validated strictly and never rounded.
OrderCostRecalculator, retail excise,OrderRepeater, therepeat endpoint, the dashboard (a measured line counts as 1 unit), emails and print.
order.
Actual quantities
Services\OrderActualQuantity::set():conflict (409).
OrderUpdated.OrderActualQuantityController: POST only,exec_module, CSRF and a nonce for the order form.scommerce_order_actual_over_tolerance(migration2026_10_27_000001).sOrder::authorizationCeiling(): the cost plus each measured line's tolerance. Payment methods that authorizefirst and capture later can opt in; existing methods are unchanged.
Stock and shipments
actual_quantity ?? quantity).StockCommitter::adjust()applies changed, added and removed lines to stock that sCommerce reduced locally.ContainerDeposit::unit()and in
applyToLines().Integrations
fail that row.
unit_pricing_measureandunit_pricing_base_measurefor weight, length and volumeproducts.
MeasuredQuantity::forAccounting()gives accounting systems exact lines: weight, length and volume in the largestunit (
0.330 kg × 24.90),countin price-basis units (1.234567 × 1,000,000 @ 0.50).Behaviour change (also for piece products)
Reduced stock now follows manager order edits. When stock reduction is on and sCommerce reduced the order's stock
locally, changing, adding or removing lines in the manager takes or returns the difference in the same request. If
stock is too low for an increase, the edit is not saved and the manager sees a notice. Orders whose stock was reduced
by a stock provider are not changed locally; the provider receives
sCommerce.OrderUpdated. Before this PR, manageredits did not change reduced stock. This is documented in
docs/en/developers/events.mdanddocs/en/configuration.md.Compatibility
sCart::addProduct,StockWriter::set,StockProviderInterface,ProductStockChanged,OrderShipments::upsert,ContainerDeposit::lineFields.quantitystays an integer.Deviations from the plan
3 l,0.33 kg). The site settinginput_decimalsonly affects the storefront input.line_weightuses the product weight per price unit:round_half_up(weight × quantity ÷ base units per price unit). This follows the plan's stated rule; its literal formula divides byfactor, which would be wrong for pricesper 100 g. Products sold by weight use the quantity itself as the weight.
quantity_min, so manager edits can be validated strictly.Tests
scommerce-measured-quantities.php(151 checks)scommerce-actual-quantities.php(46 checks)scommerce-measured-quantities.mjs: the JS port of the arithmetic, checked against the same shared values infixtures/measured-quantities.jsondelegates to
OrderLineEditor, so they look there.fixtures/piece-cart-master.json, whichfixtures/piece-cart-capture.phpproduced onmaster.below).
Docs and translations
docs/en/developers/measured-quantities.md. Updated: events, integration (order line snapshot),deliveries, payments and configuration pages, and the README features list.
How to verify
m, step10, minimum1000, tolerance500.{!! sCommerce::quantityInput($product) !!}and add2.533. The cart shows2.54 mand€3.05 (at €1.20/m).
2.553. The cost, the stock (if stock reduction ison) and the history follow.
2.7without the agreement checkbox: it is refused with 422.Follow-up, not in this PR
sCart::getProductFields()and quick order callphysicalPayload()once per line, which loadsattrValueslazilyeach time. This is a likely N+1 query; confirm it with query logging and fix it by eager loading.