A Python toolkit for logistics and supply chain calculations
Project description
logisticspy
A Python toolkit for logistics and supply chain calculations.
logisticspy is a growing collection of clean, well-tested tools for
common logistics and supply chain problems. The first module,
logisticspy.weight, calculates volumetric (dimensional) weight and
chargeable weight for air, courier, sea, road, and rail freight shipments.
More tools (volume, freight, inventory, and others) will be added over
time.
Install
pip install logisticspy
Quick start
You can use the chargeable weight tools either via the top-level package
or via the weight module:
# Option 1: top-level import
import logisticspy
result = logisticspy.calculate(
length=60, width=40, height=40, unit="cm",
actual_weight=18, weight_unit="kg",
mode="air",
)
# Option 2: import the weight module
from logisticspy.weight import chargeable_weight
result = chargeable_weight.calculate(
length=60, width=40, height=40, unit="cm",
actual_weight=18, weight_unit="kg",
mode="air",
)
print(result.volumetric_weight_kg) # 19.2
print(result.chargeable_weight_kg) # 19.2
print(result.basis) # "volumetric"
The weight module: chargeable weight
Carriers bill shipments based on whichever is greater: the actual weight or the volumetric weight (calculated from package dimensions). This module implements that calculation cleanly, with support for multiple units, transport modes, named divisor presets, and multi-package consignments.
Supported modes and default divisors
| Mode | Default divisor (cm³/kg) |
|---|---|
air |
6000 |
courier |
5000 |
road |
3000 |
rail |
3000 |
sea |
N/A (uses CBM × 1000, see below) |
These are sensible starting defaults based on common conventions seen across the freight and parcel industry. They are not tied to any specific carrier - always confirm the applicable divisor with your own carrier or contract for billing-critical calculations.
Divisor presets
Two divisor values - 5000 and 6000 - are both widely used across the industry, often for different services, regions, or contracts (sometimes even by the same carrier depending on the product). Rather than guessing which one applies to your situation, you can refer to them by generic preset labels and swap between them easily:
| Preset | Divisor |
|---|---|
"a" |
5000 |
"b" |
6000 |
import logisticspy
# Same package, two different divisor conventions
pkg = dict(length=60, width=40, height=40, actual_weight=10, mode="air")
result_a = logisticspy.calculate(**pkg, divisor_preset="a") # divisor 5000
result_b = logisticspy.calculate(**pkg, divisor_preset="b") # divisor 6000
print(result_a.volumetric_weight_kg) # 19.2
print(result_b.volumetric_weight_kg) # 16.0
This makes it easy to compare "what would this shipment cost under each
convention" without hardcoding either value, and to plug in your own
carrier's documented divisor (whether that happens to be 5000, 6000, or
something else entirely) via divisor_preset or a raw divisor= value.
You can also pass an explicit divisor directly, which overrides any preset:
result = logisticspy.calculate(**pkg, divisor=4500)
Units
Input units
Dimensions accept cm, m, mm, in, ft (default cm).
Weights accept kg, g, lb, oz (default kg).
result = logisticspy.calculate(
length=20, width=15, height=10, unit="in",
actual_weight=5, weight_unit="lb",
mode="courier",
)
Output units (always normalized)
Regardless of the input units you choose, all results are returned in a single fixed unit system:
| Field | Unit |
|---|---|
actual_weight_kg, volumetric_weight_kg, chargeable_weight_kg |
kilograms (kg) |
volume_m3 |
cubic meters (m³) |
The input units (unit, weight_unit) are only used to interpret the
numbers you pass in - they are converted to centimeters and kilograms
internally before any calculation happens. The output is never expressed
back in the input units.
If you need the result in a different unit (e.g. pounds), convert the returned kg value yourself - the library does not provide unit conversion on outputs.
Sea freight (CBM)
Sea freight chargeable weight is derived from volume in cubic meters (CBM), using the common 1 CBM ≈ 1000 kg convention:
result = logisticspy.calculate(
length=1, width=1, height=1, unit="m",
actual_weight=500, mode="sea",
)
print(result.volumetric_weight_kg) # 1000.0
Divisor presets are ignored for sea mode, since it uses a CBM-based calculation rather than a divisor.
Multi-package consignments
import logisticspy
packages = [
{"length": 50, "width": 40, "height": 40, "actual_weight": 10},
{"length": 60, "width": 40, "height": 40, "actual_weight": 25, "quantity": 2},
]
result = logisticspy.calculate_consignment(packages, mode="air")
print(result.total_actual_weight_kg)
print(result.total_volumetric_weight_kg)
print(result.total_chargeable_weight_kg)
By default, totals are compared (sum(actual) vs sum(volumetric)).
Some couriers calculate chargeable weight per package and sum those - use
per_piece=True for that behavior:
result = logisticspy.calculate_consignment(packages, mode="air", per_piece=True)
The packwise module: UOM conversion & packaging consolidation
packwise solves a fundamental logistics problem: goods are often
purchased in one unit of measure (pallets, cartons, bulk) but sold
in another (loose units, packs, kilograms). It always stores stock
internally in base units (the smallest unit in the hierarchy), so all
conversions and consolidations are computed from a single number with no
rounding drift across levels.
Quick start
from decimal import Decimal
from logisticspy.packwise import (
discrete_standard, PALLET, EACH,
ProductUOMConfig, GRNBehavior, StockLedger,
)
# Pallet -> Carton -> Box -> Each (defaults: 10 / 12 / 6 -> 720 EA per pallet)
hierarchy = discrete_standard()
cfg = ProductUOMConfig(
sku="OIL-500ML",
hierarchy=hierarchy,
purchase_uom=PALLET, # received as pallets
sale_uom=EACH, # sold as individual bottles
grn_behavior=GRNBehavior.CONSOLIDATE_UP,
)
ledger = StockLedger()
ledger.register(cfg)
# Receive 1 pallet, then sell 100 loose units
ledger.receive_grn("OIL-500ML", [(PALLET, Decimal("1"))], reference="GRN-001")
ledger.fulfil_sale("OIL-500ML", Decimal("100"), reference="SO-001")
print(ledger.stock_level("OIL-500ML").base_qty) # 620
print(ledger.stock_level("OIL-500ML").breakdown()) # [(CTN, 8), (BOX, 7), (EA, 2)]
Preset hierarchies
Five ready-to-use presets cover the most common industries (all factors are overridable):
| Preset | Hierarchy | Default factors | Base unit |
|---|---|---|---|
discrete_standard() |
PLT → CTN → BOX → EA | 10 / 12 / 6 | Each |
dry_bulk() |
PLT → BAG → KG → G | 40 / 25 / 1000 | Gram (decimal) |
liquid_bulk() |
PLT → DRUM → L → ML | 4 / 200 / 1000 | mL (decimal) |
apparel() |
PLT → CTN → PACK → PC | 20 / 10 / 5 | Piece |
pharma() |
PLT → SHIP → INNER → STRIP → TAB | 20 / 12 / 10 / 10 | Tablet |
What's included
UOMHierarchy— model any parent→child packaging chain with conversion factors; convert between any two levels.ProductUOMConfig/GRNBehavior— per-SKU purchase/sale UOMs and goods-receipt handling (NORMALIZE_TO_BASE,CONSOLIDATE_UP,KEEP_AS_IS,CUSTOM), with per-SKU factor overrides.- Stateless converters —
convert,consolidate_loose,process_grn_line,split_for_salefor one-off calculations. StockLedger— stateful inventory in base units with a full audit trail (GRN, sale, transfer, adjustment).PackwisePlugin— hooks that turn logisticspy GRN / PO / SO / stock transfer documents into ledger movements.
Stock is tracked with decimal.Decimal throughout for exact arithmetic.
Roadmap
logisticspy is designed to grow into a broader logistics toolkit. It
currently ships two modules — weight (chargeable / volumetric weight)
and packwise (UOM conversion & packaging consolidation). Additional
tools (e.g. volume and freight calculations) will be added over time.
Disclaimer
This library implements widely-used industry conventions for illustrative and estimation purposes. Divisors and CBM ratios vary by carrier, service level, region, and contract terms. Always confirm exact billing methodology with your carrier or freight forwarder for invoicing-critical calculations.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file logisticspy-0.2.0.tar.gz.
File metadata
- Download URL: logisticspy-0.2.0.tar.gz
- Upload date:
- Size: 33.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20ff8a031f7017fcdea532dd291a8a894743ffd2fe23595339c4ee5e60d9d539
|
|
| MD5 |
6308851011552717db72a8aedc61a30b
|
|
| BLAKE2b-256 |
e7c1929de2c05f019f3cbd2b17d4fd589089ebc5594cd6ff4b099379cbd42897
|
Provenance
The following attestation bundles were made for logisticspy-0.2.0.tar.gz:
Publisher:
publish.yml on krishnanz550i-cmyk/logisticspy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
logisticspy-0.2.0.tar.gz -
Subject digest:
20ff8a031f7017fcdea532dd291a8a894743ffd2fe23595339c4ee5e60d9d539 - Sigstore transparency entry: 1840658487
- Sigstore integration time:
-
Permalink:
krishnanz550i-cmyk/logisticspy@fc9c010ab35321843781714e1217968681749765 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/krishnanz550i-cmyk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fc9c010ab35321843781714e1217968681749765 -
Trigger Event:
release
-
Statement type:
File details
Details for the file logisticspy-0.2.0-py3-none-any.whl.
File metadata
- Download URL: logisticspy-0.2.0-py3-none-any.whl
- Upload date:
- Size: 31.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f89080c5a6d67a1ed62aad8e91f218544dabfd22db152f85596d0c9b48b4f626
|
|
| MD5 |
5dde6695c8b0472a228154a96874ec8d
|
|
| BLAKE2b-256 |
2140a4dceb79148f60c50365ff9f93b40c4d4fd30444601e2128342620f2f6e4
|
Provenance
The following attestation bundles were made for logisticspy-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on krishnanz550i-cmyk/logisticspy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
logisticspy-0.2.0-py3-none-any.whl -
Subject digest:
f89080c5a6d67a1ed62aad8e91f218544dabfd22db152f85596d0c9b48b4f626 - Sigstore transparency entry: 1840658521
- Sigstore integration time:
-
Permalink:
krishnanz550i-cmyk/logisticspy@fc9c010ab35321843781714e1217968681749765 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/krishnanz550i-cmyk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fc9c010ab35321843781714e1217968681749765 -
Trigger Event:
release
-
Statement type: