mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-22 14:09:04 +01:00
Initial spin-off of bambu-run from my private project separation
This commit is contained in:
595
bambu_run/models.py
Normal file
595
bambu_run/models.py
Normal file
@@ -0,0 +1,595 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Printer(models.Model):
|
||||
"""Represents a Bambu Lab 3D printer device"""
|
||||
|
||||
name = models.CharField(max_length=200, help_text="Friendly device name")
|
||||
model = models.CharField(max_length=100, help_text="Device model (e.g., X1C, P1S)")
|
||||
manufacturer = models.CharField(
|
||||
max_length=100, default="Bambu Lab", help_text="e.g., Bambu Lab"
|
||||
)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
serial_number = models.CharField(max_length=100, blank=True, null=True, unique=True)
|
||||
ip_address = models.GenericIPAddressField(blank=True, null=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
location = models.CharField(
|
||||
max_length=200, blank=True, help_text="Physical location"
|
||||
)
|
||||
|
||||
first_seen = models.DateTimeField(auto_now_add=True)
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "infrastructure_device"
|
||||
verbose_name = "Printer"
|
||||
verbose_name_plural = "Printers"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.model})"
|
||||
|
||||
|
||||
class PrinterMetrics(models.Model):
|
||||
"""Time-series metrics for 3D Printer devices (Bambu Lab)"""
|
||||
|
||||
device = models.ForeignKey(
|
||||
Printer, on_delete=models.CASCADE, related_name="printer_metrics", db_index=True
|
||||
)
|
||||
timestamp = models.DateTimeField(
|
||||
default=timezone.now, db_index=True, help_text="When this reading was taken"
|
||||
)
|
||||
|
||||
# Temperature metrics
|
||||
nozzle_temp = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
nozzle_target_temp = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
bed_temp = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
bed_target_temp = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
chamber_temp = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
|
||||
# Nozzle info
|
||||
nozzle_diameter = models.DecimalField(
|
||||
max_digits=3, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
nozzle_type = models.CharField(max_length=50, null=True, blank=True)
|
||||
|
||||
# Print job status
|
||||
gcode_state = models.CharField(
|
||||
max_length=50, null=True, blank=True, help_text="FINISH, RUNNING, IDLE, etc."
|
||||
)
|
||||
print_type = models.CharField(
|
||||
max_length=50, null=True, blank=True, help_text="idle, printing, etc."
|
||||
)
|
||||
print_percent = models.IntegerField(
|
||||
null=True, blank=True, help_text="Print progress percentage"
|
||||
)
|
||||
remaining_time_min = models.IntegerField(
|
||||
null=True, blank=True, help_text="Estimated remaining time in minutes"
|
||||
)
|
||||
layer_num = models.IntegerField(
|
||||
null=True, blank=True, help_text="Current layer number"
|
||||
)
|
||||
total_layer_num = models.IntegerField(
|
||||
null=True, blank=True, help_text="Total layers in print"
|
||||
)
|
||||
print_line_number = models.IntegerField(null=True, blank=True)
|
||||
subtask_name = models.CharField(max_length=200, null=True, blank=True)
|
||||
gcode_file = models.CharField(max_length=200, null=True, blank=True)
|
||||
|
||||
# Fan speeds (0-100%)
|
||||
cooling_fan_speed = models.IntegerField(null=True, blank=True)
|
||||
heatbreak_fan_speed = models.IntegerField(null=True, blank=True)
|
||||
big_fan1_speed = models.IntegerField(
|
||||
null=True, blank=True, help_text="Auxiliary/chamber fan 1 speed"
|
||||
)
|
||||
big_fan2_speed = models.IntegerField(
|
||||
null=True, blank=True, help_text="Auxiliary/chamber fan 2 speed"
|
||||
)
|
||||
|
||||
# Speed settings
|
||||
spd_lvl = models.IntegerField(
|
||||
null=True, blank=True,
|
||||
help_text="Speed level (1=silent, 2=standard, 3=sport, 4=ludicrous)",
|
||||
)
|
||||
spd_mag = models.IntegerField(
|
||||
null=True, blank=True, help_text="Speed magnitude percentage"
|
||||
)
|
||||
|
||||
# Network & connectivity
|
||||
wifi_signal_dbm = models.IntegerField(null=True, blank=True)
|
||||
|
||||
# Error tracking
|
||||
print_error = models.IntegerField(default=0)
|
||||
has_errors = models.BooleanField(default=False)
|
||||
|
||||
# Chamber light & camera
|
||||
chamber_light = models.CharField(
|
||||
max_length=20, null=True, blank=True, help_text="on/off"
|
||||
)
|
||||
ipcam_record = models.CharField(
|
||||
max_length=20, null=True, blank=True, help_text="enable/disable"
|
||||
)
|
||||
timelapse = models.CharField(
|
||||
max_length=20, null=True, blank=True, help_text="enable/disable"
|
||||
)
|
||||
|
||||
# System info
|
||||
stg_cur = models.IntegerField(
|
||||
null=True, blank=True, help_text="Current print stage"
|
||||
)
|
||||
sdcard = models.BooleanField(
|
||||
null=True, blank=True, help_text="SD card present"
|
||||
)
|
||||
gcode_file_prepare_percent = models.CharField(
|
||||
max_length=10, null=True, blank=True, help_text="File preparation progress"
|
||||
)
|
||||
lifecycle = models.CharField(
|
||||
max_length=50, null=True, blank=True, help_text="Product lifecycle state"
|
||||
)
|
||||
|
||||
# HMS (Health Management System)
|
||||
hms = models.JSONField(
|
||||
default=list, help_text="Health management system messages (errors/warnings)"
|
||||
)
|
||||
|
||||
# AMS (Automatic Material System) status
|
||||
ams_unit_count = models.IntegerField(null=True, blank=True)
|
||||
ams_status = models.IntegerField(null=True, blank=True)
|
||||
ams_rfid_status = models.IntegerField(null=True, blank=True)
|
||||
ams_humidity = models.IntegerField(
|
||||
null=True, blank=True, help_text="AMS humidity level (processed)"
|
||||
)
|
||||
ams_humidity_raw = models.IntegerField(
|
||||
null=True, blank=True, help_text="AMS raw humidity reading"
|
||||
)
|
||||
ams_temp = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
ams_version = models.IntegerField(
|
||||
null=True, blank=True, help_text="AMS firmware version"
|
||||
)
|
||||
tray_is_bbl_bits = models.CharField(
|
||||
max_length=20, null=True, blank=True,
|
||||
help_text="Which trays have Bambu Lab (OEM) filament",
|
||||
)
|
||||
tray_read_done_bits = models.CharField(
|
||||
max_length=20, null=True, blank=True,
|
||||
help_text="RFID read completion status bits",
|
||||
)
|
||||
|
||||
# JSON fields for complex nested data
|
||||
filaments = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of filament info [{tray_id, slot, type, sub_type, color, remain_percent, k, ...}]",
|
||||
)
|
||||
ams_units = models.JSONField(
|
||||
default=list,
|
||||
help_text="AMS unit info [{unit_id, ams_id, chip_id, humidity, temp, ...}]",
|
||||
)
|
||||
external_spool = models.JSONField(
|
||||
default=dict, help_text="External spool info {type, color, remain}"
|
||||
)
|
||||
lights_report = models.JSONField(
|
||||
default=list, help_text="Light status report [{node, mode}]"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "infrastructure_printer_metrics"
|
||||
verbose_name = "Printer Metric"
|
||||
verbose_name_plural = "Printer Metrics"
|
||||
ordering = ["-timestamp"]
|
||||
indexes = [
|
||||
models.Index(fields=["device", "-timestamp"], name="printer_dev_time_idx"),
|
||||
models.Index(fields=["-timestamp"], name="printer_time_idx"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.device.name} @ {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
|
||||
class FilamentType(models.Model):
|
||||
"""Central registry of filament types (material + sub-type + brand)"""
|
||||
|
||||
type = models.CharField(max_length=50, help_text="Base material: PLA, PETG, ABS, etc.")
|
||||
sub_type = models.CharField(
|
||||
max_length=100, null=True, blank=True,
|
||||
help_text="Sub-type: PLA Basic, PLA Matte, etc."
|
||||
)
|
||||
brand = models.CharField(
|
||||
max_length=100, default='Bambu Lab',
|
||||
help_text="Manufacturer name"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "infrastructure_filament_type"
|
||||
verbose_name = "Filament Type"
|
||||
verbose_name_plural = "Filament Types"
|
||||
ordering = ['type', 'sub_type', 'brand']
|
||||
unique_together = [['type', 'sub_type', 'brand']]
|
||||
|
||||
def __str__(self):
|
||||
sub = f" {self.sub_type}" if self.sub_type else ""
|
||||
return f"{self.type}{sub} ({self.brand})"
|
||||
|
||||
|
||||
class FilamentColor(models.Model):
|
||||
"""Master database of Bambu Lab filament colors for auto-matching"""
|
||||
|
||||
color_code = models.CharField(
|
||||
max_length=6,
|
||||
help_text="Hex color code without padding (e.g., '000000' not '000000FF')"
|
||||
)
|
||||
color_name = models.CharField(
|
||||
max_length=100,
|
||||
help_text="Human-readable color name (e.g., 'Black', 'Orange')"
|
||||
)
|
||||
|
||||
filament_type_fk = models.ForeignKey(
|
||||
'FilamentType', null=True, blank=True, on_delete=models.SET_NULL,
|
||||
related_name='colors',
|
||||
help_text="Link to FilamentType registry"
|
||||
)
|
||||
|
||||
filament_type = models.CharField(
|
||||
max_length=50,
|
||||
help_text="Base material type: PLA, PETG, ABS, TPU, etc."
|
||||
)
|
||||
filament_sub_type = models.CharField(
|
||||
max_length=100,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Material sub-type: 'PLA Basic', 'PLA Matte', 'ABS GF', etc."
|
||||
)
|
||||
brand = models.CharField(
|
||||
max_length=100,
|
||||
default='Bambu Lab',
|
||||
help_text="Manufacturer name"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "infrastructure_filament_color"
|
||||
verbose_name = "Filament Color"
|
||||
verbose_name_plural = "Filament Colors"
|
||||
ordering = ['filament_type', 'filament_sub_type', 'color_name']
|
||||
indexes = [
|
||||
models.Index(fields=['color_code', 'filament_type', 'filament_sub_type', 'brand']),
|
||||
models.Index(fields=['filament_type']),
|
||||
]
|
||||
unique_together = [['color_code', 'filament_type', 'filament_sub_type', 'brand']]
|
||||
|
||||
def __str__(self):
|
||||
sub_type_info = f" {self.filament_sub_type}" if self.filament_sub_type else ""
|
||||
return f"{self.filament_type}{sub_type_info}: {self.color_name} (#{self.color_code})"
|
||||
|
||||
def get_hex_color(self):
|
||||
"""Return color code with # prefix for display"""
|
||||
return f"#{self.color_code}"
|
||||
|
||||
|
||||
class Filament(models.Model):
|
||||
"""Master inventory of filament spools owned by user"""
|
||||
|
||||
# Unique identification
|
||||
tray_uuid = models.CharField(
|
||||
max_length=100, unique=True, null=True, blank=True, db_index=True,
|
||||
help_text="Spool serial number from MQTT"
|
||||
)
|
||||
tag_uid = models.CharField(
|
||||
max_length=100, null=True, blank=True, db_index=True,
|
||||
help_text="RFID chip unique identifier"
|
||||
)
|
||||
tag_id = models.CharField(
|
||||
max_length=100, null=True, blank=True,
|
||||
help_text="User-defined unique identifier (barcode, label, etc.)"
|
||||
)
|
||||
|
||||
# Creation tracking
|
||||
created_by = models.CharField(
|
||||
max_length=20, default='Manual',
|
||||
choices=[
|
||||
('Auto Detection', 'Auto Detection'),
|
||||
('Manual', 'Manual'),
|
||||
],
|
||||
help_text="How this filament was added to inventory"
|
||||
)
|
||||
|
||||
# FK to FilamentType registry
|
||||
filament_type = models.ForeignKey(
|
||||
'FilamentType', null=True, blank=True, on_delete=models.SET_NULL,
|
||||
related_name='filaments',
|
||||
help_text="Link to FilamentType registry"
|
||||
)
|
||||
|
||||
# Filament specifications
|
||||
type = models.CharField(max_length=50, help_text="PLA, PETG, ABS, TPU, etc.")
|
||||
sub_type = models.CharField(
|
||||
max_length=100, null=True, blank=True,
|
||||
help_text="Material sub-type from MQTT: 'PLA Matte', 'PLA Basic', etc."
|
||||
)
|
||||
brand = models.CharField(max_length=100, help_text="Manufacturer name")
|
||||
color = models.CharField(max_length=50, help_text="Color name")
|
||||
color_hex = models.CharField(
|
||||
max_length=7, null=True, blank=True,
|
||||
help_text="Color hex code for display (#RRGGBB)"
|
||||
)
|
||||
|
||||
# Physical properties
|
||||
diameter = models.DecimalField(
|
||||
max_digits=4, decimal_places=2, default=1.75,
|
||||
help_text="Filament diameter in mm (1.75 or 2.85)"
|
||||
)
|
||||
initial_weight_grams = models.IntegerField(
|
||||
null=True, blank=True,
|
||||
help_text="Spool weight when new (typically 1000g)"
|
||||
)
|
||||
|
||||
# Current status
|
||||
remaining_percent = models.IntegerField(
|
||||
default=100,
|
||||
help_text="Estimated remaining filament (0-100%)"
|
||||
)
|
||||
remaining_weight_grams = models.IntegerField(
|
||||
null=True, blank=True,
|
||||
help_text="Calculated remaining weight"
|
||||
)
|
||||
|
||||
# Current location in AMS
|
||||
is_loaded_in_ams = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Is this spool currently loaded in AMS?"
|
||||
)
|
||||
current_tray_id = models.IntegerField(
|
||||
null=True, blank=True,
|
||||
help_text="Which AMS slot (0-3) if loaded"
|
||||
)
|
||||
last_loaded_date = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
help_text="When was this spool loaded into AMS"
|
||||
)
|
||||
|
||||
# Purchase/inventory tracking
|
||||
purchase_date = models.DateField(null=True, blank=True)
|
||||
purchase_price = models.DecimalField(
|
||||
max_digits=8, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
supplier = models.CharField(max_length=100, null=True, blank=True)
|
||||
notes = models.TextField(blank=True, help_text="Custom notes about this spool")
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
last_used = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
help_text="Last time this spool was used in a print"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "infrastructure_filament"
|
||||
verbose_name = "Filament Spool"
|
||||
verbose_name_plural = "Filament Spools"
|
||||
ordering = ['type', 'brand', 'color', '-remaining_percent']
|
||||
indexes = [
|
||||
models.Index(fields=['type', 'brand', 'color']),
|
||||
models.Index(fields=['tray_uuid']),
|
||||
models.Index(fields=['tag_uid']),
|
||||
models.Index(fields=['tag_id']),
|
||||
models.Index(fields=['is_loaded_in_ams', 'current_tray_id']),
|
||||
models.Index(fields=['remaining_percent']),
|
||||
models.Index(fields=['created_by']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
sn_info = f"[SN:{self.tray_uuid[:8]}...] " if self.tray_uuid else ""
|
||||
return f"{sn_info}{self.brand} {self.type} - {self.color} ({self.remaining_percent}%)"
|
||||
|
||||
def update_remaining_weight(self):
|
||||
"""Calculate remaining weight based on percentage"""
|
||||
if self.initial_weight_grams:
|
||||
self.remaining_weight_grams = int(
|
||||
self.initial_weight_grams * (self.remaining_percent / 100.0)
|
||||
)
|
||||
|
||||
|
||||
class FilamentSnapshot(models.Model):
|
||||
"""Links PrinterMetrics to Filament inventory with point-in-time AMS data"""
|
||||
|
||||
printer_metric = models.ForeignKey(
|
||||
'PrinterMetrics', on_delete=models.CASCADE,
|
||||
related_name='filament_snapshots'
|
||||
)
|
||||
filament = models.ForeignKey(
|
||||
'Filament', on_delete=models.SET_NULL,
|
||||
null=True, blank=True,
|
||||
related_name='usage_snapshots',
|
||||
help_text="Matched filament from inventory (null if no match)"
|
||||
)
|
||||
|
||||
tray_id = models.IntegerField(help_text="AMS slot number (0-3)")
|
||||
slot_name = models.CharField(
|
||||
max_length=20, null=True, blank=True,
|
||||
help_text="Slot identifier like A00-W1"
|
||||
)
|
||||
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
sub_type = models.CharField(
|
||||
max_length=100, null=True, blank=True,
|
||||
help_text="Material sub-type from MQTT (PLA Basic, PLA Matte, etc.)"
|
||||
)
|
||||
brand = models.CharField(
|
||||
max_length=100, null=True, blank=True,
|
||||
help_text="Deprecated: MQTT doesn't provide brand. Use Filament.brand instead."
|
||||
)
|
||||
color = models.CharField(max_length=50, null=True, blank=True)
|
||||
remain_percent = models.IntegerField(null=True, blank=True)
|
||||
k_value = models.DecimalField(
|
||||
max_digits=6, decimal_places=4, null=True, blank=True
|
||||
)
|
||||
|
||||
tag_uid = models.CharField(
|
||||
max_length=100, null=True, blank=True, db_index=True,
|
||||
help_text="RFID chip unique identifier"
|
||||
)
|
||||
tray_uuid = models.CharField(
|
||||
max_length=100, null=True, blank=True,
|
||||
help_text="Tray UUID from MQTT"
|
||||
)
|
||||
state = models.IntegerField(
|
||||
null=True, blank=True,
|
||||
help_text="Tray state from MQTT"
|
||||
)
|
||||
|
||||
temp = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
humidity = models.IntegerField(null=True, blank=True)
|
||||
|
||||
auto_matched = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Was this auto-matched to inventory or manually set?"
|
||||
)
|
||||
match_method = models.CharField(
|
||||
max_length=20, default='none',
|
||||
help_text="tag_id, lowest_remaining, manual, or none"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "infrastructure_filament_snapshot"
|
||||
verbose_name = "Filament Snapshot"
|
||||
verbose_name_plural = "Filament Snapshots"
|
||||
ordering = ['printer_metric', 'tray_id']
|
||||
indexes = [
|
||||
models.Index(fields=['printer_metric', 'tray_id']),
|
||||
models.Index(fields=['filament']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
filament_info = str(self.filament) if self.filament else f"{self.brand} {self.type}"
|
||||
return f"Tray {self.tray_id}: {filament_info}"
|
||||
|
||||
|
||||
class PrintJob(models.Model):
|
||||
"""Represents a single print job from start to finish"""
|
||||
|
||||
device = models.ForeignKey(
|
||||
'Printer', on_delete=models.CASCADE,
|
||||
related_name='print_jobs'
|
||||
)
|
||||
|
||||
project_name = models.CharField(
|
||||
max_length=200, help_text="From subtask_name field"
|
||||
)
|
||||
gcode_file = models.CharField(max_length=200, null=True, blank=True)
|
||||
|
||||
start_time = models.DateTimeField(help_text="When print started")
|
||||
end_time = models.DateTimeField(null=True, blank=True, help_text="When print finished/failed")
|
||||
duration_minutes = models.IntegerField(null=True, blank=True, help_text="Total print duration")
|
||||
|
||||
total_layers = models.IntegerField(null=True, blank=True)
|
||||
final_status = models.CharField(
|
||||
max_length=50, null=True, blank=True, help_text="FINISH, FAILED, CANCELLED"
|
||||
)
|
||||
completion_percent = models.IntegerField(
|
||||
default=0, help_text="Final completion percentage"
|
||||
)
|
||||
|
||||
start_metric = models.ForeignKey(
|
||||
'PrinterMetrics', on_delete=models.SET_NULL,
|
||||
null=True, related_name='started_jobs'
|
||||
)
|
||||
end_metric = models.ForeignKey(
|
||||
'PrinterMetrics', on_delete=models.SET_NULL,
|
||||
null=True, related_name='ended_jobs'
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "infrastructure_print_job"
|
||||
verbose_name = "Print Job"
|
||||
verbose_name_plural = "Print Jobs"
|
||||
ordering = ['-start_time']
|
||||
indexes = [
|
||||
models.Index(fields=['device', '-start_time']),
|
||||
models.Index(fields=['project_name']),
|
||||
models.Index(fields=['-start_time']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
status = self.final_status or 'In Progress'
|
||||
return f"{self.project_name} ({status}) - {self.start_time.strftime('%Y-%m-%d %H:%M')}"
|
||||
|
||||
def calculate_duration(self):
|
||||
"""Calculate print duration if end_time is set"""
|
||||
if self.end_time and self.start_time:
|
||||
delta = self.end_time - self.start_time
|
||||
self.duration_minutes = int(delta.total_seconds() / 60)
|
||||
|
||||
|
||||
class FilamentUsage(models.Model):
|
||||
"""Tracks filament consumption during print jobs"""
|
||||
|
||||
print_job = models.ForeignKey(
|
||||
'PrintJob', on_delete=models.CASCADE,
|
||||
related_name='filament_usages'
|
||||
)
|
||||
filament = models.ForeignKey(
|
||||
'Filament', on_delete=models.CASCADE,
|
||||
related_name='print_usages'
|
||||
)
|
||||
|
||||
tray_id = models.IntegerField(help_text="Which AMS slot was used")
|
||||
|
||||
starting_percent = models.IntegerField(help_text="Filament remaining % at job start")
|
||||
ending_percent = models.IntegerField(
|
||||
null=True, blank=True, help_text="Filament remaining % at job end"
|
||||
)
|
||||
consumed_percent = models.IntegerField(
|
||||
null=True, blank=True, help_text="Amount consumed during print"
|
||||
)
|
||||
consumed_grams = models.IntegerField(
|
||||
null=True, blank=True, help_text="Estimated grams consumed"
|
||||
)
|
||||
|
||||
is_primary = models.BooleanField(
|
||||
default=True, help_text="Primary filament vs multi-color"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "infrastructure_filament_usage"
|
||||
verbose_name = "Filament Usage"
|
||||
verbose_name_plural = "Filament Usages"
|
||||
ordering = ['print_job', 'tray_id']
|
||||
indexes = [
|
||||
models.Index(fields=['print_job']),
|
||||
models.Index(fields=['filament']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.filament} - {self.print_job.project_name} ({self.consumed_percent}%)"
|
||||
|
||||
def calculate_consumed(self):
|
||||
"""Calculate consumed amount"""
|
||||
if self.ending_percent is not None:
|
||||
self.consumed_percent = self.starting_percent - self.ending_percent
|
||||
if self.filament.initial_weight_grams:
|
||||
self.consumed_grams = int(
|
||||
self.filament.initial_weight_grams * (self.consumed_percent / 100.0)
|
||||
)
|
||||
Reference in New Issue
Block a user