logisticspy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- logisticspy/__init__.py +43 -0
- logisticspy/weight/__init__.py +28 -0
- logisticspy/weight/chargeable_weight.py +291 -0
- logisticspy/weight/divisors.py +72 -0
- logisticspy-0.1.0.dist-info/METADATA +220 -0
- logisticspy-0.1.0.dist-info/RECORD +9 -0
- logisticspy-0.1.0.dist-info/WHEEL +5 -0
- logisticspy-0.1.0.dist-info/licenses/LICENSE +21 -0
- logisticspy-0.1.0.dist-info/top_level.txt +1 -0
logisticspy/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
logisticspy: A Python toolkit for logistics and supply chain calculations.
|
|
3
|
+
|
|
4
|
+
The first module, ``logisticspy.weight``, calculates volumetric (dimensional)
|
|
5
|
+
and chargeable weight for air, courier, sea, road, and rail freight shipments.
|
|
6
|
+
More tools will be added over time.
|
|
7
|
+
|
|
8
|
+
Top-level convenience imports let you do either of:
|
|
9
|
+
|
|
10
|
+
import logisticspy
|
|
11
|
+
logisticspy.calculate(...)
|
|
12
|
+
|
|
13
|
+
from logisticspy.weight import chargeable_weight
|
|
14
|
+
chargeable_weight.calculate(...)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .weight import (
|
|
18
|
+
calculate,
|
|
19
|
+
calculate_consignment,
|
|
20
|
+
WeightResult,
|
|
21
|
+
ConsolidatedResult,
|
|
22
|
+
Mode,
|
|
23
|
+
DEFAULT_DIVISORS,
|
|
24
|
+
DIVISOR_PRESETS,
|
|
25
|
+
UnsupportedUnitError,
|
|
26
|
+
UnsupportedModeError,
|
|
27
|
+
UnsupportedPresetError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"calculate",
|
|
34
|
+
"calculate_consignment",
|
|
35
|
+
"WeightResult",
|
|
36
|
+
"ConsolidatedResult",
|
|
37
|
+
"Mode",
|
|
38
|
+
"DEFAULT_DIVISORS",
|
|
39
|
+
"DIVISOR_PRESETS",
|
|
40
|
+
"UnsupportedUnitError",
|
|
41
|
+
"UnsupportedModeError",
|
|
42
|
+
"UnsupportedPresetError",
|
|
43
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
logisticspy.weight: Calculate volumetric (dimensional) and chargeable weight
|
|
3
|
+
for air, courier, sea, road, and rail freight shipments.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .chargeable_weight import (
|
|
7
|
+
calculate,
|
|
8
|
+
calculate_consignment,
|
|
9
|
+
WeightResult,
|
|
10
|
+
ConsolidatedResult,
|
|
11
|
+
UnsupportedUnitError,
|
|
12
|
+
UnsupportedModeError,
|
|
13
|
+
UnsupportedPresetError,
|
|
14
|
+
)
|
|
15
|
+
from .divisors import Mode, DEFAULT_DIVISORS, DIVISOR_PRESETS
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"calculate",
|
|
19
|
+
"calculate_consignment",
|
|
20
|
+
"WeightResult",
|
|
21
|
+
"ConsolidatedResult",
|
|
22
|
+
"Mode",
|
|
23
|
+
"DEFAULT_DIVISORS",
|
|
24
|
+
"DIVISOR_PRESETS",
|
|
25
|
+
"UnsupportedUnitError",
|
|
26
|
+
"UnsupportedModeError",
|
|
27
|
+
"UnsupportedPresetError",
|
|
28
|
+
]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core calculation logic for volumetric (dimensional) and chargeable weight.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
from .divisors import (
|
|
9
|
+
Mode,
|
|
10
|
+
DEFAULT_DIVISORS,
|
|
11
|
+
DIVISOR_PRESETS,
|
|
12
|
+
SEA_CBM_TO_KG,
|
|
13
|
+
LENGTH_TO_CM,
|
|
14
|
+
WEIGHT_TO_KG,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UnsupportedUnitError(ValueError):
|
|
19
|
+
"""Raised when an unrecognized unit is supplied."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnsupportedModeError(ValueError):
|
|
23
|
+
"""Raised when an unrecognized transport mode is supplied."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UnsupportedPresetError(ValueError):
|
|
27
|
+
"""Raised when an unrecognized divisor preset is supplied."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class WeightResult:
|
|
32
|
+
"""Result of a chargeable weight calculation for a single package."""
|
|
33
|
+
|
|
34
|
+
actual_weight_kg: float
|
|
35
|
+
volumetric_weight_kg: float
|
|
36
|
+
chargeable_weight_kg: float
|
|
37
|
+
basis: str # "actual" or "volumetric"
|
|
38
|
+
volume_m3: float
|
|
39
|
+
mode: str
|
|
40
|
+
divisor: Optional[float] = None # None for sea (CBM-based)
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
return {
|
|
44
|
+
"actual_weight_kg": round(self.actual_weight_kg, 6),
|
|
45
|
+
"volumetric_weight_kg": round(self.volumetric_weight_kg, 6),
|
|
46
|
+
"chargeable_weight_kg": round(self.chargeable_weight_kg, 6),
|
|
47
|
+
"basis": self.basis,
|
|
48
|
+
"volume_m3": round(self.volume_m3, 6),
|
|
49
|
+
"mode": self.mode,
|
|
50
|
+
"divisor": self.divisor,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ConsolidatedResult:
|
|
56
|
+
"""Result of a chargeable weight calculation across multiple packages."""
|
|
57
|
+
|
|
58
|
+
packages: List[WeightResult]
|
|
59
|
+
total_actual_weight_kg: float
|
|
60
|
+
total_volumetric_weight_kg: float
|
|
61
|
+
total_chargeable_weight_kg: float
|
|
62
|
+
basis: str
|
|
63
|
+
mode: str
|
|
64
|
+
per_piece: bool
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict:
|
|
67
|
+
return {
|
|
68
|
+
"packages": [p.to_dict() for p in self.packages],
|
|
69
|
+
"total_actual_weight_kg": round(self.total_actual_weight_kg, 6),
|
|
70
|
+
"total_volumetric_weight_kg": round(self.total_volumetric_weight_kg, 6),
|
|
71
|
+
"total_chargeable_weight_kg": round(self.total_chargeable_weight_kg, 6),
|
|
72
|
+
"basis": self.basis,
|
|
73
|
+
"mode": self.mode,
|
|
74
|
+
"per_piece": self.per_piece,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _to_cm(value: float, unit: str) -> float:
|
|
79
|
+
unit = unit.lower()
|
|
80
|
+
if unit not in LENGTH_TO_CM:
|
|
81
|
+
raise UnsupportedUnitError(
|
|
82
|
+
f"Unsupported length unit '{unit}'. "
|
|
83
|
+
f"Supported units: {sorted(LENGTH_TO_CM)}"
|
|
84
|
+
)
|
|
85
|
+
return value * LENGTH_TO_CM[unit]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _to_kg(value: float, unit: str) -> float:
|
|
89
|
+
unit = unit.lower()
|
|
90
|
+
if unit not in WEIGHT_TO_KG:
|
|
91
|
+
raise UnsupportedUnitError(
|
|
92
|
+
f"Unsupported weight unit '{unit}'. "
|
|
93
|
+
f"Supported units: {sorted(WEIGHT_TO_KG)}"
|
|
94
|
+
)
|
|
95
|
+
return value * WEIGHT_TO_KG[unit]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve_mode(mode) -> Mode:
|
|
99
|
+
if isinstance(mode, Mode):
|
|
100
|
+
return mode
|
|
101
|
+
try:
|
|
102
|
+
return Mode(str(mode).lower())
|
|
103
|
+
except ValueError:
|
|
104
|
+
raise UnsupportedModeError(
|
|
105
|
+
f"Unsupported mode '{mode}'. "
|
|
106
|
+
f"Supported modes: {[m.value for m in Mode]}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_divisor(
|
|
111
|
+
mode: Mode, divisor_preset: Optional[str], divisor: Optional[float]
|
|
112
|
+
) -> Optional[float]:
|
|
113
|
+
"""Resolve the divisor to use, following precedence:
|
|
114
|
+
explicit divisor > named preset > mode default.
|
|
115
|
+
Returns None for SEA mode (CBM-based, no divisor applies).
|
|
116
|
+
"""
|
|
117
|
+
if mode == Mode.SEA:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
if divisor is not None:
|
|
121
|
+
return divisor
|
|
122
|
+
|
|
123
|
+
if divisor_preset is not None:
|
|
124
|
+
key = divisor_preset.lower()
|
|
125
|
+
if key not in DIVISOR_PRESETS:
|
|
126
|
+
raise UnsupportedPresetError(
|
|
127
|
+
f"Unsupported divisor_preset '{divisor_preset}'. "
|
|
128
|
+
f"Supported presets: {sorted(DIVISOR_PRESETS)}"
|
|
129
|
+
)
|
|
130
|
+
return DIVISOR_PRESETS[key]
|
|
131
|
+
|
|
132
|
+
return DEFAULT_DIVISORS[mode]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def calculate(
|
|
136
|
+
length: float,
|
|
137
|
+
width: float,
|
|
138
|
+
height: float,
|
|
139
|
+
actual_weight: float,
|
|
140
|
+
mode,
|
|
141
|
+
unit: str = "cm",
|
|
142
|
+
weight_unit: str = "kg",
|
|
143
|
+
divisor_preset: Optional[str] = None,
|
|
144
|
+
divisor: Optional[float] = None,
|
|
145
|
+
) -> WeightResult:
|
|
146
|
+
"""
|
|
147
|
+
Calculate volumetric and chargeable weight for a single package.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
length, width, height: Package dimensions.
|
|
151
|
+
actual_weight: Physical (scale) weight of the package.
|
|
152
|
+
mode: Transport mode - "air", "courier", "sea", "road", or "rail"
|
|
153
|
+
(or a Mode enum value).
|
|
154
|
+
unit: Unit for dimensions. One of "cm", "m", "mm", "in", "ft".
|
|
155
|
+
Default "cm".
|
|
156
|
+
weight_unit: Unit for actual_weight. One of "kg", "g", "lb", "oz".
|
|
157
|
+
Default "kg".
|
|
158
|
+
divisor_preset: Optional named divisor preset ("a" = 5000,
|
|
159
|
+
"b" = 6000) to use instead of the mode default. These are
|
|
160
|
+
generic labels for two widely-used divisor values in the
|
|
161
|
+
freight/parcel industry - they are not tied to any specific
|
|
162
|
+
carrier. Ignored for sea mode.
|
|
163
|
+
divisor: Optional explicit divisor (cm^3 per kg) overriding both
|
|
164
|
+
the preset and mode default. Ignored for sea mode.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
WeightResult with actual, volumetric, and chargeable weights in kg,
|
|
168
|
+
plus the basis ("actual" or "volumetric") used for billing.
|
|
169
|
+
|
|
170
|
+
Notes:
|
|
171
|
+
- For sea freight, volumetric weight is derived from CBM
|
|
172
|
+
(cubic meters) x 1000, following the common "1 CBM = 1000 kg"
|
|
173
|
+
weight-or-measurement (W/M) convention. Some carriers use
|
|
174
|
+
different ratios or apply this only above certain thresholds.
|
|
175
|
+
- Divisors of 5000 and 6000 (presets "a" and "b") are both widely
|
|
176
|
+
used across the freight and parcel industry for different
|
|
177
|
+
modes, services, and regions. Always confirm the applicable
|
|
178
|
+
divisor with your specific carrier or contract for
|
|
179
|
+
billing-critical calculations.
|
|
180
|
+
"""
|
|
181
|
+
resolved_mode = _resolve_mode(mode)
|
|
182
|
+
|
|
183
|
+
if length <= 0 or width <= 0 or height <= 0:
|
|
184
|
+
raise ValueError("Dimensions must be positive numbers.")
|
|
185
|
+
if actual_weight < 0:
|
|
186
|
+
raise ValueError("actual_weight must be non-negative.")
|
|
187
|
+
|
|
188
|
+
length_cm = _to_cm(length, unit)
|
|
189
|
+
width_cm = _to_cm(width, unit)
|
|
190
|
+
height_cm = _to_cm(height, unit)
|
|
191
|
+
actual_weight_kg = _to_kg(actual_weight, weight_unit)
|
|
192
|
+
|
|
193
|
+
volume_cm3 = length_cm * width_cm * height_cm
|
|
194
|
+
volume_m3 = volume_cm3 / 1_000_000
|
|
195
|
+
|
|
196
|
+
resolved_divisor = _resolve_divisor(resolved_mode, divisor_preset, divisor)
|
|
197
|
+
|
|
198
|
+
if resolved_mode == Mode.SEA:
|
|
199
|
+
volumetric_weight_kg = volume_m3 * SEA_CBM_TO_KG
|
|
200
|
+
else:
|
|
201
|
+
volumetric_weight_kg = volume_cm3 / resolved_divisor
|
|
202
|
+
|
|
203
|
+
chargeable_weight_kg = max(actual_weight_kg, volumetric_weight_kg)
|
|
204
|
+
basis = "volumetric" if volumetric_weight_kg > actual_weight_kg else "actual"
|
|
205
|
+
|
|
206
|
+
return WeightResult(
|
|
207
|
+
actual_weight_kg=actual_weight_kg,
|
|
208
|
+
volumetric_weight_kg=volumetric_weight_kg,
|
|
209
|
+
chargeable_weight_kg=chargeable_weight_kg,
|
|
210
|
+
basis=basis,
|
|
211
|
+
volume_m3=volume_m3,
|
|
212
|
+
mode=resolved_mode.value,
|
|
213
|
+
divisor=resolved_divisor,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def calculate_consignment(
|
|
218
|
+
packages: List[dict],
|
|
219
|
+
mode,
|
|
220
|
+
unit: str = "cm",
|
|
221
|
+
weight_unit: str = "kg",
|
|
222
|
+
divisor_preset: Optional[str] = None,
|
|
223
|
+
divisor: Optional[float] = None,
|
|
224
|
+
per_piece: bool = False,
|
|
225
|
+
) -> ConsolidatedResult:
|
|
226
|
+
"""
|
|
227
|
+
Calculate chargeable weight across multiple packages in a consignment.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
packages: List of dicts, each with keys "length", "width", "height",
|
|
231
|
+
"actual_weight", and optionally "quantity" (default 1).
|
|
232
|
+
Dimensions/weights are interpreted using the shared `unit`
|
|
233
|
+
and `weight_unit` arguments unless overridden per-package
|
|
234
|
+
with "unit" / "weight_unit" keys.
|
|
235
|
+
mode, unit, weight_unit, divisor_preset, divisor: See `calculate()`.
|
|
236
|
+
per_piece: If True, chargeable weight is computed per package
|
|
237
|
+
(actual vs volumetric compared individually, then summed -
|
|
238
|
+
a convention some couriers use). If False (default), totals
|
|
239
|
+
of actual and volumetric weight are summed separately first,
|
|
240
|
+
and chargeable weight is the max of the two totals.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
ConsolidatedResult with per-package results and consignment totals.
|
|
244
|
+
"""
|
|
245
|
+
if not packages:
|
|
246
|
+
raise ValueError("packages list must not be empty.")
|
|
247
|
+
|
|
248
|
+
results: List[WeightResult] = []
|
|
249
|
+
|
|
250
|
+
for pkg in packages:
|
|
251
|
+
quantity = pkg.get("quantity", 1)
|
|
252
|
+
if quantity <= 0:
|
|
253
|
+
raise ValueError("quantity must be a positive integer.")
|
|
254
|
+
|
|
255
|
+
pkg_unit = pkg.get("unit", unit)
|
|
256
|
+
pkg_weight_unit = pkg.get("weight_unit", weight_unit)
|
|
257
|
+
|
|
258
|
+
result = calculate(
|
|
259
|
+
length=pkg["length"],
|
|
260
|
+
width=pkg["width"],
|
|
261
|
+
height=pkg["height"],
|
|
262
|
+
actual_weight=pkg["actual_weight"],
|
|
263
|
+
mode=mode,
|
|
264
|
+
unit=pkg_unit,
|
|
265
|
+
weight_unit=pkg_weight_unit,
|
|
266
|
+
divisor_preset=divisor_preset,
|
|
267
|
+
divisor=divisor,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
for _ in range(quantity):
|
|
271
|
+
results.append(result)
|
|
272
|
+
|
|
273
|
+
total_actual = sum(r.actual_weight_kg for r in results)
|
|
274
|
+
total_volumetric = sum(r.volumetric_weight_kg for r in results)
|
|
275
|
+
|
|
276
|
+
if per_piece:
|
|
277
|
+
total_chargeable = sum(r.chargeable_weight_kg for r in results)
|
|
278
|
+
else:
|
|
279
|
+
total_chargeable = max(total_actual, total_volumetric)
|
|
280
|
+
|
|
281
|
+
overall_basis = "volumetric" if total_volumetric > total_actual else "actual"
|
|
282
|
+
|
|
283
|
+
return ConsolidatedResult(
|
|
284
|
+
packages=results,
|
|
285
|
+
total_actual_weight_kg=total_actual,
|
|
286
|
+
total_volumetric_weight_kg=total_volumetric,
|
|
287
|
+
total_chargeable_weight_kg=total_chargeable,
|
|
288
|
+
basis=overall_basis,
|
|
289
|
+
mode=_resolve_mode(mode).value,
|
|
290
|
+
per_piece=per_piece,
|
|
291
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Volumetric (dimensional) weight divisors and conversion factors.
|
|
3
|
+
|
|
4
|
+
Divisors represent cubic cm per kg.
|
|
5
|
+
Volumetric weight = volume / divisor.
|
|
6
|
+
|
|
7
|
+
Sea freight is handled differently: chargeable weight is typically based
|
|
8
|
+
on CBM (cubic meters), with 1 CBM commonly treated as equivalent to
|
|
9
|
+
1000 kg (i.e. volume_m3 * 1000 -> kg), under a "weight or measurement"
|
|
10
|
+
(W/M) basis.
|
|
11
|
+
|
|
12
|
+
Common divisor values seen across the freight and parcel industry:
|
|
13
|
+
- 5000: a widely-used divisor for parcel/express-style shipments
|
|
14
|
+
- 6000: a widely-used divisor for general air cargo
|
|
15
|
+
- Road/rail freight commonly uses 3000-5000 depending on region and mode
|
|
16
|
+
|
|
17
|
+
These values vary by carrier, service level, region, and contract terms.
|
|
18
|
+
Always confirm the applicable divisor with your specific carrier for
|
|
19
|
+
billing-critical calculations.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from enum import Enum
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Mode(str, Enum):
|
|
26
|
+
AIR = "air"
|
|
27
|
+
COURIER = "courier"
|
|
28
|
+
SEA = "sea"
|
|
29
|
+
ROAD = "road"
|
|
30
|
+
RAIL = "rail"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Default divisors in cm^3 per kg, applied as: volumetric_weight_kg = volume_cm3 / divisor
|
|
34
|
+
# SEA mode is handled separately via CBM calculation (see calculator.py)
|
|
35
|
+
DEFAULT_DIVISORS = {
|
|
36
|
+
Mode.AIR: 6000,
|
|
37
|
+
Mode.COURIER: 5000,
|
|
38
|
+
Mode.ROAD: 3000,
|
|
39
|
+
Mode.RAIL: 3000,
|
|
40
|
+
# Mode.SEA intentionally omitted - handled via CBM x 1000
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Named divisor presets, for users who want to refer to common values by
|
|
44
|
+
# label rather than remembering raw numbers. These are generic industry
|
|
45
|
+
# conventions, not tied to any specific carrier - always confirm the
|
|
46
|
+
# applicable value with your own carrier or contract.
|
|
47
|
+
DIVISOR_PRESETS = {
|
|
48
|
+
"a": 5000,
|
|
49
|
+
"b": 6000,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Sea freight: 1 cubic meter (CBM) is conventionally treated as
|
|
53
|
+
# equivalent to 1000 kg for chargeable weight purposes (the "W/M" -
|
|
54
|
+
# weight or measurement - basis many ocean carriers use as a reference).
|
|
55
|
+
SEA_CBM_TO_KG = 1000
|
|
56
|
+
|
|
57
|
+
# Length unit conversions to centimeters
|
|
58
|
+
LENGTH_TO_CM = {
|
|
59
|
+
"cm": 1.0,
|
|
60
|
+
"m": 100.0,
|
|
61
|
+
"mm": 0.1,
|
|
62
|
+
"in": 2.54,
|
|
63
|
+
"ft": 30.48,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Weight unit conversions to kilograms
|
|
67
|
+
WEIGHT_TO_KG = {
|
|
68
|
+
"kg": 1.0,
|
|
69
|
+
"g": 0.001,
|
|
70
|
+
"lb": 0.45359237,
|
|
71
|
+
"oz": 0.0283495231,
|
|
72
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logisticspy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python toolkit for logistics and supply chain calculations
|
|
5
|
+
Author: krishnanz550i-cmyk
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/krishnanz550i-cmyk/logisticspy
|
|
8
|
+
Project-URL: Issues, https://github.com/krishnanz550i-cmyk/logisticspy/issues
|
|
9
|
+
Keywords: logistics,supply chain,freight,chargeable weight
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Office/Business
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# logisticspy
|
|
24
|
+
|
|
25
|
+
A Python toolkit for logistics and supply chain calculations.
|
|
26
|
+
|
|
27
|
+
`logisticspy` is a growing collection of clean, well-tested tools for
|
|
28
|
+
common logistics and supply chain problems. The first module,
|
|
29
|
+
**`logisticspy.weight`**, calculates volumetric (dimensional) weight and
|
|
30
|
+
chargeable weight for air, courier, sea, road, and rail freight shipments.
|
|
31
|
+
More tools (volume, freight, inventory, and others) will be added over
|
|
32
|
+
time.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install logisticspy
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
You can use the chargeable weight tools either via the top-level package
|
|
43
|
+
or via the `weight` module:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Option 1: top-level import
|
|
47
|
+
import logisticspy
|
|
48
|
+
|
|
49
|
+
result = logisticspy.calculate(
|
|
50
|
+
length=60, width=40, height=40, unit="cm",
|
|
51
|
+
actual_weight=18, weight_unit="kg",
|
|
52
|
+
mode="air",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Option 2: import the weight module
|
|
56
|
+
from logisticspy.weight import chargeable_weight
|
|
57
|
+
|
|
58
|
+
result = chargeable_weight.calculate(
|
|
59
|
+
length=60, width=40, height=40, unit="cm",
|
|
60
|
+
actual_weight=18, weight_unit="kg",
|
|
61
|
+
mode="air",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
print(result.volumetric_weight_kg) # 19.2
|
|
65
|
+
print(result.chargeable_weight_kg) # 19.2
|
|
66
|
+
print(result.basis) # "volumetric"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## The `weight` module: chargeable weight
|
|
70
|
+
|
|
71
|
+
Carriers bill shipments based on whichever is greater: the **actual
|
|
72
|
+
weight** or the **volumetric weight** (calculated from package
|
|
73
|
+
dimensions). This module implements that calculation cleanly, with
|
|
74
|
+
support for multiple units, transport modes, named divisor presets, and
|
|
75
|
+
multi-package consignments.
|
|
76
|
+
|
|
77
|
+
### Supported modes and default divisors
|
|
78
|
+
|
|
79
|
+
| Mode | Default divisor (cm³/kg) |
|
|
80
|
+
|-----------|---------------------------|
|
|
81
|
+
| `air` | 6000 |
|
|
82
|
+
| `courier` | 5000 |
|
|
83
|
+
| `road` | 3000 |
|
|
84
|
+
| `rail` | 3000 |
|
|
85
|
+
| `sea` | N/A (uses CBM × 1000, see below) |
|
|
86
|
+
|
|
87
|
+
These are sensible starting defaults based on common conventions seen
|
|
88
|
+
across the freight and parcel industry. They are **not** tied to any
|
|
89
|
+
specific carrier - always confirm the applicable divisor with your own
|
|
90
|
+
carrier or contract for billing-critical calculations.
|
|
91
|
+
|
|
92
|
+
### Divisor presets
|
|
93
|
+
|
|
94
|
+
Two divisor values - 5000 and 6000 - are both widely used across the
|
|
95
|
+
industry, often for different services, regions, or contracts (sometimes
|
|
96
|
+
even by the same carrier depending on the product). Rather than guessing
|
|
97
|
+
which one applies to your situation, you can refer to them by generic
|
|
98
|
+
preset labels and swap between them easily:
|
|
99
|
+
|
|
100
|
+
| Preset | Divisor |
|
|
101
|
+
|--------|---------|
|
|
102
|
+
| `"a"` | 5000 |
|
|
103
|
+
| `"b"` | 6000 |
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import logisticspy
|
|
107
|
+
|
|
108
|
+
# Same package, two different divisor conventions
|
|
109
|
+
pkg = dict(length=60, width=40, height=40, actual_weight=10, mode="air")
|
|
110
|
+
|
|
111
|
+
result_a = logisticspy.calculate(**pkg, divisor_preset="a") # divisor 5000
|
|
112
|
+
result_b = logisticspy.calculate(**pkg, divisor_preset="b") # divisor 6000
|
|
113
|
+
|
|
114
|
+
print(result_a.volumetric_weight_kg) # 19.2
|
|
115
|
+
print(result_b.volumetric_weight_kg) # 16.0
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This makes it easy to compare "what would this shipment cost under each
|
|
119
|
+
convention" without hardcoding either value, and to plug in your own
|
|
120
|
+
carrier's documented divisor (whether that happens to be 5000, 6000, or
|
|
121
|
+
something else entirely) via `divisor_preset` or a raw `divisor=` value.
|
|
122
|
+
|
|
123
|
+
You can also pass an explicit divisor directly, which overrides any preset:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
result = logisticspy.calculate(**pkg, divisor=4500)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Units
|
|
130
|
+
|
|
131
|
+
#### Input units
|
|
132
|
+
|
|
133
|
+
Dimensions accept `cm`, `m`, `mm`, `in`, `ft` (default `cm`).
|
|
134
|
+
Weights accept `kg`, `g`, `lb`, `oz` (default `kg`).
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
result = logisticspy.calculate(
|
|
138
|
+
length=20, width=15, height=10, unit="in",
|
|
139
|
+
actual_weight=5, weight_unit="lb",
|
|
140
|
+
mode="courier",
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### Output units (always normalized)
|
|
145
|
+
|
|
146
|
+
Regardless of the input units you choose, **all results are returned in
|
|
147
|
+
a single fixed unit system**:
|
|
148
|
+
|
|
149
|
+
| Field | Unit |
|
|
150
|
+
|-------|------|
|
|
151
|
+
| `actual_weight_kg`, `volumetric_weight_kg`, `chargeable_weight_kg` | kilograms (kg) |
|
|
152
|
+
| `volume_m3` | cubic meters (m³) |
|
|
153
|
+
|
|
154
|
+
The input units (`unit`, `weight_unit`) are only used to *interpret* the
|
|
155
|
+
numbers you pass in - they are converted to centimeters and kilograms
|
|
156
|
+
internally before any calculation happens. The output is never expressed
|
|
157
|
+
back in the input units.
|
|
158
|
+
|
|
159
|
+
If you need the result in a different unit (e.g. pounds), convert the
|
|
160
|
+
returned kg value yourself - the library does not provide unit conversion
|
|
161
|
+
on outputs.
|
|
162
|
+
|
|
163
|
+
### Sea freight (CBM)
|
|
164
|
+
|
|
165
|
+
Sea freight chargeable weight is derived from volume in cubic meters
|
|
166
|
+
(CBM), using the common 1 CBM ≈ 1000 kg convention:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
result = logisticspy.calculate(
|
|
170
|
+
length=1, width=1, height=1, unit="m",
|
|
171
|
+
actual_weight=500, mode="sea",
|
|
172
|
+
)
|
|
173
|
+
print(result.volumetric_weight_kg) # 1000.0
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Divisor presets are ignored for sea mode, since it uses a CBM-based
|
|
177
|
+
calculation rather than a divisor.
|
|
178
|
+
|
|
179
|
+
### Multi-package consignments
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
import logisticspy
|
|
183
|
+
|
|
184
|
+
packages = [
|
|
185
|
+
{"length": 50, "width": 40, "height": 40, "actual_weight": 10},
|
|
186
|
+
{"length": 60, "width": 40, "height": 40, "actual_weight": 25, "quantity": 2},
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
result = logisticspy.calculate_consignment(packages, mode="air")
|
|
190
|
+
|
|
191
|
+
print(result.total_actual_weight_kg)
|
|
192
|
+
print(result.total_volumetric_weight_kg)
|
|
193
|
+
print(result.total_chargeable_weight_kg)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
By default, totals are compared (`sum(actual)` vs `sum(volumetric)`).
|
|
197
|
+
Some couriers calculate chargeable weight per package and sum those - use
|
|
198
|
+
`per_piece=True` for that behavior:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
result = logisticspy.calculate_consignment(packages, mode="air", per_piece=True)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Roadmap
|
|
205
|
+
|
|
206
|
+
`logisticspy` is designed to grow into a broader logistics toolkit.
|
|
207
|
+
`chargeable_weight` is the first module; additional tools (e.g. volume,
|
|
208
|
+
freight, and inventory calculations) will be added over time.
|
|
209
|
+
|
|
210
|
+
## Disclaimer
|
|
211
|
+
|
|
212
|
+
This library implements widely-used industry conventions for
|
|
213
|
+
illustrative and estimation purposes. Divisors and CBM ratios vary by
|
|
214
|
+
carrier, service level, region, and contract terms. Always confirm exact
|
|
215
|
+
billing methodology with your carrier or freight forwarder for
|
|
216
|
+
invoicing-critical calculations.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
logisticspy/__init__.py,sha256=H4X2MNhhLn7vcLUYhvdMIK6_VB6_Cv587R-WOpesDZQ,993
|
|
2
|
+
logisticspy/weight/__init__.py,sha256=94IdASrAiVchplZA6i3TUQoErHgKNA7t4TPVlKX35SE,656
|
|
3
|
+
logisticspy/weight/chargeable_weight.py,sha256=tTjHgBlXAetOEqDQ5bJv8YHeRY35zvzlsUCM-YbPSss,9725
|
|
4
|
+
logisticspy/weight/divisors.py,sha256=I4E5JcZGnxZU8ysfDoudje_rtHtQRVBli0oizGOcwEI,2107
|
|
5
|
+
logisticspy-0.1.0.dist-info/licenses/LICENSE,sha256=dSwPcwW5G_idlsArqi07jZ0aEczyuJ1jpJ92cGvWuAs,1075
|
|
6
|
+
logisticspy-0.1.0.dist-info/METADATA,sha256=bSZ7A-nswv6VzE32-zx8RFyoZA-Vuhz1LqFpN7_-AOE,6784
|
|
7
|
+
logisticspy-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
logisticspy-0.1.0.dist-info/top_level.txt,sha256=pXagobTJb9Npe89H4jqfrGhY7dStvu2QKz5F5o7Y5ec,12
|
|
9
|
+
logisticspy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 krishnanz550i-cmyk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
logisticspy
|