- Input: CSV files for stock inventory and demand list
- Output: Excel (.xlsx) with 3 sheets, print-ready, timestamped filename
- UI: Simple GUI (tkinter) with file selectors and parameter fields
- Algorithm: Best Fit Decreasing (BFD) with local swap optimization
- Dependencies:
openpyxlonly
c:\Projects\ProfileCutOptim\
├── src/
│ ├── __init__.py
│ ├── main.py # GUI entry point
│ ├── models/
│ │ ├── __init__.py
│ │ ├── stock.py # StockPiece, StockInventory
│ │ └── demand.py # DemandItem, DemandList
│ ├── parsers/
│ │ ├── __init__.py
│ │ ├── stock_parser.py # CSV parser for stock
│ │ └── demand_parser.py # CSV parser for demands
│ ├── optimizer/
│ │ ├── __init__.py
│ │ ├── cutting_optimizer.py # Main orchestration
│ │ ├── bfd_algorithm.py # Best Fit Decreasing
│ │ └── local_optimizer.py # Swap optimization
│ ├── output/
│ │ ├── __init__.py
│ │ └── excel_writer.py # Excel generation (3 sheets)
│ └── gui/
│ ├── __init__.py
│ └── app.py # Tkinter GUI
├── tests/
│ ├── __init__.py
│ ├── fixtures/ # Synthetic test data
│ │ ├── stock_basic.csv
│ │ ├── stock_splitting.csv
│ │ ├── demands_basic.csv
│ │ ├── demands_edge_cases.csv
│ │ └── ...
│ ├── test_models.py
│ ├── test_parsers.py
│ ├── test_bfd_algorithm.py
│ ├── test_local_optimizer.py
│ ├── test_cutting_optimizer.py
│ ├── test_excel_writer.py
│ └── test_integration.py
├── requirements.txt
└── README.md
@dataclass
class StockPiece:
part_number: str
lot_batch: str
length: float
length_limit: float | None
storage_location: str
uom: strsplit_if_oversized(): Splits into pieces oflength_limit+ remaindercreate_remnant(remaining_length): Creates new piece with "-1" suffix
@dataclass
class DemandItem:
order_no: str
line_no: str
component_part_number: str
part_description: str
lot_size: int
required_qty: int
length: float
demand_date: date
cut_type: str # "straight" or "miter"
angle_left: float
angle_right: float
cutting_code: str
uom: strtotal_pieces: property returninglot_size × required_qtyis_valid: checks length > 0 and required_qty > 0
- Parse CSV with columns:
part_number,lot_batch,length,length_limit,storage_location,uom - Handle missing
length_limit(default: None = no limit) - Return list of
StockPieceobjects
- Parse CSV with all demand columns including
part_description,uom - Filter by
demand_date ≤ today + timespan_days - Sort by
demand_dateascending (oldest first) - Expand to individual pieces (lot_size × required_qty)
Best Fit Decreasing algorithm:
- Sort demand pieces by length (descending)
- For each piece, find stock with least remaining space that still fits
- Account for
cut_thicknessper cut - Track assignments:
{stock_piece: [list of cuts]}
Swap optimization:
- Iterate through all stock pieces with cuts
- Try swapping pieces between stock bars
- Accept swap if total waste decreases
- Stop when waste <
good_enoughthreshold or no improvement found
Main orchestration:
- Group demands by
part_number - For each part number:
- Run BFD to assign pieces to stock
- Run local optimizer to reduce waste
- Group cuts by angle within each stock piece
- Track unfulfilled demands with reasons
- Generate remnants for partially used stock
Creates Excel with 3 sheets:
- Sheet 1: Cutting Plan - Hierarchical format with stock headers, cuts (including Demand Date), remnant/waste
- Sheet 2: Unfulfilled Demands - Table with order, part, qty, reason
- Sheet 3: Summary - Waste stats per part number
Cutting Plan Columns: Cut #, Order, Demand Date, Line, Length, Cut Type, L/R Angle, Code, Remaining
Sorting: Plans are sorted based on user-selected sort order (Date or Part Number)
Print formatting:
- Page orientation: landscape
- Fit to page width
- Headers repeat on each page
- Borders and alternating row colors
Tkinter interface:
- File browser for Stock Inventory CSV
- File browser for Demand List CSV
- Output directory selector
- Parameter inputs with defaults:
- Cut Thickness: 3.0
- Timespan (days): 30
- Min Remnant Length: 100
- Good Enough (%): 5
- Sort By: Dropdown ("Date (Oldest First)" / "Part Number")
- "Run Optimization" button
- Progress bar and status label
- "Open Output Folder" button (enabled after completion)
Sorting Feature:
- Sort By selection determines order of cutting plans in:
- Pattern visualization canvas
- Excel output Sheet 1
- "Date (Oldest First)": Sorts by earliest demand date per stock piece
- "Part Number": Sorts alphabetically by stock part number
A canvas-based cutting pattern diagram displayed after optimization:
Layout Structure:
┌─────────────────────────────────────────────────────────────────┐
│ PATTERN │
├─────────────────┬───────────────────────────────────────────────┤
│ PN-001 │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░ │
│ Steel Profile A │ [ORD-101 ] [ORD-102] [ ] ░░░░░░░ │
├─────────────────┼───────────────────────────────────────────────┤
│ PN-001 │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░ │
│ Steel Profile A │ [ORD-103 ] [ORD-104 ] ░░░ │
└─────────────────┴───────────────────────────────────────────────┘
Implementation:
- Use
tkinter.Canvaswidget for drawing - Wrap in
tkinter.Framewith vertical scrollbar
Color Palette:
COLORS = [
'#FFFF00', # Yellow
'#FF69B4', # Pink
'#90EE90', # Light Green
'#87CEEB', # Sky Blue
'#DDA0DD', # Plum
'#FFB347', # Orange
'#98D8C8', # Teal
'#F0E68C', # Khaki
]
WASTE_COLOR = '#C0C0C0' # GrayKey Functions:
def draw_pattern(canvas, optimization_result):
"""Draw all stock pieces with their cuts."""
def draw_stock_row(canvas, y, stock_plan, color_map):
"""Draw a single stock piece row."""
def get_segment_color(part_number, color_map):
"""Get consistent color for a part number."""
def fits_text(segment_width, text, font):
"""Check if text fits within segment width."""Smart Text Visibility:
- Measure text width using
font.measure(text) - If
segment_width > text_width + padding: draw centered text - If too narrow: skip text, rely on tooltip
Hover Tooltips:
- Bind
<Enter>and<Leave>events to segments - Show
Toplevelwindow with: Order number, Length, Cut type/angles
All tests use pytest with synthetic CSV fixtures.
cd c:\Projects\ProfileCutOptim
python -m pytest tests/ -v --tb=short| Test ID | Feature | Input | Expected Output |
|---|---|---|---|
| M1 | StockPiece creation | Valid data | Object created |
| M2 | Stock splitting | length=15000, limit=6000 | 2×6000 + 1×3000 |
| M3 | Stock no split needed | length=5000, limit=6000 | 1×5000 |
| M4 | Remnant creation | lot_batch="LOT-001", remaining=500 | lot_batch="LOT-001-1" |
| M5 | DemandItem total_pieces | lot_size=5, required_qty=3 | 15 |
| M6 | DemandItem is_valid (valid) | length=1000, qty=2 | True |
| M7 | DemandItem is_valid (zero length) | length=0, qty=2 | False |
| M8 | DemandItem is_valid (zero qty) | length=1000, qty=0 | False |
| Test ID | Feature | Fixture | Expected Output |
|---|---|---|---|
| P1 | Basic stock parsing | stock_basic.csv |
3 StockPiece objects |
| P2 | Stock with missing length_limit | stock_no_limit.csv |
length_limit=None |
| P3 | Stock with splitting | stock_splitting.csv |
Correct split pieces |
| P4 | Basic demand parsing | demands_basic.csv |
5 DemandItem objects |
| P5 | Demand date filtering | demands_dated.csv, timespan=7 |
Only demands within range |
| P6 | Demand sorting | demands_unsorted.csv |
Sorted by demand_date asc |
| P7 | Demand expansion | lot_size=2, qty=3 | 6 individual pieces |
| P8 | Malformed demand (zero length) | demands_malformed.csv |
Flagged as invalid |
Fixture: stock_basic.csv
part_number,lot_batch,length,length_limit,storage_location,uom
PN-001,LOT-A001,6000,,Aisle-1-Bin-5,mm
PN-001,LOT-A002,6000,,Aisle-1-Bin-5,mm
PN-002,LOT-B001,4000,4000,Aisle-2-Bin-3,mmFixture: demands_basic.csv
order_no,line_no,component_part_number,part_description,lot_size,required_qty,length,demand_date,cut_type,angle_left,angle_right,cutting_code,uom
ORD-101,1,PN-001,Steel Profile A,1,2,1500,2026-01-20,straight,0,0,,mm
ORD-102,1,PN-001,Steel Profile A,1,1,2000,2026-01-22,miter,45,0,MC-001,mm
ORD-103,1,PN-002,Steel Profile B,2,2,800,2026-01-25,straight,0,0,,mm| Test ID | Feature | Input | Expected Output |
|---|---|---|---|
| B1 | Single piece fits | Stock: 6000mm, Demand: 1500mm, kerf=3 | Assigned, remaining=4497 |
| B2 | Multiple pieces one stock | Stock: 6000mm, Demands: 1500+2000+1200 | All fit, waste calculated |
| B3 | Piece doesn't fit | Stock: 1000mm, Demand: 1500mm | Unfulfilled |
| B4 | Best fit selection | Stocks: 6000, 3000. Demand: 2500 | Assigned to 3000 (least waste) |
| B5 | Kerf accumulation | Stock: 6000, 3×2000mm pieces | 6000 - (3×2000 + 3×3) = -9 → only 2 fit |
| B6 | Empty demands | Stock: 6000, Demands: [] | No assignments |
| B7 | Empty stock | Stock: [], Demands: [1500] | All unfulfilled |
| Test ID | Feature | Input | Expected Output |
|---|---|---|---|
| L1 | No improvement possible | Optimal assignment | Same assignment |
| L2 | Swap reduces waste | Suboptimal assignment | Improved assignment |
| L3 | Good enough threshold | Waste=4%, threshold=5% | Stops immediately |
| L4 | Multiple swap iterations | Complex assignment | Converges to better solution |
| Test ID | Feature | Input | Expected Output |
|---|---|---|---|
| C1 | Full workflow | Basic stock + demands | Cutting plan + summary |
| C2 | Part number grouping | 2 part numbers | Separate optimization per PN |
| C3 | Angle grouping | Miter + straight cuts | Grouped by angle in output |
| C4 | Partial fulfillment | Demand=10, Stock fits 8 | 8 fulfilled, 2 unfulfilled |
| C5 | Remnant generation | Stock partially used | Remnant with -1 suffix |
| C6 | Min remnant threshold | Remaining=50, threshold=100 | Marked as waste, not remnant |
| C7 | No stock for part number | Demand for PN-999 | Unfulfilled "No stock exists" |
| C8 | Demand too long | Demand=8000, all stock≤6000 | Unfulfilled "no stock long enough" |
| Test ID | Feature | Input | Expected Output |
|---|---|---|---|
| E1 | File creation | Valid cutting plan | .xlsx file created |
| E2 | Sheet 1 structure | Cutting plan data | Correct hierarchical format |
| E3 | Sheet 2 structure | Unfulfilled list | Correct columns |
| E4 | Sheet 3 structure | Summary data | Waste % calculated correctly |
| E5 | Timestamped filename | - | Format: cutting_plan_YYYY-MM-DD_HHMMSS.xlsx |
| E6 | Print formatting | - | Landscape, fit to width |
| Test ID | Scenario | Fixtures | Validation |
|---|---|---|---|
| I1 | Happy path | Basic stock + demands | Complete Excel output verified |
| I2 | All edge cases | Edge case fixtures | All 15 business rules validated |
| I3 | Large dataset | 100 stock, 500 demands | Completes in <5 seconds |
| I4 | Empty inputs | Empty CSVs | Graceful handling, empty output |
Note
These require visual inspection of Excel output and GUI behavior.
Steps:
- Launch application
- Click "Browse" for Stock Inventory
- Select a CSV file
- Click "Browse" for Demand List
- Select a CSV file
Expected: File paths displayed in text fields
Steps:
- Enter negative value for Cut Thickness
- Enter non-numeric value for Timespan
- Click "Run Optimization"
Expected: Error message, optimization does not run
Steps:
- Run optimization with sample data
- Open generated Excel file
- Go to Print Preview (Ctrl+P)
Expected:
- Landscape orientation
- Fits on page width
- Headers visible on each page
- No manual adjustment needed
Steps:
- Run optimization with large dataset (500+ demands)
Expected:
- Progress bar updates
- Status shows current step
- UI remains responsive
Steps:
- Complete optimization
- Click "Open Output Folder"
Expected: File explorer opens to output directory with generated file visible
Steps:
- Run optimization with sample data
- Observe the pattern diagram in the GUI
Expected:
- "PATTERN" header visible above diagram
- Part number and description shown on left of each row
- Colored segments represent cuts (proportional width)
- Gray segments at end represent waste/remnant
- Order numbers visible in wider segments
- Narrow segments have no text
Steps:
- Run optimization with sample data
- Hover mouse over a narrow segment in the pattern
- Hover over a wide segment
Expected:
- Tooltip appears showing: Order number, Length, Cut type/angles
- Tooltip follows cursor or appears near segment
- Tooltip disappears when mouse leaves segment
Create tests/generate_fixtures.py to generate test CSVs programmatically:
def generate_stock_csv(filename, num_pieces, part_numbers, length_range):
"""Generate synthetic stock inventory."""
def generate_demands_csv(filename, num_demands, part_numbers, date_range):
"""Generate synthetic demands with various cut types and angles."""This ensures repeatable, version-controlled test data.
openpyxl>=3.1.0
pytest>=7.0.0
All requirements documented with comprehensive test coverage. Ready to proceed.