Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ The following Django apps are defined in `src/backend/InvenTree/`:
| `machine/` | Support for external machines and devices |
| `order/` | Purchase orders and sales orders |
| `part/` | Parts catalogue and categories |
| `pricing/` | Pricing calculation and caching |
| `stock/` | Stock items and locations |
| `report/` | Report templates and generation |
| `plugin/` | Plugin system |
Expand Down
1 change: 1 addition & 0 deletions src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@
'machine.apps.MachineConfig',
'data_exporter.apps.DataExporterConfig',
'importer.apps.ImporterConfig',
'pricing.apps.PricingConfig',
'web',
'generic',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
Expand Down
Empty file.
16 changes: 16 additions & 0 deletions src/backend/InvenTree/pricing/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Admin class definitions for the pricing app."""

from django.contrib import admin

from .models import StockItemCost


@admin.register(StockItemCost)
class StockItemCostAdmin(admin.ModelAdmin):
"""Admin class for the StockItemCost model."""

list_display = ['stock_item', 'part', 'cost_type', 'min_cost', 'max_cost', 'date']

list_filter = ['cost_type']

autocomplete_fields = ['stock_item', 'part', 'user']
1 change: 1 addition & 0 deletions src/backend/InvenTree/pricing/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""API endpoints for the pricing app."""
10 changes: 10 additions & 0 deletions src/backend/InvenTree/pricing/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""AppConfig for the 'pricing' app."""

from django.apps import AppConfig


class PricingConfig(AppConfig):
"""AppConfig class for the 'pricing' app."""

default_auto_field = 'django.db.models.BigAutoField'
name = 'pricing'
173 changes: 173 additions & 0 deletions src/backend/InvenTree/pricing/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Generated by Django 5.2.16 on 2026-08-01 05:10

import InvenTree.fields
import django.db.models.deletion
import djmoney.models.fields
import djmoney.models.validators
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
("part", "0152_alter_partpricing_currency"),
("stock", "0126_serial_number_concurrency_guard"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="StockItemCost",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"cost_type",
models.PositiveIntegerField(
choices=[
(10, "Purchase"),
(20, "Landed"),
(30, "Manufacturing"),
(40, "Manual"),
(50, "System"),
],
default=10,
help_text="Source of this cost entry",
verbose_name="Cost Type",
),
),
(
"min_cost_currency",
djmoney.models.fields.CurrencyField(
choices=[], default="", editable=False, max_length=3, null=True
),
),
(
"min_cost",
InvenTree.fields.InvenTreeModelMoneyField(
blank=True,
currency_choices=[],
decimal_places=6,
default_currency="",
help_text="Minimum estimated cost for this entry",
max_digits=19,
null=True,
validators=[djmoney.models.validators.MinMoneyValidator(0)],
verbose_name="Minimum Cost",
),
),
(
"max_cost_currency",
djmoney.models.fields.CurrencyField(
choices=[], default="", editable=False, max_length=3, null=True
),
),
(
"max_cost",
InvenTree.fields.InvenTreeModelMoneyField(
blank=True,
currency_choices=[],
decimal_places=6,
default_currency="",
help_text="Maximum estimated cost for this entry",
max_digits=19,
null=True,
validators=[djmoney.models.validators.MinMoneyValidator(0)],
verbose_name="Maximum Cost",
),
),
(
"cost_currency",
djmoney.models.fields.CurrencyField(
choices=[], default="", editable=False, max_length=3, null=True
),
),
(
"cost",
InvenTree.fields.InvenTreeModelMoneyField(
blank=True,
currency_choices=[],
decimal_places=6,
default_currency="",
help_text="Single point-value estimate for this entry",
max_digits=19,
null=True,
validators=[djmoney.models.validators.MinMoneyValidator(0)],
verbose_name="Cost",
),
),
(
"date",
models.DateTimeField(
auto_now_add=True,
help_text="Date at which this cost entry was calculated",
verbose_name="Date",
),
),
(
"source_data",
models.JSONField(
blank=True,
help_text="Source data used to calculate this cost entry",
null=True,
verbose_name="Source Data",
),
),
(
"notes",
models.CharField(
blank=True,
help_text="Notes associated with this cost entry",
max_length=512,
verbose_name="Notes",
),
),
(
"part",
models.ForeignKey(
editable=False,
help_text="Part associated with this cost entry",
on_delete=django.db.models.deletion.CASCADE,
related_name="stock_cost_entries",
to="part.part",
verbose_name="Part",
),
),
(
"stock_item",
models.ForeignKey(
help_text="Stock item to which this cost entry applies",
on_delete=django.db.models.deletion.CASCADE,
related_name="cost_entries",
to="stock.stockitem",
verbose_name="Stock Item",
),
),
(
"user",
models.ForeignKey(
blank=True,
help_text="User associated with this cost calculation",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
],
options={
"verbose_name": "Stock Item Cost",
"ordering": ["-date"],
},
),
]
Empty file.
123 changes: 123 additions & 0 deletions src/backend/InvenTree/pricing/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Database models for the 'pricing' app."""

