Landed Cost in ERP: True Import Cost for Indonesian Trade

Photo by dok1 on flickr
Landed cost is the true total cost of an imported item once it reaches the warehouse, including its purchase price plus freight, insurance, import duty, and other acquisition charges. Recording only the supplier invoice price without these layers understates inventory value and overstates gross margin on every imported SKU.
Bea masuk is calculated as the CIF value, meaning the FOB price plus freight and insurance, multiplied by the duty rate assigned to the item's HS code in the official tariff schedule. Rates vary widely by product category and can be reduced or eliminated under preferential trade agreements, so the HS code classification must be verified before the rate is applied.
PPN impor is a value-added tax that becomes a recoverable input tax credit offset against output VAT in the monthly VAT return, unlike freight or duty, which are permanent, irrecoverable costs of acquiring the goods. Including PPN impor in landed cost overstates inventory value and understates a tax asset the business can actually reclaim.
Pick an apportionment key such as each line's share of total FOB value, weight, or volume, then multiply that share by every shipment-level charge (freight, insurance, duty) and add the result to the line's own FOB cost. The right key depends on what actually drives the cost: weight usually fits ocean or air freight, while value often fits insurance premiums.
Yes, customs can issue a post-clearance correction known as an SPTNP that revises the customs value or duty rate after the goods have been released. Most ERPs post a provisional landed cost at goods receipt using the declared PIB figures and then book a true-up adjustment against inventory or cost of goods sold once the final assessment arrives.

