pub struct InventoryWriteService { /* private fields */ }Implementations§
Source§impl InventoryWriteService
impl InventoryWriteService
pub async fn create_warehouse( &self, w: NewWarehouse, ) -> Result<Uuid, InventoryError>
pub async fn create_stock_item( &self, s: NewStockItem, ) -> Result<Uuid, InventoryError>
Source§impl InventoryWriteService
impl InventoryWriteService
pub async fn create_purchase_receipt( &self, r: NewReceipt, ) -> Result<Uuid, InventoryError>
Sourcepub async fn submit_purchase_receipt(
&self,
id: Uuid,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn submit_purchase_receipt( &self, id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Submit the goods-in voucher. Every line mints ONE stock move through the converged
engine (supplier location → the warehouse’s stock location): confirm → assign (an
inbound move’s supply is unconditionally available, so the execution line covers the
full line) → done, which materializes the destination quant, reblends the Bin
(value += qty·rate; rate = value/qty) and mints the SLE row. The voucher keeps its
identity verbatim — header status, the ONE Dr Inventory · Cr GR/IR envelope, the
posting_state/repost machinery, the StockReceived event; the moves post no GL (empty
directive + the door-owned-GL tripwire sink).
Not cross-move atomic (each engine verb commits its own transaction): a crash mid-way
leaves the receipt draft with some line moves landed — the deterministic move names
({receipt_number}/{seq}, origin = the receipt number) make a re-submit RESUME
instead of double-receiving.
Sourcepub async fn submit_purchase_receipt_into(
&self,
id: Uuid,
destination_location_id: Option<Uuid>,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn submit_purchase_receipt_into( &self, id: Uuid, destination_location_id: Option<Uuid>, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Submit a receipt into a destination other than the warehouse’s stock location.
The ordinary receipt lands on the shelf, which is what a goods receipt means. A subcontract receipt does not: the supplier’s output belongs to an open manufacturing order, and landing it on the shelf makes the estate believe in it twice, once here and once when that order is received. The caller passes the location the goods are really standing in — the production counterpart — and the manufacturing receive moves them onto the shelf at their finished value.
Inventory does not know what subcontracting is and does not learn it here: it takes a location. Deciding WHICH location belongs to whoever knows why the goods arrived.
Sourcepub async fn repost_purchase_receipt(
&self,
id: Uuid,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn repost_purchase_receipt( &self, id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Re-emit the GL post for a voucher whose physical movement committed but whose post is
pending or failed (a transient accounting outage, or a crash between commit and the
status update). This is the exit from a stuck failed voucher (council 2026-07-04) — without
it, (submitted, failed) is terminal and the subledger silently stops tying to the GL.
Idempotent against the “physically-posted-but-status-not-updated” crash window: it rebuilds
the SAME envelope (source_id = voucher_id), and accounting dedupes on
(company, source_type, source_id, posting_type) — so a re-emit of an already-posted voucher
returns the original journal, never a second one. An already-posted voucher short-circuits
(returns the recorded ids); not_applicable is a no-op. Rebuilds the envelope from the stored
header (never re-touches the SLE/Bin — the physical movement already happened).
Source§impl InventoryWriteService
impl InventoryWriteService
pub async fn create_delivery_note( &self, d: NewDelivery, ) -> Result<Uuid, InventoryError>
Sourcepub async fn submit_delivery_note(
&self,
id: Uuid,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn submit_delivery_note( &self, id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Submit the goods-out voucher. Every line mints ONE stock move through the converged
engine (the warehouse’s stock location → the customer location): confirm → assign
(reserves against the source quants — the door refuses a line the reservation cannot
cover WHOLE, preserving the voucher’s all-or-nothing availability posture) → done,
which releases the reservation, draws the source quant, reblends the Bin (cogs = qty·rate; rate unchanged; residual flush when the bin drains to 0) and mints the SLE
row. The voucher keeps its identity verbatim — header status + per-line COGS snapshot,
the ONE Dr COGS · Cr Inventory envelope, the posting_state/repost machinery, the
StockDelivered event; the moves post no GL (empty directive + the door-owned-GL
tripwire sink).
Not cross-move atomic (each engine verb commits its own transaction): a crash mid-way
leaves the delivery draft with some line moves landed — the deterministic move names
({delivery_number}/{seq}, origin = the delivery number) make a re-submit RESUME
instead of double-shipping. A refusal mid-way (a line the reservation cannot cover)
unreserves that line’s move and leaves the voucher draft: no stock moved, no
reservation held.
pub async fn repost_delivery_note( &self, id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Source§impl InventoryWriteService
impl InventoryWriteService
Sourcepub async fn create_picking(
&self,
p: NewPicking,
) -> Result<PickingCreated, InventoryError>
pub async fn create_picking( &self, p: NewPicking, ) -> Result<PickingCreated, InventoryError>
Mint a picking: insert the transfer header, then mint one DRAFT move per line through
the engine and confirm each (the operation type’s reservation posture fires with
confirm — at_confirm reserves immediately). Every mint/confirm REPROJECTS the
transfer (the engine does it on every move change — this method never touches
transfers.state). Guards: R3 (name unique — the typed duplicate error), R9 (source
!= destination), non-empty, non-negative demand.
Not cross-move atomic: each engine verb commits its own transaction. A failure
mid-way leaves the transfer projected at whatever its minted moves say (typically
draft/confirmed) — a probe-able, retryable state, never a half-flipped stock
position (stock only moves on validate_picking).
Sourcepub async fn fetch_picking(
&self,
transfer_id: Uuid,
) -> Result<(TransferHeaderRow, Vec<MoveStateRow>), InventoryError>
pub async fn fetch_picking( &self, transfer_id: Uuid, ) -> Result<(TransferHeaderRow, Vec<MoveStateRow>), InventoryError>
The projection PROBE: read the transfer header (with its projected state) and the member move states. The state is a stored compute the engine re-derived — callers read it, they never assert it. The read rides the caller’s ambient org scope (ADR-0029): under the composed shape the decorator’s fence bounds it; undecorated (module tests, jobs) it is plain.
Sourcepub async fn assign_picking(
&self,
move_id: Uuid,
) -> Result<PickingAssignment, InventoryError>
pub async fn assign_picking( &self, move_id: Uuid, ) -> Result<PickingAssignment, InventoryError>
_assign_picking: attach a rule-launched move to its grouping transfer — the hop that
carries a demand launched through the procurement engine (run_procurement / _run_pull
mint moves with no transfer) onto the picking surface an operator validates. The move’s
RULE names the operation type; the group key is (operation type, source, destination,
partner, origin) with origin standing in for the procurement group — every move
launched for one source document joins ONE transfer, and a transfer stays open for
later lines of the same document. An open transfer that matches is joined; otherwise a
header is minted (name from the operation type’s reference prefix, unique within the
composing tenant’s scope; shipping policy inherited from the operation type). The
projection then re-derives from the member move states, exactly as it does on every
move change.
Idempotent: a move that already belongs to a transfer returns it (minted: false).
Fail-closed on a move with no procurement rule (voucher-door moves keep their voucher
identity — their door mints the moves and owns the GL, and no picking is derived for
them here) and on terminal states (done / cancel never re-group).
Sourcepub async fn validate_picking(
&self,
transfer_id: Uuid,
gl: &MoveGlDirective,
sink: &dyn GlPostSink,
) -> Result<PickingValidated, InventoryError>
pub async fn validate_picking( &self, transfer_id: Uuid, gl: &MoveGlDirective, sink: &dyn GlPostSink, ) -> Result<PickingValidated, InventoryError>
button_validate (spec §2): _action_done over the transfer’s moves — nothing else.
Done/cancelled member moves are skipped (idempotent re-validate). For each live move:
the quant surface at its source grain is healed if the stock predates the converged
model (Bins without quants); unreserved internal moves get an assign pass first (it
mints the reservation mirror lines), and a move still without lines — nothing
reservable, or an inbound move whose source is virtual — gets its demand line minted
as the physical detail row so done has lines (R24); _action_done then flips the
quants, mints the V7-ordered SLE pair, and posts the GL leg the move’s shape calls
for. The partial-validate backorder policy comes from the operation type (ask maps
to creating the backorder — the non-interactive default of Odoo’s wizard; delayed
creates it too but leaves its reservation to the scheduler’s assign sweep).
Posting a GL leg needs the composing service’s GlPostSink; a leg whose accounts the
directive lacks is simply not posted (the physical movement is unaffected).
Sourcepub async fn submit_transfer(
&self,
t: NewTransfer,
) -> Result<Uuid, InventoryError>
pub async fn submit_transfer( &self, t: NewTransfer, ) -> Result<Uuid, InventoryError>
The warehouse-to-warehouse stock entry, converged: the voucher rows keep their exact
identity (header, item lines, the StockMoved event, value-neutral no-GL posture),
while each line’s physical/valuation legs are minted by the move pipeline — one move
per line, confirmed, reserved against the source quants, validated. The engine’s
valuation core produces the W1-proven arithmetic: the OUT leg valued at the source
Bin’s pre-move average BEFORE the destination reblende (V7 ordering), the residual
flush when a Bin drains to zero, value conserved across the pair.
Source§impl InventoryWriteService
impl InventoryWriteService
Sourcepub async fn stage_quant_count(
&self,
item_id: Uuid,
location_id: Uuid,
counted_qty: Decimal,
) -> Result<StagedCount, InventoryError>
pub async fn stage_quant_count( &self, item_id: Uuid, location_id: Uuid, counted_qty: Decimal, ) -> Result<StagedCount, InventoryError>
Stage a counted quantity on a quant (spec §5.2): the count is a LEVEL (the counted
on-hand), staged as inventory_quantity with its stored diff compute (T4) and the
inventory_quantity_set gate that Self::apply_inventory demands. Staging
OVERWRITES any prior staging — the count is not a delta, so re-counting replaces the
previous count. Guards: R24 (the quant holds no reservations), R13 (only internal
locations are countable). A quant that does not exist yet is initialized at zero —
the count surface exists even before any stock does.
Sourcepub async fn apply_inventory(
&self,
selector: QuantSelector,
gl: &MoveGlDirective,
sink: &dyn GlPostSink,
) -> Result<AppliedCount, InventoryError>
pub async fn apply_inventory( &self, selector: QuantSelector, gl: &MoveGlDirective, sink: &dyn GlPostSink, ) -> Result<AppliedCount, InventoryError>
Apply a staged count (spec §5.2 _apply_inventory): mint (or resume) the
is_inventory move that reconciles the quant’s on-hand to the count, and validate
it through the engine pipeline — the move is the ONE writer of the quant’s
quantity; this method never touches it. Idempotent by construction:
- nothing staged (gate down) → no-op;
- on-hand already equals the count (a crash between the move’s commit and the staging consume, or offsetting activity) → consume the staging, mint nothing;
- a live
is_inventorymove for the grain already exists (a crash mid-pipeline) → resume IT, never mint a second (the mint-once gate); - a move landed between the count and here → LOUD
OutdatedCount(the staged diff no longer reconciles the count; re-stage against the new on-hand).
GL: with accounts in gl, the engine posts the adjustment leg per move
(Dr Inventory · Cr Adjustment up / reversed down, idempotency key = move id); a
directive without accounts posts nothing (the physical movement is unaffected) —
the voucher path drives the door that way and posts its own single net envelope.
Sourcepub async fn staged_counts(
&self,
location_id: Uuid,
) -> Result<Vec<StagedCountRow>, InventoryError>
pub async fn staged_counts( &self, location_id: Uuid, ) -> Result<Vec<StagedCountRow>, InventoryError>
The pending-count worklist at a location (the partial index
WHERE inventory_quantity_set = true serves this read): every staged-but-unapplied
count, with the stored diff compute. A probe — it derives nothing, writes nothing.
Sourcepub async fn submit_reconciliation(
&self,
r: NewReconciliation,
sink: &dyn GlPostSink,
) -> Result<Uuid, InventoryError>
pub async fn submit_reconciliation( &self, r: NewReconciliation, sink: &dyn GlPostSink, ) -> Result<Uuid, InventoryError>
Submit a physical-count voucher (converged onto the adjustment door). Document
identity verbatim: header + item rows + the single value-diff GL envelope + the
StockReconciled event. Physical: per line, the count stages on the warehouse stock
location’s quant and applies through Self::apply_inventory — the minted
is_inventory moves carry the SLE/Bin legs (voucher-type stock_entry, voucher id
= the move id: the ledger contract is the append-only SLE + moving average, which
the engine owns). The voucher’s GL leg posts exactly once (recon-keyed idempotency);
the door is driven with a no-accounts directive so no per-move GL duplicates it.
Sequencing: the physical applications run FIRST, the voucher rows are recorded LAST — a failure mid-way leaves no half-written voucher (the same number can be re-submitted), and the door’s idempotency (gate consumed per applied line; fresh staging per unapplied line) makes the re-run converge instead of double-applying. Not atomic across lines: an applied line’s stock movement stays applied (the SLE is append-only truth); the retry re-stages every line against the then-current on-hand.
Sourcepub async fn repost_reconciliation(
&self,
id: Uuid,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn repost_reconciliation( &self, id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Re-drive the GL leg for a reconciliation whose physical movement committed but whose post is
pending or failed, OR — the case this exists for — a net==0 recon that crashed between
the voucher record and mark_not_applicable and is stuck in (submitted, pending). Mirrors the
receipt/delivery repost: rebuilds from the stored header, never re-touches the physical
movement (the door’s staging gate is consumed and the moves are done — the apply is a no-op,
so a repost can never double-apply the counts).
net != 0 → re-emit the value-diff envelope (idempotent: accounting dedupes on
(company, source_type, source_id, posting_type)); net == 0 → reconcile to
not_applicable. Already-posted/not_applicable short-circuits via already_settled.
Source§impl InventoryWriteService
impl InventoryWriteService
Sourcepub async fn create_batch(
&self,
name: String,
is_wave: bool,
user_id: Option<Uuid>,
) -> Result<BatchHeaderRow, InventoryError>
pub async fn create_batch( &self, name: String, is_wave: bool, user_id: Option<Uuid>, ) -> Result<BatchHeaderRow, InventoryError>
Mint a batch: insert the header (a draft grouping point). state starts at
draft and is from here on ONLY ever written by the projection recompute — this
method never touches it. Guards: R3-shaped org-scoped name unique (the typed
duplicate error on collision).
Sourcepub async fn fetch_batch(
&self,
batch_id: Uuid,
) -> Result<(BatchHeaderRow, Vec<BatchMemberRow>), InventoryError>
pub async fn fetch_batch( &self, batch_id: Uuid, ) -> Result<(BatchHeaderRow, Vec<BatchMemberRow>), InventoryError>
The projection PROBE: read the batch header (with its projected state) and its member pickings’ states, riding the caller’s ambient org scope (ADR-0029) — an out-of-scope batch reads as absent under the composition’s fence.
Sourcepub async fn add_picking_to_batch(
&self,
batch_id: Uuid,
picking_id: Uuid,
) -> Result<BatchHeaderRow, InventoryError>
pub async fn add_picking_to_batch( &self, batch_id: Uuid, picking_id: Uuid, ) -> Result<BatchHeaderRow, InventoryError>
Attach a picking to its batch. Guards (all typed, all service-side with the
DB trigger as backstop — enforcement: both, ADR-0015):
- the batch exists (absent under the ambient scope reads as
NotFound), - the batch is non-terminal (
done/cancelrefuse —BatchTerminal), - the picking exists and is non-terminal (
PickingTerminalForBatch— a done or cancelled picking cannot join a work list), - the picking belongs to no other batch (
PickingAlreadyBatched— membership is one batch per picking). The write moves only the membership pointer and stamps thehad_membersdiscriminator; the projection recompute that follows derives the state. Returns the re-derived header (a READ of the projection).
Sourcepub async fn remove_picking_from_batch(
&self,
batch_id: Uuid,
picking_id: Uuid,
) -> Result<BatchHeaderRow, InventoryError>
pub async fn remove_picking_from_batch( &self, batch_id: Uuid, picking_id: Uuid, ) -> Result<BatchHeaderRow, InventoryError>
Detach a picking from its batch. Removal is always allowed — including a done or cancelled picking (that is how a finished list gets cleaned up). Detaching the last member leaves an EMPTY batch, which the projection then auto-cancels (the SB-1 discriminator: a batch that has grouped work must not linger as a live work item once emptied). Returns the re-derived header after the removal.
Source§impl InventoryWriteService
impl InventoryWriteService
Sourcepub async fn create_scrap(
&self,
s: NewScrap,
) -> Result<ScrapRow, InventoryError>
pub async fn create_scrap( &self, s: NewScrap, ) -> Result<ScrapRow, InventoryError>
Mint a scrap order in draft. Nothing moves yet — the header records WHAT will be
scrapped, where from, and where to. Guards: strictly positive quantity, a live
internal source location, a resolvable scrap location. The name is
minted SCRAP/<short-uuid> (org-scoped unique — the typed duplicate error on the
rare collision).
Sourcepub async fn fetch_scrap(
&self,
scrap_id: Uuid,
) -> Result<Option<ScrapRow>, InventoryError>
pub async fn fetch_scrap( &self, scrap_id: Uuid, ) -> Result<Option<ScrapRow>, InventoryError>
The probe: read one scrap header, riding the caller’s ambient org scope (ADR-0029).
Sourcepub async fn process_scrap(
&self,
scrap_id: Uuid,
gl: &MoveGlDirective,
sink: &dyn GlPostSink,
) -> Result<ScrapProcessed, InventoryError>
pub async fn process_scrap( &self, scrap_id: Uuid, gl: &MoveGlDirective, sink: &dyn GlPostSink, ) -> Result<ScrapProcessed, InventoryError>
Process a DRAFT scrap: mint the move, drive it to done through the engine, stamp
the header. The full-GL form — the caller’s directive carries the accounts and the
sink posts the envelope. The move is is_inventory (the engine’s adjustment leg
shape: the directive’s adjustment_account_id is the scrap expense account) and
scrapped (the engine’s marker for scrap-marked moves). A scrap lands WHOLE: the
backorder policy is Never and a move that cannot fully reserve refuses loudly
(you cannot scrap stock that is not there — R22).
Crash-window safe: the done-stamp’s state = 'draft' guard means exactly one
process call closes the header; a crash after the move landed but before the stamp
leaves the scrap re-processable, and the resume check below re-drives the SAME
move instead of minting a second one.
Sourcepub async fn process_scrap_deferred(
&self,
scrap_id: Uuid,
) -> Result<ScrapProcessed, InventoryError>
pub async fn process_scrap_deferred( &self, scrap_id: Uuid, ) -> Result<ScrapProcessed, InventoryError>
The HTTP-shaped process: the SAME physical movement (the move lands done, the
quants flip, the SLEs mint), but the GL leg carries a no-accounts directive — the
engine builds no envelope and the move’s posting stays not_applicable until a
service-driven Self::repost_move_gl drives it with real accounts (the module’s
standing deferred posture for GL-posting verbs on a bare route).
Source§impl InventoryWriteService
impl InventoryWriteService
Sourcepub async fn cancel_purchase_receipt(
&self,
id: Uuid,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn cancel_purchase_receipt( &self, id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Cancel the goods-in voucher: per line, ONE reverse move (stock location → supplier
location) minted with forced_value = money(qty·rate) — the exact value the inflow
added — confirm → assign (reserves against the stock quants; the door refuses a line
whose free availability cannot cover the WHOLE reversal) → done, which draws the source
quant, reblends the Bin back to its pre-receipt state and mints the compensating SLE row.
The voucher’s ONE reversal envelope (Dr GR/IR · Cr Inventory) is unchanged.
Not cross-move atomic (each engine verb commits its own transaction): a crash mid-way
leaves the voucher submitted with some reverse moves landed — the deterministic names
({receipt_number}/REV/{seq}) make a re-cancel RESUME instead of double-reversing.
Sourcepub async fn cancel_delivery_note(
&self,
id: Uuid,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn cancel_delivery_note( &self, id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Cancel the goods-out voucher: per line, ONE reverse move (the customer location → the
stock location) minted with forced_value = cogs_amount — the EXACT value the outflow
consumed (the whole remaining value for a drain-to-zero line) — confirm → assign (an
external source’s supply is unconditionally available, so the full line is covered) →
done, which materializes the stock quant, reblends the Bin back to its pre-delivery
state and mints the compensating SLE row. The voucher’s ONE reversal envelope
(Dr Inventory · Cr COGS) is unchanged. Always safe — qty only increases.
Source§impl InventoryWriteService
The move lifecycle engine. An impl InventoryWriteService chunk over the vocabulary in
super::inventory_write_service — the converged stock pipeline the routes/scheduler surfaces
drive.
impl InventoryWriteService
The move lifecycle engine. An impl InventoryWriteService chunk over the vocabulary in
super::inventory_write_service — the converged stock pipeline the routes/scheduler surfaces
drive.
Sourcepub async fn create_move(&self, m: NewStockMove) -> Result<Uuid, InventoryError>
pub async fn create_move(&self, m: NewStockMove) -> Result<Uuid, InventoryError>
Create a DRAFT move (spec §1: draft → waiting/confirmed happens on confirm, never at
insert). Guards: R9 (src != dest), non-negative demand (R23 at the door), R13 (a quant
surface can never be a view location — checked for both endpoints that hold stock).
Sourcepub async fn action_confirm(
&self,
move_id: Uuid,
) -> Result<String, InventoryError>
pub async fn action_confirm( &self, move_id: Uuid, ) -> Result<String, InventoryError>
draft → confirmed, or draft → waiting while any move_orig_ids parent is not done
(spec §1: waiting is the state a chained move sits in until its parents land).
procure_method gates supply: make_to_order / mts_else_mto moves are minted by the
procurement (rule) engine, not here — this method only owns the state gate.
The move read rides the caller’s ambient org scope (ADR-0029): under the composed shape the decorator’s fence bounds it; undecorated (module tests, jobs) it is plain.
Sourcepub async fn action_assign(
&self,
move_id: Uuid,
) -> Result<MoveAssignOutcome, InventoryError>
pub async fn action_assign( &self, move_id: Uuid, ) -> Result<MoveAssignOutcome, InventoryError>
Reserve against quants at the source location (spec §1 _action_assign): the authoritative
reserved_quantity is written under each quant’s FOR UPDATE (competing reservations
serialize there — one winner per unit of stock, R22/R25), a mirror move-line is minted per
reserved quant grain, and the move state aggregates the mirror.
The move read rides the caller’s ambient org scope (ADR-0029): under the composed shape the decorator’s fence bounds it; undecorated (module tests, jobs) it is plain.
Sourcepub async fn unreserve_move(
&self,
move_id: Uuid,
) -> Result<Decimal, InventoryError>
pub async fn unreserve_move( &self, move_id: Uuid, ) -> Result<Decimal, InventoryError>
Release every reservation the move holds (spec §1: cancel frees reservations first; assign
retries also release-then-re.reserve). The mirror lines zero out; the authoritative
reserved_quantity drops by exactly what the lines held.
The move read rides the caller’s ambient org scope (ADR-0029): under the composed shape the decorator’s fence bounds it; undecorated (module tests, jobs) it is plain.
Sourcepub async fn action_done(
&self,
move_id: Uuid,
backorder: BackorderPolicy,
gl: &MoveGlDirective,
sink: &dyn GlPostSink,
) -> Result<MoveDoneOutcome, InventoryError>
pub async fn action_done( &self, move_id: Uuid, backorder: BackorderPolicy, gl: &MoveGlDirective, sink: &dyn GlPostSink, ) -> Result<MoveDoneOutcome, InventoryError>
The mutation core (spec §4). See the module docs above for the full pipeline; the guards, the two-step quant sync, the V7-ordered valuation, the chain propagation, the backorder split and the projection all live here.
The move read rides the caller’s ambient org scope (ADR-0029): under the composed shape the decorator’s fence bounds it; undecorated (module tests, jobs) it is plain.
Sourcepub async fn action_cancel(
&self,
move_id: Uuid,
) -> Result<Decimal, InventoryError>
pub async fn action_cancel( &self, move_id: Uuid, ) -> Result<Decimal, InventoryError>
* → cancel (spec §1): frees the reservation first, then propagates to the chained
children (move_dest_ids) unless propagate_cancel=false — never into a done move.
The move read rides the caller’s ambient org scope (ADR-0029): under the composed shape the decorator’s fence bounds it; undecorated (module tests, jobs) it is plain.
Sourcepub async fn repost_move_gl(
&self,
move_id: Uuid,
gl: &MoveGlDirective,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn repost_move_gl( &self, move_id: Uuid, gl: &MoveGlDirective, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Re-drive the GL leg of a done move whose posting_state is pending or failed — a
rejected or interrupted AccountingPost from _action_done. The physical movement
(quants, Bin, SLE rows) already happened and is never re-touched: the envelope is rebuilt
from the move’s committed ledger legs plus the caller’s account directive, re-emitted
under the move’s ORIGINAL source identity (source_type='inventory', source_id = move
id, posting_type='original'), and accounting’s dedupe on that identity makes the
re-drive idempotent — a leg that actually landed before the status write was lost returns
the original journal instead of a second one.
Short-circuits exactly like the voucher doors’ repost verbs: posted → nothing to drive;
not_applicable → this move owns no GL leg (a voucher door posted its voucher’s
envelope; a value-neutral shape posts nothing), so a sweep can call this blindly. If the
rebuilt envelope is None (the directive no longer supplies the accounts this leg shape
needs), the unsettled leg retires to not_applicable — the GL is genuinely not postable
under the current configuration and the move leaves the sweep worklist.
On a fresh rejection the move parks in failed again and the error surfaces, as in
_action_done.
The move read rides the caller’s ambient org scope (ADR-0029): under the composed shape the decorator’s fence bounds it; undecorated (module tests, jobs) it is plain.
Source§impl InventoryWriteService
impl InventoryWriteService
Sourcepub async fn create_landed_cost(
&self,
lc: NewLandedCost,
) -> Result<Uuid, InventoryError>
pub async fn create_landed_cost( &self, lc: NewLandedCost, ) -> Result<Uuid, InventoryError>
Open a DRAFT landed cost: header + cost lines as one unit. Guards: at least one line (nothing to allocate); every line carries a credit account (a line without one is rejected loudly — its split’s credit side would be undefined). Negative amounts are allowed: a negative landed cost is the reversal pattern.
Sourcepub async fn validate_landed_cost(
&self,
lc_id: Uuid,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn validate_landed_cost( &self, lc_id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Validate the DRAFT landed cost: split → revalue → post, the full verb. The physical
revaluation (worksheet rebuild + value-only SLE rows + bin reblends + state=done +
GL leg armed pending) commits FIRST; the GL post is eventually consistent, exactly
like the voucher doors. Service/job-driven (it needs the composing service’s
GlPostSink).
Sourcepub async fn validate_landed_cost_deferred(
&self,
lc_id: Uuid,
) -> Result<SubmitOutcome, InventoryError>
pub async fn validate_landed_cost_deferred( &self, lc_id: Uuid, ) -> Result<SubmitOutcome, InventoryError>
The HTTP-shaped validate: the SAME physical revalidation, but the GL leg stays ARMED
pending for a service-driven Self::repost_landed_cost to drive (the composing
service’s sink is not available on a bare route — the module’s standing posture for
GL-posting verbs). Documents that structurally post nothing (a periodic valuation
policy, an
all-consumed allocation) retire to not_applicable immediately.
Sourcepub async fn cancel_landed_cost(
&self,
lc_id: Uuid,
) -> Result<(), InventoryError>
pub async fn cancel_landed_cost( &self, lc_id: Uuid, ) -> Result<(), InventoryError>
Cancel a DRAFT landed cost. A done landed cost can never cancel — its bins and ledger
already revalued; the correction pattern is a NEGATIVE landed cost (swapped legs).
Sourcepub async fn repost_landed_cost(
&self,
lc_id: Uuid,
sink: &dyn GlPostSink,
) -> Result<SubmitOutcome, InventoryError>
pub async fn repost_landed_cost( &self, lc_id: Uuid, sink: &dyn GlPostSink, ) -> Result<SubmitOutcome, InventoryError>
Re-drive the GL leg of a DONE landed cost whose posting_state is pending or
failed (a rejected or interrupted post — the physical revaluation already committed
and is never re-touched). The envelope is rebuilt from the MINTED ledger and the
worksheet: debit legs from the landed-cost SLE rows (each row’s signed delta, its
target line resolving the valuation-account chain), credit legs from the worksheet’s
per-cost-line shares scaled by each row’s snapshotted remaining share (a done move’s
quantity is frozen, so the rebuild is exact). Idempotent: posted short-circuits with
the recorded ids, not_applicable is a no-op, and the envelope re-uses the document’s
source identity so accounting’s dedupe on (company, source_type, source_id, posting_type) can never double-post.
Trait Implementations§
Source§impl Clone for InventoryWriteService
impl Clone for InventoryWriteService
Source§impl MovePipeline for InventoryWriteService
The write service IS the move engine, so it implements the port the daily scheduler drives
(procurement_service::MovePipeline): the per-move confirm/assign verbs on the CALLER’S
connection — the scheduler runs them inside its per-batch transaction, so a batch’s
transitions commit or roll back together. Failures cross the boundary as a flat
{code, message} (the engine’s own error taxonomy stays on this side of the port). Events
publish immediately through the advisory sink; the batch commit boundary is the scheduler’s
concern.
impl MovePipeline for InventoryWriteService
The write service IS the move engine, so it implements the port the daily scheduler drives
(procurement_service::MovePipeline): the per-move confirm/assign verbs on the CALLER’S
connection — the scheduler runs them inside its per-batch transaction, so a batch’s
transitions commit or roll back together. Failures cross the boundary as a flat
{code, message} (the engine’s own error taxonomy stays on this side of the port). Events
publish immediately through the advisory sink; the batch commit boundary is the scheduler’s
concern.
fn confirm<'life0, 'life1, 'async_trait>(
&'life0 self,
conn: &'life1 mut PgConnection,
move_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<MoveState, MovePipelineError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn assign<'life0, 'life1, 'async_trait>(
&'life0 self,
conn: &'life1 mut PgConnection,
move_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<MoveState, MovePipelineError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Auto Trait Implementations§
impl !RefUnwindSafe for InventoryWriteService
impl !UnwindSafe for InventoryWriteService
impl Freeze for InventoryWriteService
impl Send for InventoryWriteService
impl Sync for InventoryWriteService
impl Unpin for InventoryWriteService
impl UnsafeUnpin for InventoryWriteService
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more