Metadata-Version: 2.4
Name: dimcalc
Version: 1.0.0
Summary: Dimensional weight calculator and box selector for shipping
Author-email: Mohammad Amin Khara <kharama8798@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: box-selection,dimensional-weight,logistics,shipping
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Description-Content-Type: text/markdown

# dimcalc

A Python library for calculating dimensional (volumetric) weight and selecting optimal shipping boxes.

## Features

- Calculate dimensional weight using configurable DIM factors
- Select optimal shipping boxes from a list of available sizes
- Distribute chargeable weight proportionally across products
- Built with Pydantic for data validation
- Type hints throughout for IDE support

## Installation

```bash
pip install dimcalc
```

## Quick Start

### Basic Dimensional Weight Calculation

```python
from dimcalc import DimCalc, Dimensions

# Create a calculator with your carrier's DIM factor
calc = DimCalc(dimensional_factor=5.0)  # 5.0 for cm³ to grams

# Define product dimensions (in centimeters)
dims = Dimensions(length=30, width=20, height=10)

# Calculate chargeable weight
result = calc.calculate_chargeable_weight(
    actual_weight=500,  # grams
    dimensions=dims,
    quantity=2
)

print(f"Actual weight: {result.actual_weight}g")
print(f"Dimensional weight: {result.dimensional_weight}g")
print(f"Chargeable weight: {result.chargeable_weight}g")
print(f"Using dimensional: {result.is_dimensional_used}")
```

### Box Selection

```python
from dimcalc import BoxSelector, Box, Product, Dimensions

# Define your available box sizes
boxes = [
    Box(id="small", length=20, width=15, height=10, name="Small"),
    Box(id="medium", length=30, width=25, height=15, name="Medium"),
    Box(id="large", length=40, width=30, height=20, name="Large"),
]

# Create a selector with 20% volume buffer for packing inefficiency
selector = BoxSelector(boxes=boxes, volume_buffer_percentage=20.0)

# Define products to ship
products = [
    Product(
        weight=500,  # grams per unit
        quantity=2,
        dimensions=Dimensions(length=15, width=10, height=8)
    ),
    Product(
        weight=300,
        quantity=1,
        dimensions=Dimensions(length=10, width=10, height=5)
    ),
]

# Select optimal box
result = selector.select_box(products)

print(f"Selected box: {result.box.name}")
print(f"Chargeable weight: {result.weight.chargeable_weight}g")
print(f"Box utilization: {result.utilization_percentage:.1f}%")
```

### Weight Distribution

When using box-based calculation, distribute the chargeable weight back to individual products:

```python
from dimcalc import WeightDistributor

# After box selection...
distributed = WeightDistributor.distribute(products, result)

for i, dist in enumerate(distributed):
    print(f"Product {i+1}: {dist.distributed_weight}g per unit")
```

## API Reference

### Models

#### `Dimensions`
Represents physical dimensions in centimeters.

```python
dims = Dimensions(length=30, width=20, height=10)
dims.volume        # 6000.0 (cm³)
dims.max_dimension # 30.0
```

#### `Box`
Represents a shipping box.

```python
box = Box(
    id="box_1",           # Unique identifier
    length=30,            # cm
    width=20,             # cm
    height=10,            # cm
    name="Medium Box"     # Optional display name
)
box.volume         # 6000.0 (cm³)
box.max_dimension  # 30.0
box.dimensions     # Returns Dimensions object
```

#### `Product`
Represents a product for weight/box calculation.

```python
product = Product(
    weight=500,                    # grams per unit
    quantity=2,                    # number of units (default: 1)
    dimensions=Dimensions(...)     # optional dimensions
)
product.total_weight  # 1000 (weight × quantity)
product.total_volume  # volume × quantity (or None if no dimensions)
```

#### `WeightResult`
Result of a dimensional weight calculation.