Photo by dok1 on flickr
Key Takeaway
Landed cost in ERP means capturing freight, insurance, bea masuk import duty, and PPN impor as first-class shipment data, then apportioning them across every purchase line by FOB value, weight, or volume so each imported item carries its true unit cost, while PPN impor stays out of inventory as a recoverable input tax credit.
Every importer eventually learns the hard way that the price on a supplier invoice is not what the goods actually cost once they land on a warehouse shelf. Freight, marine insurance, bea masuk import duty, and PPN impor stack on top of the purchase price, and none of it shows up automatically on a purchase order. If your ERP only records the invoice amount, your inventory valuation, cost of goods sold, and gross margin are all quietly wrong from the moment goods are received.
This post walks through building a landed cost calculation into a custom ERP for Indonesian trade: how to move from FOB price to a dutiable CIF value, how bea masuk and PPN impor are layered on top, how to apportion shipment-level charges across many purchase lines fairly, and why one of those charges should never touch your inventory value at all. The examples use a worked shipment in rupiah so every formula has a concrete number behind it.
A purchase order captures a supplier's selling price, but an import shipment carries several more cost layers before the goods are usable inventory. If those layers are expensed separately instead of being capitalized into the item cost, unit margins on imported SKUs look better than they actually are, and management ends up pricing products against a cost that was never real. The four layers that most commonly need to be captured are:
Capture landed cost at the goods receipt step, not when the freight forwarder's invoice finally arrives weeks later. Post a provisional estimate against the shipment as soon as the bill of lading and customs declaration are available, then true it up once the actual bills land. Waiting for every invoice before touching inventory value means your margin reports are wrong for the entire lag period.
Bea masuk and PPN impor are not calculated on the supplier's invoice price. Indonesian customs assesses both against the customs value, which in practice is almost always the CIF value: the free-on-board price of the goods plus the freight and insurance needed to get them to the Indonesian port. Getting this base value right is the first step, because every downstream tax and duty figure is a percentage of it.
Under the CIF Incoterm, the seller pays for carriage and insurance up to the named port of destination, while the buyer takes on unloading costs, import duty, and tax from that point forward. Even when a shipment is bought on a different Incoterm such as FOB, an ERP still needs to reconstruct a CIF-equivalent value for customs purposes by adding the actual freight and insurance the importer paid to get the goods to port. That reconstructed CIF value, not the invoice total, is the number every subsequent calculation is built on.
| Component | Formula | Amount (IDR) |
|---|---|---|
| CIF value | FOB price + freight + insurance | Rp 555,000,000 |
| Bea masuk (import duty) | CIF value times duty rate (7.5% in this example) | Rp 41,625,000 |
| Import value | CIF value + bea masuk | Rp 596,625,000 |
| PPN impor (recoverable) | Import value times 11% VAT rate | Rp 65,628,750 |
| Landed cost for inventory | CIF value + bea masuk, excluding recoverable PPN | Rp 596,625,000 |
Bea masuk rates are set per HS code in the official tariff book maintained by the Directorate General of Customs and Excise, and they range widely depending on the goods and whether a preferential trade agreement applies. PPN impor is then charged on the import value, which is the CIF value plus the duty just calculated, at the standard VAT rate. Separately, PPh Article 22 withholding tax is collected at import, at a lower rate for holders of an importer identification number and a higher rate for those without one. None of this happens on the purchase order; it happens on the customs declaration, so an ERP needs a place to record it against the shipment rather than the individual item.
Bea masuk rates depend entirely on correct HS code classification, and a wrong code can mean a wrong duty rate, a wrong landed cost, and a compliance problem with customs. Do not hard-code a duty rate into item master data and assume it will stay correct. Validate every HS code and rate against the current official tariff schedule before it drives a landed cost calculation, and flag shipments where the classification is uncertain for manual review rather than letting the system silently apply a default rate.
A single import shipment almost always carries several SKUs on one bill of lading, but freight, insurance, and duty are assessed against the whole shipment, not against each line item individually. The ERP has to take those shipment-level totals and split them fairly across every purchase line so each item ends up with its own true unit cost. That apportionment logic is the actual engineering problem behind landed cost, and it typically works in four steps.
-- purchase_lines: goods received on a single import shipment
CREATE TABLE purchase_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shipment_id UUID NOT NULL REFERENCES import_shipments(id),
item_id UUID NOT NULL REFERENCES items(id),
quantity NUMERIC(15,4) NOT NULL,
unit_cost_fob NUMERIC(15,2) NOT NULL, -- FOB price per unit, origin currency
weight_kg NUMERIC(15,4) NOT NULL,
line_fob_idr NUMERIC(15,2) NOT NULL -- quantity * unit_cost_fob, converted to IDR
);
-- import_shipments: freight, insurance, duty and PPN captured once per shipment
CREATE TABLE import_shipments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
freight_idr NUMERIC(15,2) NOT NULL,
insurance_idr NUMERIC(15,2) NOT NULL,
duty_rate NUMERIC(6,4) NOT NULL, -- e.g. 0.075 for 7.5% bea masuk
ppn_rate NUMERIC(6,4) NOT NULL DEFAULT 0.11,
other_fees_idr NUMERIC(15,2) NOT NULL DEFAULT 0
);
// NestJS: apportion shipment-level costs across lines by FOB value share
async function apportionLandedCost(shipmentId: string): Promise<LandedCostLine[]> {
const shipment = await this.shipmentRepo.findOneOrFail({ where: { id: shipmentId } });
const lines = await this.lineRepo.find({ where: { shipmentId } });
const totalFob = lines.reduce((sum, l) => sum + l.lineFobIdr, 0);
const cifBase = totalFob + shipment.freightIdr + shipment.insuranceIdr;
const dutyTotal = cifBase * shipment.dutyRate;
const ppnBase = cifBase + dutyTotal;
const ppnTotal = ppnBase * shipment.ppnRate;
return lines.map((line) => {
const share = line.lineFobIdr / totalFob; // apportionment key
const freightShare = shipment.freightIdr * share;
const insuranceShare = shipment.insuranceIdr * share;
const dutyShare = dutyTotal * share;
const otherShare = shipment.otherFeesIdr * share;
// PPN import is a recoverable input credit, not part of inventory value
const landedUnitCost =
(line.lineFobIdr + freightShare + insuranceShare + dutyShare + otherShare) /
line.quantity;
return { itemId: line.itemId, landedUnitCost };
});
}PPN impor is a value-added tax paid at the border, but it is not a permanent cost of the goods the way freight or duty is. It becomes a Pajak Masukan, an input tax credit, that the importer offsets against output VAT collected on sales in the monthly VAT return. Folding it into landed cost and therefore into inventory value overstates what the goods actually cost the business and understates a tax asset that is genuinely recoverable within the same accounting period in most cases.
Keep bea masuk, freight, and insurance inside the landed cost calculation because they are irrecoverable costs of acquiring the goods, and keep PPN impor entirely outside it, posted instead to a VAT input account. Getting this separation right is what keeps inventory valuation and cost of goods sold defensible when an auditor or the tax office asks how a unit cost was derived.
Not every shipment-level cost should be split the same way, because different costs are driven by different physical realities of the shipment.
The schema and apportionment function above keep the shipment-level charges on one header row and let every purchase line reference that shipment, so the calculation only needs to run once per shipment and fan out to every line proportionally. The key design decision is to keep the apportionment function idempotent: it should be safe to re-run against the same shipment after an input changes, recomputing every line's landed unit cost from scratch rather than incrementally adjusting a running total, which is where double-posting bugs usually creep in.
Design the recalculation as a full re-derivation keyed by shipment ID, not an incremental patch. When the freight forwarder's actual invoice replaces the provisional freight estimate, or customs issues a correction to the duty assessment, re-running the same function against the updated shipment header should produce a clean new landed unit cost for every line, and the difference against the previous cost becomes a single traceable inventory or cost-of-goods-sold adjustment.
Bea masuk on the import declaration is often provisional in practice, because customs can issue a post-clearance correction, known as an SPTNP, that revises the assessed customs value or duty rate after the goods have already been released. Most ERPs handle this by posting an estimated landed cost at goods receipt using the declared PIB figures, then booking a true-up adjustment once the final assessment or the freight forwarder's actual invoice arrives. Whether that adjustment lands on inventory or on cost of goods sold depends on whether the stock is still on hand or has already been sold, so the ERP needs to check current quantity on hand for the item before deciding where the correction posts.
None of this is complicated math on its own, but it only works if the ERP treats freight, insurance, duty, and tax as first-class data attached to the shipment rather than afterthoughts bolted onto a purchase order. Get the apportionment key right, keep PPN impor out of inventory value, and build the recalculation to be idempotent, and the landed cost your system reports will hold up whether the audience is a margin dashboard, an auditor, or the tax office.