Add migrations and final model changes
This commit is contained in:
parent
5cee0194f5
commit
2bbd643cf9
4 changed files with 411 additions and 40 deletions
309
src/servala/core/migrations/0016_computeplan_and_more.py
Normal file
309
src/servala/core/migrations/0016_computeplan_and_more.py
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
# Generated by Django 5.2.8 on 2025-12-02 09:51
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import rules.contrib.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0015_add_hide_expert_mode_to_service_definition"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ComputePlan",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="Created"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(auto_now=True, verbose_name="Last updated"),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100, verbose_name="Name")),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.TextField(blank=True, verbose_name="Description"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Whether this plan is available for selection",
|
||||||
|
verbose_name="Is active",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"memory_requests",
|
||||||
|
models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
verbose_name="Memory requests",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"memory_limits",
|
||||||
|
models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
verbose_name="Memory limits",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cpu_requests",
|
||||||
|
models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
verbose_name="CPU requests",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cpu_limits",
|
||||||
|
models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
verbose_name="CPU limits",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Compute Plan",
|
||||||
|
"verbose_name_plural": "Compute Plans",
|
||||||
|
"ordering": ["name"],
|
||||||
|
},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="controlplane",
|
||||||
|
name="storage_plan_odoo_product_id",
|
||||||
|
field=models.IntegerField(
|
||||||
|
blank=True,
|
||||||
|
help_text="ID of the storage product in Odoo",
|
||||||
|
null=True,
|
||||||
|
verbose_name="Storage plan Odoo product ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="controlplane",
|
||||||
|
name="storage_plan_odoo_unit_id",
|
||||||
|
field=models.IntegerField(
|
||||||
|
blank=True,
|
||||||
|
help_text="ID of the unit of measure in Odoo (uom.uom)",
|
||||||
|
null=True,
|
||||||
|
verbose_name="Storage plan Odoo unit ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="controlplane",
|
||||||
|
name="storage_plan_price_per_gib",
|
||||||
|
field=models.DecimalField(
|
||||||
|
blank=True,
|
||||||
|
decimal_places=2,
|
||||||
|
help_text="Price per GiB of storage",
|
||||||
|
max_digits=10,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Storage plan price per GiB",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ComputePlanAssignment",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="Created"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(auto_now=True, verbose_name="Last updated"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sla",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("besteffort", "Best Effort"),
|
||||||
|
("guaranteed", "Guaranteed Availability"),
|
||||||
|
],
|
||||||
|
help_text="Service Level Agreement",
|
||||||
|
max_length=20,
|
||||||
|
verbose_name="SLA",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"odoo_product_id",
|
||||||
|
models.IntegerField(
|
||||||
|
help_text="ID of the product in Odoo (product.product or product.template)",
|
||||||
|
verbose_name="Odoo product ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"odoo_unit_id",
|
||||||
|
models.IntegerField(
|
||||||
|
help_text="ID of the unit of measure in Odoo (uom.uom)",
|
||||||
|
verbose_name="Odoo unit ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"price",
|
||||||
|
models.DecimalField(
|
||||||
|
decimal_places=2,
|
||||||
|
help_text="Price per unit",
|
||||||
|
max_digits=10,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(Decimal("0.00"))
|
||||||
|
],
|
||||||
|
verbose_name="Price",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"minimum_service_size",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=1,
|
||||||
|
help_text="Minimum value for spec.parameters.instances (Guaranteed Availability may require multiple instances)",
|
||||||
|
validators=[django.core.validators.MinValueValidator(1)],
|
||||||
|
verbose_name="Minimum service size",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sort_order",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=0,
|
||||||
|
help_text="Order in which plans are displayed to users",
|
||||||
|
verbose_name="Sort order",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Whether this plan is available for this CRD",
|
||||||
|
verbose_name="Is active",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"compute_plan",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="assignments",
|
||||||
|
to="core.computeplan",
|
||||||
|
verbose_name="Compute plan",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"control_plane_crd",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="compute_plan_assignments",
|
||||||
|
to="core.controlplanecrd",
|
||||||
|
verbose_name="Control plane CRD",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Compute Plan Assignment",
|
||||||
|
"verbose_name_plural": "Compute Plan Assignments",
|
||||||
|
"ordering": ["sort_order", "compute_plan__name", "sla"],
|
||||||
|
"unique_together": {("compute_plan", "control_plane_crd", "sla")},
|
||||||
|
},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="serviceinstance",
|
||||||
|
name="compute_plan_assignment",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Compute plan with SLA for this instance",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="instances",
|
||||||
|
to="core.computeplanassignment",
|
||||||
|
verbose_name="Compute plan assignment",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="OdooObjectCache",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="Created"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(auto_now=True, verbose_name="Last updated"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"odoo_model",
|
||||||
|
models.CharField(
|
||||||
|
help_text="Odoo model name: 'product.product', 'product.template', 'uom.uom', etc.",
|
||||||
|
max_length=100,
|
||||||
|
verbose_name="Odoo model",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"odoo_id",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
help_text="ID in the Odoo model", verbose_name="Odoo ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
models.JSONField(
|
||||||
|
help_text="Cached Odoo data including price, reporting_product_id, etc.",
|
||||||
|
verbose_name="Cached data",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"expires_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="When cache should be refreshed (null = never expires)",
|
||||||
|
null=True,
|
||||||
|
verbose_name="Expires at",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Odoo Object Cache",
|
||||||
|
"verbose_name_plural": "Odoo Object Caches",
|
||||||
|
"indexes": [
|
||||||
|
models.Index(
|
||||||
|
fields=["odoo_model", "odoo_id"],
|
||||||
|
name="core_odooob_odoo_mo_51e258_idx",
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["expires_at"], name="core_odooob_expires_8fc00b_idx"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"unique_together": {("odoo_model", "odoo_id")},
|
||||||
|
},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Generated by Django 5.2.8 on 2025-12-02 10:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0016_computeplan_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="computeplanassignment",
|
||||||
|
name="unit",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("hour", "Hour"),
|
||||||
|
("day", "Day"),
|
||||||
|
("month", "Month (30 days)"),
|
||||||
|
("year", "Year"),
|
||||||
|
],
|
||||||
|
default="hour",
|
||||||
|
help_text="Unit for the price (e.g., price per hour)",
|
||||||
|
max_length=10,
|
||||||
|
verbose_name="Billing unit",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="computeplanassignment",
|
||||||
|
name="odoo_product_id",
|
||||||
|
field=models.CharField(
|
||||||
|
help_text="Product ID in Odoo (e.g., 'openshift-exoscale-workervcpu-standard')",
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="Odoo product ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="computeplanassignment",
|
||||||
|
name="odoo_unit_id",
|
||||||
|
field=models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="Odoo unit ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="controlplane",
|
||||||
|
name="storage_plan_odoo_product_id",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Storage product ID in Odoo",
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Storage plan Odoo product ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="controlplane",
|
||||||
|
name="storage_plan_odoo_unit_id",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Unit of measure ID in Odoo",
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Storage plan Odoo unit ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from auditlog.registry import auditlog
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
@ -29,26 +30,21 @@ class ComputePlan(ServalaModelMixin):
|
||||||
help_text=_("Whether this plan is available for selection"),
|
help_text=_("Whether this plan is available for selection"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Kubernetes resource specifications (use Kubernetes format: "2Gi", "500m")
|
|
||||||
memory_requests = models.CharField(
|
memory_requests = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
verbose_name=_("Memory requests"),
|
verbose_name=_("Memory requests"),
|
||||||
help_text=_("e.g., '2Gi', '512Mi'"),
|
|
||||||
)
|
)
|
||||||
memory_limits = models.CharField(
|
memory_limits = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
verbose_name=_("Memory limits"),
|
verbose_name=_("Memory limits"),
|
||||||
help_text=_("e.g., '4Gi', '1Gi'"),
|
|
||||||
)
|
)
|
||||||
cpu_requests = models.CharField(
|
cpu_requests = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
verbose_name=_("CPU requests"),
|
verbose_name=_("CPU requests"),
|
||||||
help_text=_("e.g., '500m', '1', '2'"),
|
|
||||||
)
|
)
|
||||||
cpu_limits = models.CharField(
|
cpu_limits = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
verbose_name=_("CPU limits"),
|
verbose_name=_("CPU limits"),
|
||||||
help_text=_("e.g., '2000m', '2', '4'"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -60,12 +56,6 @@ class ComputePlan(ServalaModelMixin):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_resource_summary(self):
|
def get_resource_summary(self):
|
||||||
"""
|
|
||||||
Get a human-readable summary of resources.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
String like "2 vCPU, 4Gi RAM"
|
|
||||||
"""
|
|
||||||
return f"{self.cpu_limits} vCPU, {self.memory_limits} RAM"
|
return f"{self.cpu_limits} vCPU, {self.memory_limits} RAM"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,26 +85,23 @@ class ComputePlanAssignment(ServalaModelMixin):
|
||||||
related_name="compute_plan_assignments",
|
related_name="compute_plan_assignments",
|
||||||
verbose_name=_("Control plane CRD"),
|
verbose_name=_("Control plane CRD"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Service Level Agreement
|
|
||||||
sla = models.CharField(
|
sla = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=SLA_CHOICES,
|
choices=SLA_CHOICES,
|
||||||
verbose_name=_("SLA"),
|
verbose_name=_("SLA"),
|
||||||
help_text=_("Service Level Agreement"),
|
help_text=_("Service Level Agreement"),
|
||||||
)
|
)
|
||||||
|
odoo_product_id = models.CharField(
|
||||||
# Odoo product reference
|
max_length=255,
|
||||||
odoo_product_id = models.IntegerField(
|
|
||||||
verbose_name=_("Odoo product ID"),
|
verbose_name=_("Odoo product ID"),
|
||||||
help_text=_("ID of the product in Odoo (product.product or product.template)"),
|
help_text=_(
|
||||||
|
"Product ID in Odoo (e.g., 'openshift-exoscale-workervcpu-standard')"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
odoo_unit_id = models.IntegerField(
|
odoo_unit_id = models.CharField(
|
||||||
|
max_length=255,
|
||||||
verbose_name=_("Odoo unit ID"),
|
verbose_name=_("Odoo unit ID"),
|
||||||
help_text=_("ID of the unit of measure in Odoo (uom.uom)"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pricing
|
|
||||||
price = models.DecimalField(
|
price = models.DecimalField(
|
||||||
max_digits=10,
|
max_digits=10,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
|
|
@ -123,7 +110,20 @@ class ComputePlanAssignment(ServalaModelMixin):
|
||||||
help_text=_("Price per unit"),
|
help_text=_("Price per unit"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Service constraints
|
BILLING_UNIT_CHOICES = [
|
||||||
|
("hour", _("Hour")),
|
||||||
|
("day", _("Day")),
|
||||||
|
("month", _("Month (30 days / 720 hours)")),
|
||||||
|
("year", _("Year")),
|
||||||
|
]
|
||||||
|
unit = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=BILLING_UNIT_CHOICES,
|
||||||
|
default="hour",
|
||||||
|
verbose_name=_("Billing unit"),
|
||||||
|
help_text=_("Unit for the price (e.g., price per hour)"),
|
||||||
|
)
|
||||||
|
|
||||||
minimum_service_size = models.PositiveIntegerField(
|
minimum_service_size = models.PositiveIntegerField(
|
||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
|
|
@ -133,15 +133,11 @@ class ComputePlanAssignment(ServalaModelMixin):
|
||||||
"(Guaranteed Availability may require multiple instances)"
|
"(Guaranteed Availability may require multiple instances)"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Display ordering in UI
|
|
||||||
sort_order = models.PositiveIntegerField(
|
sort_order = models.PositiveIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
verbose_name=_("Sort order"),
|
verbose_name=_("Sort order"),
|
||||||
help_text=_("Order in which plans are displayed to users"),
|
help_text=_("Order in which plans are displayed to users"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Allow per-assignment activation
|
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name=_("Is active"),
|
verbose_name=_("Is active"),
|
||||||
|
|
@ -158,15 +154,12 @@ class ComputePlanAssignment(ServalaModelMixin):
|
||||||
return f"{self.compute_plan.name} ({self.get_sla_display()}) → {self.control_plane_crd}"
|
return f"{self.compute_plan.name} ({self.get_sla_display()}) → {self.control_plane_crd}"
|
||||||
|
|
||||||
def get_odoo_reporting_product_id(self):
|
def get_odoo_reporting_product_id(self):
|
||||||
"""
|
|
||||||
Get the reporting product ID for this plan.
|
|
||||||
|
|
||||||
In the future, this will query Odoo based on invoicing policy.
|
|
||||||
For now, returns the product ID directly.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The Odoo product ID to use for billing
|
|
||||||
"""
|
|
||||||
# TODO: Implement Odoo cache lookup when OdooObjectCache is integrated
|
# TODO: Implement Odoo cache lookup when OdooObjectCache is integrated
|
||||||
# For now, just return the product ID
|
# For now, just return the product ID
|
||||||
return self.odoo_product_id
|
return self.odoo_product_id
|
||||||
|
|
||||||
|
|
||||||
|
auditlog.register(ComputePlan, exclude_fields=["updated_at"], serialize_data=True)
|
||||||
|
auditlog.register(
|
||||||
|
ComputePlanAssignment, exclude_fields=["updated_at"], serialize_data=True
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -170,18 +170,19 @@ class ControlPlane(ServalaModelMixin, models.Model):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Storage plan configuration (hardcoded per control plane)
|
storage_plan_odoo_product_id = models.CharField(
|
||||||
storage_plan_odoo_product_id = models.IntegerField(
|
max_length=255,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Storage plan Odoo product ID"),
|
verbose_name=_("Storage plan Odoo product ID"),
|
||||||
help_text=_("ID of the storage product in Odoo"),
|
help_text=_("Storage product ID in Odoo"),
|
||||||
)
|
)
|
||||||
storage_plan_odoo_unit_id = models.IntegerField(
|
storage_plan_odoo_unit_id = models.CharField(
|
||||||
|
max_length=255,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Storage plan Odoo unit ID"),
|
verbose_name=_("Storage plan Odoo unit ID"),
|
||||||
help_text=_("ID of the unit of measure in Odoo (uom.uom)"),
|
help_text=_("Unit of measure ID in Odoo"),
|
||||||
)
|
)
|
||||||
storage_plan_price_per_gib = models.DecimalField(
|
storage_plan_price_per_gib = models.DecimalField(
|
||||||
max_digits=10,
|
max_digits=10,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue