Metadata-Version: 2.4
Name: logisticspy
Version: 0.2.0
Summary: A Python toolkit for logistics and supply chain calculations
Author: krishnanz550i-cmyk
License: MIT
Project-URL: Homepage, https://github.com/krishnanz550i-cmyk/logisticspy
Project-URL: Issues, https://github.com/krishnanz550i-cmyk/logisticspy/issues
Keywords: logistics,supply chain,freight,chargeable weight,uom,unit of measure,packaging,warehouse,inventory,grn
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# 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

```bash
pip install logisticspy
```

## Quick start

You can use the chargeable weight tools either via the top-level package
or via the `weight` module:

```python
# 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 |

```python
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:

```python
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`).

```python
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:

```python
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

```python
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:

```python
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

```python
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_sale` for 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