```python
result.actual_weight       # Total actual weight in grams
result.dimensional_weight  # Calculated dimensional weight
result.chargeable_weight   # max(actual, dimensional)
result.is_dimensional_used # True if dimensional > actual
result.weight_difference   # chargeable - actual
```

#### `BoxSelectionResult`
Result of box selection with weight calculation.

```python
result.box                    # Selected Box
result.weight                 # WeightResult
result.total_products_volume  # Sum of product volumes
result.utilization_percentage # (products_volume / box_volume) × 100
result.to_dict()              # Convert to dictionary for JSON
```

### Classes

#### `DimCalc`
Core dimensional weight calculator.

```python
calc = DimCalc(dimensional_factor=5.0)

# Calculate dimensional weight only
dim_weight = calc.calculate_dimensional_weight(dimensions, quantity=1)

# Calculate chargeable weight (max of actual and dimensional)
result = calc.calculate_chargeable_weight(
    actual_weight=500,
    dimensions=dims,
    quantity=1
)

# Calculate for multiple products
result = calc.calculate_for_products(products)
```

#### `BoxSelector`
Selects optimal shipping box for products.

```python
selector = BoxSelector(
    boxes=boxes,                      # List of available Box objects
    volume_buffer_percentage=20.0,    # Extra space for packing (default: 20%)
    calculator=DimCalc()              # Optional custom calculator
)

result = selector.select_box(products)  # Returns BoxSelectionResult or None
can_fit = selector.can_fit(products)    # Returns bool
```

#### `WeightDistributor`
Distributes chargeable weight across products.

```python
# Proportional distribution (based on actual weight)
distributed = WeightDistributor.distribute(products, box_result)

# Even distribution (equal per unit)
distributed = WeightDistributor.distribute_evenly(products, total_weight)
```

## Dimensional Factors

The dimensional factor (DIM factor) converts volume to weight. Common values:

| Carrier | Factor | Unit Conversion |
|---------|--------|-----------------|
| Iran Post | 5.0 | cm³ → grams |
| DHL | 5000.0 | cm³ → kg |

Formula: `dimensional_weight = volume / dimensional_factor`

## Presets

The library includes example presets you can use as reference:

```python
from dimcalc.presets import IRAN_POST_BOXES, DIMENSIONAL_FACTORS, get_iran_post_selector

# Example box configurations
print(IRAN_POST_BOXES)  # List of 8 standard Iran Post boxes

# Common DIM factors
print(DIMENSIONAL_FACTORS)  # {"iran_post": 5.0, "dhl": 5000.0, ...}

# Pre-configured selector for Iran Post
selector = get_iran_post_selector()
```

> **Note:** Box sizes vary by region and carrier. Create your own box list based on your specific requirements:

```python
# Define your own boxes
MY_BOXES = [
    Box(id="xs", length=15, width=10, height=10, name="Extra Small"),
    Box(id="s", length=20, width=15, height=10, name="Small"),
    Box(id="m", length=30, width=20, height=15, name="Medium"),
    Box(id="l", length=40, width=30, height=20, name="Large"),
]

selector = BoxSelector(boxes=MY_BOXES)
```

## JSON Serialization

Results can be easily serialized for API responses:

```python
result = selector.select_box(products)
data = result.to_dict()

# Returns:
{
    "box_id": "medium",
    "box_name": "Medium Box",
    "box_dimensions": {
        "length_cm": 30,
        "width_cm": 20,
        "height_cm": 15,
        "volume_cm3": 9000
    },
    "weight": {
        "actual_weight_g": 1300,
        "dimensional_weight_g": 1800,
        "chargeable_weight_g": 1800,
        "is_dimensional_used": True
    },
    "packing_info": {
        "total_products_volume_cm3": 2900,
        "utilization_percentage": 32.22
    }
}
```

Pydantic models also support `.model_dump()` for serialization:

```python
dims = Dimensions(length=30, width=20, height=10)
dims.model_dump()  # {"length": 30, "width": 20, "height": 10, "volume": 6000, "max_dimension": 30}
```

## License

MIT
