FEFO — first-expired, first-out — picks stock by expiry date, not by receipt date. It is not a refinement of FIFO. The two give different answers whenever a later delivery has a shorter remaining life, which in fresh grocery is an ordinary week, not an edge case.

FEFO against the alternatives

MethodPicks byResult in fresh categories
FIFOReceipt dateShips older stock that may expire later. Newer, shorter-dated stock spoils.
FEFOExpiry dateShortest-dated stock moves first. Correct for anything perishable.
Nearest binWhatever the picker reachesWhat happens by default when the system does not direct the pick.

That third row is the real comparison. Most FEFO failures are not a wrong configuration; they are a correct configuration that only exists in a report while the pick itself stays undirected.

Codeunit 50200 Supermarket FEFO Engine.al
codeunit 50200 "Supermarket FEFO Engine"
{
    Access = Public;
    Subtype = Normal;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Create Pick", 'OnBeforeCreatePick', '', false, false)]
    local procedure EnforceFEFOPicking(var WarehouseActivityHeader: Record "Warehouse Activity Header"; var IsHandled: Boolean)
    var
        ItemTracking: Record "Item Tracking Code";
        LotNoInformation: Record "Lot No. Information";
    begin
        // Enforce strict FEFO picking order based on Lot Expiration Date
        LotNoInformation.SetCurrentKey("Expiration Date");
        LotNoInformation.SetAscending("Expiration Date", true);
        LotNoInformation.SetFilter("Expiration Date", '>=%1', WorkDate());

        if LotNoInformation.FindFirst() then begin
            // Lock pick line to earliest expiring Lot Number
            WarehouseActivityHeader."FEFO Enforced" := true;
        end;
    end;

    [EventSubscriber(ObjectType::Table, Database::"Item Journal Line", 'OnBeforeItemJournalPost', '', false, false)]
    local procedure PreventExpiredItemPosting(var ItemJournalLine: Record "Item Journal Line")
    var
        ReservationEntry: Record "Reservation Entry";
    begin
        if ItemJournalLine."Entry Type" = ItemJournalLine."Entry Type"::Sale then begin
            ReservationEntry.SetRange("Source ID", ItemJournalLine."Document No.");
            if ReservationEntry.FindFirst() then begin
                if (ReservationEntry."Expiration Date" <> 0D) and (ReservationEntry."Expiration Date" < WorkDate()) then
                    Error('FEFO Violation: Lot %1 for Item %2 expired on %3 and cannot be sold.',
                        ReservationEntry."Lot No.",
                        ItemJournalLine."Item No.",
                        ReservationEntry."Expiration Date");
            end;
        end;
    end;
}

Catch-weight

A case of chicken is sold as one case and priced by the kilogram. Both numbers are real, and they are not derivable from each other — the actual weight of that case is what the customer is charged for.

So the system has to carry two units through every step: inventory in cases, valuation and invoicing in weight, captured from the scale at pick. Implementations that store a nominal weight per case and invoice from it produce a margin error on every single line, in whichever direction the nominal is wrong.

Markdowns before the write-off

Stock approaching expiry has a value that falls to zero on a known date. The only question is how much is recovered before then.

  1. Set the markdown trigger by category — dairy and produce need different windows.
  2. Trigger on days remaining, not on a manual review that competes with everything else in a store.
  3. Report recovery rate: value sold at markdown against value written off. That ratio is the whole measure of whether the policy works.
A markdown taken three days early costs margin. One taken a day late costs the entire item plus the disposal. The asymmetry is the argument for automating the trigger.

Across stores

Multi-site grocery adds one more move worth having: transferring short-dated stock from a store where it will not sell to one where it will. This only works if expiry is visible at the batch level across sites, which is the same data FEFO already requires.