from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _

import InvenTree.fields

from .status_codes import CostType


class StockItemCost(models.Model):
"""Model representing a single landed-cost calculation for a StockItem.

This is an append-only ledger - a new entry is created every time the cost
of a StockItem is (re)calculated, rather than updating a single record in place.
This preserves a full history of how the cost of an item has changed over time
(e.g. as purchase invoices, duties, or freight costs are reconciled).

Attributes:
stock_item: The StockItem that this cost entry applies to
part: The Part associated with the linked StockItem (denormalized for query convenience)
cost_type: The type (source) of this cost entry
min_cost: The minimum estimated cost for this entry
max_cost: The maximum estimated cost for this entry
cost: A single point-value estimate for this entry (e.g. a weighted / representative cost)
date: Date at which this cost entry was calculated
user: The user associated with this cost calculation (nullable, e.g. for automated calculations)
source_data: JSON field capturing the source data used to calculate this cost
notes: Optional notes associated with this cost entry
"""

class Meta:
"""Meta options for the StockItemCost model."""

verbose_name = _('Stock Item Cost')
ordering = ['-date']

stock_item = models.ForeignKey(
'stock.StockItem',
on_delete=models.CASCADE,
related_name='cost_entries',
verbose_name=_('Stock Item'),
help_text=_('Stock item to which this cost entry applies'),
)

part = models.ForeignKey(
'part.Part',
on_delete=models.CASCADE,
related_name='stock_cost_entries',
editable=False,
verbose_name=_('Part'),
help_text=_('Part associated with this cost entry'),
)

cost_type = models.PositiveIntegerField(
default=CostType.PURCHASE.value,
choices=CostType.items(),
verbose_name=_('Cost Type'),
help_text=_('Source of this cost entry'),
)

min_cost = InvenTree.fields.InvenTreeModelMoneyField(
null=True,
blank=True,
verbose_name=_('Minimum Cost'),
help_text=_('Minimum estimated cost for this entry'),
)

max_cost = InvenTree.fields.InvenTreeModelMoneyField(
null=True,
blank=True,
verbose_name=_('Maximum Cost'),
help_text=_('Maximum estimated cost for this entry'),
)

cost = InvenTree.fields.InvenTreeModelMoneyField(
null=True,
blank=True,
verbose_name=_('Cost'),
help_text=_('Single point-value estimate for this entry'),
)

date = models.DateTimeField(
auto_now_add=True,
editable=False,
verbose_name=_('Date'),
help_text=_('Date at which this cost entry was calculated'),
)

user = models.ForeignKey(
get_user_model(),
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('User'),
help_text=_('User associated with this cost calculation'),
)

source_data = models.JSONField(
null=True,
blank=True,
verbose_name=_('Source Data'),
help_text=_('Source data used to calculate this cost entry'),
)

notes = models.CharField(
max_length=512,
blank=True,
verbose_name=_('Notes'),
help_text=_('Notes associated with this cost entry'),
)

def save(self, *args, **kwargs):
"""Ensure that the 'part' link is always up to date."""
if self.stock_item:
self.part = self.stock_item.part

super().save(*args, **kwargs)

def __str__(self):
"""Return string representation of this cost entry."""
return f'{self.stock_item} - {CostType(self.cost_type).label}'
1 change: 1 addition & 0 deletions src/backend/InvenTree/pricing/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""DRF API serializers for the pricing app."""
23 changes: 23 additions & 0 deletions src/backend/InvenTree/pricing/status_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Status codes for the 'pricing' app."""

from django.utils.translation import gettext_lazy as _

from generic.states import ColorEnum, StatusCode


class CostType(StatusCode):
"""Defines the type (source) of a StockItemCost entry.

Attributes:
PURCHASE: Cost taken directly from a purchase (e.g. supplier price break, PO line item)
LANDED: Purchase cost plus additional landed costs (freight, duty, handling, etc)
MANUFACTURING: Cost calculated from a build order (BOM cost plus labor / overhead)
MANUAL: Cost manually entered (or overridden) by a user
SYSTEM: Cost calculated automatically by the pricing system (e.g. a pricing plugin)
"""

PURCHASE = 10, _('Purchase'), ColorEnum.primary
LANDED = 20, _('Landed'), ColorEnum.info
MANUFACTURING = 30, _('Manufacturing'), ColorEnum.secondary
MANUAL = 40, _('Manual'), ColorEnum.warning
SYSTEM = 50, _('System'), ColorEnum.success
1 change: 1 addition & 0 deletions src/backend/InvenTree/users/oauth2_scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def get_granular_scope(method, role=None, type='r'):
'admin': 'Role Admin',
'part_category': 'Role Part Categories',
'part': 'Role Parts',
'pricing': 'Role Part Pricing',
'stock_location': 'Role Stock Locations',
'stock': 'Role Stock Items',
'bom': 'Role Bills of Material',
Expand Down
Loading
Loading