Squash migrations (#12830)

* Squash migrations for "users" app

* Squash migrations for "common" app

Note: we will come back again later to squash some more

* Significant squashing

* Update legacy migrations

* Further squashing of migrations

* Even more cleanup

* Adjust CI workflow

* Update CI Job

* Update docs

* Update CHANGELOG.md

* Cleanup old comments

* Throw error if pre 1.0.0 DB detected

* Refactor incomplete migration check
This commit is contained in:
Oliver
2026-09-11 16:03:41 +10:00
committed by GitHub
parent 028d1cba1f
commit c26db6a790
548 changed files with 5875 additions and 18451 deletions
+38 -30
View File
@@ -50,7 +50,7 @@ jobs:
contains(github.event.pull_request.labels.*.name, 'full-run')
sqlite:
name: Tests - Migrations [SQLite]
name: Migrations [SQLite]
runs-on: ubuntu-latest
needs: paths-filter
if: ${{ (needs.paths-filter.outputs.force == 'true') || (github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true') }}
@@ -83,7 +83,7 @@ jobs:
flags: migrations-sqlite
mysql:
name: Tests - Migrations [MySQL]
name: Migrations [MySQL]
runs-on: ubuntu-24.04
needs: paths-filter
if: ${{ (needs.paths-filter.outputs.force == 'true') || (github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true') }}
@@ -134,7 +134,7 @@ jobs:
flags: migrations-mysql
postgresql:
name: Tests - Migrations [PostgreSQL]
name: Migrations [PostgreSQL]
runs-on: ubuntu-latest
needs: paths-filter
if: ${{ (needs.paths-filter.outputs.force == 'true') || (github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true') }}
@@ -181,7 +181,7 @@ jobs:
flags: migrations-postgresql
migrations-checks:
name: Tests - Database Migrations
name: Database Migrations
runs-on: ubuntu-latest
needs: paths-filter
if: ${{ (needs.paths-filter.outputs.force == 'true') || (github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true') }}
@@ -205,37 +205,45 @@ jobs:
- name: Fetch Database
run: git clone --depth 1 https://github.com/inventree/test-db ./test-db
- name: 0.10.0 Database
- name: 1.0.0 Database
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_1.0.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
- name: 1.5.0 Database
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_1.5.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
- name: 0.10.0 Database (SHOULD FAIL)
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_0.10.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
- name: 0.11.0 Database
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_0.11.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
set +e
output=$(invoke migrate 2>&1)
exit_code=$?
set -e
- name: 0.13.5 Database
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_0.13.5.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
# Always show the actual output, so the reason for pass/fail is visible
# in the job log rather than just a generic summary line
echo "$output"
- name: 0.16.0 Database
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_0.16.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
if [ "$exit_code" -eq 0 ]; then
echo "::error::Migration succeeded for 0.10.0 database, but this is expected to fail (squashed migrations no longer support this old baseline)"
exit 1
fi
- name: 0.17.0 Database
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_0.17.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
# Confirm it failed for the *expected* reason (INVE-E19: stuck mid-way
# through the pre-1.0.0 migration squash), not some unrelated crash
if echo "$output" | grep -q "INVE-E19"; then
echo "Migration failed as expected for 0.10.0 database (INVE-E19)"
else
echo "::error::Migration failed for 0.10.0 database, but not with the expected INVE-E19 error - this looks like an unrelated failure"
exit 1
fi
+1
View File
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Breaking Changes
- [#12830](https://github.com/inventree/InvenTree/pull/12830) squashes all database migrations prior to the 1.0.0 release. This means that any users who are updating from a version older than 1.0.0 must first update to the 1.0.0 release before updating to the current release.
- [#11971](https://github.com/inventree/InvenTree/pull/11971) is a major refactor of how notes are handled. Notes are now stored in a separate database table (in line with how attachments are handled), and each model instance can have multiple notes associated with it. The `notes` field has been removed from the individual models (and their associated API endpoints), and notes are now accessed via the new `/api/note/` endpoint. Existing notes data (and any embedded images) are automatically migrated to the new notes table, with the markdown content converted to HTML. Any external client applications which read or write the `notes` field via the API will need to be updated to use the new endpoint.
- [#12507](https://github.com/inventree/InvenTree/pull/12507) calling an invalid or repeated state transition now raises a ValidationError. Plugins implementing state transitions should evaluate the PR and adapt their usage of transitions to gain the new safeguards.
- [#12672](https://github.com/inventree/InvenTree/pull/12672) renames the newly added `tags` filter from 1.4.0 (https://github.com/inventree/InvenTree/pull/12077) to `tag_name` to remove a nameclash.
+4
View File
@@ -107,10 +107,14 @@ For more information, refer to the installation guides:
- [Docker Installation](./start/docker_install.md#updating-inventree)
- [Bare Metal Installation](./start/install.md#updating-inventree)
- [Package Installer](./start/installer.md#updating-inventree)
!!! warning "Invoke Update"
You must ensure that the `invoke update` command is performed *every time* you update InvenTree
!!! danger "Updating from Pre 1.0.0"
If your installation is running a version of InvenTree older than `1.0.0`, you cannot update directly to the current release - see [Updating from Pre 1.0.0](./start/migrate.md#updating-from-pre-100) for the required intermediate step.
### Breaking Changes
Before performing an update, check the release notes! Any *breaking changes* (changes which require user intervention) will be clearly noted.
+5
View File
@@ -116,6 +116,11 @@ An error occurred while rendering a component in the frontend. Typically this is
The InvenTree server failed to start, because the configured server URL does not include a top-level domain (TLD). A valid TLD is required for proper operation.
#### INVE-E19
**Database stuck mid-way through pre-1.0.0 migration squash - Backend**
If your installation is currently running a version of InvenTree older than `1.0.0`, you must first update to the `1.0.0` release before updating further. See [Updating from Pre 1.0.0](../start/migrate.md#updating-from-pre-100) for the full upgrade procedure.
## Warning Codes
### INVE-W (InvenTree Warning)
+3
View File
@@ -118,6 +118,9 @@ This command launches the following containers:
To update your InvenTree installation to the latest version, follow these steps:
!!! danger "Updating from Pre 1.0.0"
If your installation is running a version of InvenTree older than `1.0.0`, you cannot update directly to the current release. See [Updating from Pre 1.0.0](./migrate.md#updating-from-pre-100) for the required intermediate step.
### Stop Containers
Stop all running containers as below:
+3
View File
@@ -289,6 +289,9 @@ Administrators wishing to update InvenTree to the latest version should follow t
!!! info "Update Database"
It is advisable to [backup the InvenTree database](./backup.md) before performing these steps. The particular backup procedure may depend on your installation details.
!!! danger "Updating from Pre 1.0.0"
If your installation is running a version of InvenTree older than `1.0.0`, you cannot update directly to the current release. See [Updating from Pre 1.0.0](./migrate.md#updating-from-pre-100) for the required intermediate step.
### Stop InvenTree Server
Ensure the InvenTree server is stopped. This will depend on the particulars of your database installation.
+3
View File
@@ -140,6 +140,9 @@ To change the data storage location, link the new location to `/opt/inventree/da
## Updating InvenTree
!!! danger "Updating from Pre 1.0.0"
If your installation is running a version of InvenTree older than `1.0.0`, you cannot update directly to the current release. See [Updating from Pre 1.0.0](./migrate.md#updating-from-pre-100) for the required intermediate step.
To update InvenTree run the following command, which updates the InvenTree package to the latest version:
```bash
+21
View File
@@ -2,6 +2,24 @@
title: Migrating Data
---
## Updating from Pre 1.0.0
!!! danger "Required Stopover"
As part of the 1.0.0 release cycle, InvenTree's database migration history was *squashed* - many individual migration files were consolidated into a smaller number of squashed migrations, and the original (now-redundant) migration files were subsequently removed from the codebase.
As a result, InvenTree cannot migrate a database directly from a version **older than 1.0.0** to the current release. Attempting to do so will cause `invoke update` (or `invoke migrate`) to fail.
If your installation is currently running a version of InvenTree older than `1.0.0`, you must first update to the `1.0.0` release, before updating to the current release.
### How to Update
1. Determine your current InvenTree version. If it is older than `1.0.0`, do not skip directly to the latest release.
2. Follow the normal update procedure for your installation method - [Bare Metal](./install.md#updating-inventree), [Docker](./docker_install.md#updating-inventree), or [Package Installer](./installer.md#updating-inventree) - targeting a `1.0.0` release.
3. Once the database has been successfully updated to `1.0.0`, repeat the update procedure again to bring the installation up to the current release.
!!! danger "Skipping Directly to Latest"
Attempting to update directly from a pre-1.0.0 database to the current release, skipping the `1.0.0` stopover, is not supported and will fail.
## Migrating Data to a Different Database
In the case that data needs to be migrated from one database installation to another, the following procedure can be used to export data, initialize the new database, and re-import the data. The following instructions apply to bare-metal and docker installations, although the particular commands required may vary slightly in each case.
@@ -100,6 +118,9 @@ Copy the entire directory tree from the original InvenTree installation to the n
If you are updating from an older version of InvenTree to a newer version, the migration steps outlined above *do not apply*.
!!! danger "Updating from Pre 1.0.0"
If your existing installation is running a version older than `1.0.0`, you cannot update directly to the current release. See [Updating from Pre 1.0.0](#updating-from-pre-100) above for the required intermediate step.
An update from an old version to a new one requires not only that the database *schema* are updated, but the *data* held within the database must be updated in the correct sequence.
Follow the sequence of steps below to ensure that the database records are updated correctly.
+33
View File
@@ -22,6 +22,7 @@ from InvenTree.ready import ignore_ready_warning
logger = structlog.get_logger('inventree')
MIGRATIONS_CHECK_DONE = False
PRE_1_0_0_CHECK_DONE = False
OIDC_CLIENT_CHECKED = False
DEFAULT_OIDC_APP_ID = 'zDFnsiRheJIOKNx6aCQ0quBxECg1QBHtVFDPloJ6'
@@ -55,6 +56,13 @@ class InvenTreeConfig(AppConfig):
):
return
# Check for a database stuck mid-way through the pre-1.0.0 migration squash.
if (
InvenTree.ready.canAppAccessDatabase(allow_plugins=True)
or settings.TESTING_ENV
):
self.check_pre_1_0_0_upgrade()
# Skip if running migrations
if InvenTree.ready.isRunningMigrations():
return
@@ -370,6 +378,31 @@ class InvenTreeConfig(AppConfig):
logger.info('Default OIDC client created: %s', client)
OIDC_CLIENT_CHECKED = True
def check_pre_1_0_0_upgrade(self=None):
"""Check for a database stuck mid-way through the pre-1.0.0 migration squash."""
global PRE_1_0_0_CHECK_DONE
if PRE_1_0_0_CHECK_DONE:
return
if not InvenTree.ready.canAppAccessDatabase(allow_plugins=True):
return
if stuck_apps := InvenTree.tasks.get_stuck_pre_1_0_0_apps():
docs = 'https://docs.inventree.org/en/stable/start/migrate/#updating-from-pre-100'
logger.error(
'INVE-E19: Database is only partially migrated through a pre-1.0.0 '
'install, for app(s): %s\n'
'This instance must first be fully upgraded to InvenTree 1.0.0 '
'before upgrading further.\n'
'- Refer to the InvenTree documentation for more information:\n'
'- %s',
', '.join(stuck_apps),
docs,
)
sys.exit(1)
PRE_1_0_0_CHECK_DONE = True
def ensure_migrations_done(self=None):
"""Ensures there are no open migrations, stop if inconsistent state."""
global MIGRATIONS_CHECK_DONE
+57
View File
@@ -995,6 +995,63 @@ def get_migration_count():
return executor.loader.applied_migrations
# The first and last individual migrations of each pre-1.0.0 squash range,
# for every app squashed as part of the pre-1.0.0 migration-history cleanup.
PRE_1_0_0_MIGRATION_BOUNDARIES = [
('common', '0001_initial', '0007_colortheme'),
(
'common',
'0008_remove_inventreesetting_description',
'0039_emailthread_emailmessage',
),
('build', '0006_auto_20190913_1407', '0015_auto_20200425_1350'),
('build', '0017_auto_20200426_0612', '0058_buildline_consumed'),
('company', '0003_remove_supplierpart_minimum', '0047_supplierpart_pack_size'),
('company', '0048_auto_20220913_0312', '0075_company_tax_id'),
('order', '0001_initial', '0023_auto_20200420_2309'),
('order', '0031_auto_20200426_0612', '0112_alter_salesorderlineitem_part'),
('stock', '0002_auto_20190525_2226', '0030_auto_20200422_0015'),
('stock', '0059_auto_20210404_2016', '0116_alter_stockitem_link'),
('part', '0003_auto_20190525_2226', '0060_merge_20201112_1722'),
(
'part',
'0061_auto_20210103_2313',
'0142_remove_part_last_stocktake_remove_partstocktake_note_and_more',
),
('users', '0001_initial', '0015_alter_userprofile_type'),
]
def get_stuck_pre_1_0_0_apps() -> list:
"""Detect apps stuck mid-way through the pre-1.0.0 migration squash.
A database which has applied the *first* migration of one of
PRE_1_0_0_MIGRATION_BOUNDARIES's ranges but not the *last* is stuck
between the old, granular history and the squashed one.
Returns a list of app labels which are in this "stuck" state. An empty
list means it is safe to proceed with migrations.
"""
from django.db.migrations.recorder import MigrationRecorder
connection = connections[DEFAULT_DB_ALIAS]
recorder = MigrationRecorder(connection)
if not recorder.has_table():
# No migrations have ever been recorded - a genuinely fresh database
return []
applied = recorder.applied_migrations()
stuck_apps = set()
for app_label, first, last in PRE_1_0_0_MIGRATION_BOUNDARIES:
if (app_label, first) in applied and (app_label, last) not in applied:
stuck_apps.add(app_label)
return sorted(stuck_apps)
@tracer.start_as_current_span('check_for_migrations')
@scheduled_task(ScheduledTask.DAILY)
def check_for_migrations(force: bool = False, reload_registry: bool = True) -> bool:
@@ -1,19 +0,0 @@
# Generated by Django 2.2.5 on 2019-09-13 14:07
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('build', '0005_auto_20190604_2217'),
]
operations = [
migrations.AlterField(
model_name='build',
name='URL',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL'),
),
]
@@ -0,0 +1,197 @@
# Generated by Django 5.2.17 on 2026-09-09 23:26
import InvenTree.fields
import django.core.validators
import django.db.models.deletion
import mptt.fields
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
("build", "0006_auto_20190913_1407"),
("build", "0007_auto_20191118_2321"),
("build", "0008_auto_20200201_1247"),
("build", "0009_auto_20200210_1032"),
("build", "0010_auto_20200318_1027"),
("build", "0011_auto_20200406_0123"),
("build", "0012_build_sales_order"),
("build", "0013_auto_20200425_0507"),
("build", "0014_auto_20200425_1243"),
("build", "0015_auto_20200425_1350"),
]
dependencies = [
("build", "0005_auto_20190604_2217"),
("order", "0029_auto_20200423_1042"),
("part", "0035_auto_20200406_0045"),
("stock", "0031_auto_20200422_0209"),
]
operations = [
migrations.AlterField(
model_name="build",
name="notes",
field=models.TextField(
blank=True, help_text="Extra build notes", verbose_name="Notes"
),
),
migrations.AlterField(
model_name="build",
name="creation_date",
field=models.DateField(auto_now_add=True),
),
migrations.AlterField(
model_name="build",
name="part",
field=models.ForeignKey(
help_text="Select part to build",
limit_choices_to={
"active": True,
"assembly": True,
"is_template": False,
"virtual": False,
},
on_delete=django.db.models.deletion.CASCADE,
related_name="builds",
to="part.part",
verbose_name="Part",
),
),
migrations.RenameField(
model_name="build",
old_name="URL",
new_name="link",
),
migrations.AlterField(
model_name="build",
name="link",
field=InvenTree.fields.InvenTreeURLField(
blank=True,
help_text="Link to external URL",
max_length=2000,
verbose_name="External Link",
),
),
migrations.AddField(
model_name="build",
name="sales_order",
field=models.ForeignKey(
blank=True,
help_text="Sales Order to which this build is allocated",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="builds",
to="order.salesorder",
verbose_name="Sales Order Reference",
),
),
migrations.AddField(
model_name="build",
name="level",
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name="build",
name="lft",
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name="build",
name="rght",
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name="build",
name="tree_id",
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
preserve_default=False,
),
migrations.AlterField(
model_name="build",
name="batch",
field=models.CharField(
blank=True,
help_text="Batch code for this build output",
max_length=100,
null=True,
verbose_name="Batch Code",
),
),
migrations.AlterField(
model_name="build",
name="quantity",
field=models.PositiveIntegerField(
default=1,
help_text="Number of parts to build",
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Build Quantity",
),
),
migrations.AlterField(
model_name="build",
name="status",
field=models.PositiveIntegerField(
choices=[
(10, "Pending"),
(20, "Allocated"),
(30, "Cancelled"),
(40, "Complete"),
],
default=10,
help_text="Build status code",
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Build Status",
),
),
migrations.AlterField(
model_name="build",
name="take_from",
field=models.ForeignKey(
blank=True,
help_text="Select location to take stock from for this build (leave blank to take from any stock location)",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="sourcing_builds",
to="stock.stocklocation",
verbose_name="Source Location",
),
),
migrations.AlterField(
model_name="build",
name="title",
field=models.CharField(
help_text="Brief description of the build",
max_length=100,
verbose_name="Build Title",
),
),
migrations.AddField(
model_name="build",
name="parent",
field=mptt.fields.TreeForeignKey(
blank=True,
help_text="Parent build to which this build is allocated",
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="children",
to="build.build",
verbose_name="Parent Build",
),
),
migrations.AlterField(
model_name="builditem",
name="quantity",
field=models.DecimalField(
decimal_places=5,
default=1,
help_text="Stock quantity to allocate to build",
max_digits=15,
validators=[django.core.validators.MinValueValidator(0)],
),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 2.2.5 on 2019-11-18 23:21
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0006_auto_20190913_1407'),
]
operations = [
migrations.AlterField(
model_name='builditem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, help_text='Stock quantity to allocate to build', max_digits=15, validators=[django.core.validators.MinValueValidator(1)]),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 2.2.9 on 2020-02-01 12:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0007_auto_20191118_2321'),
]
operations = [
migrations.AlterField(
model_name='build',
name='notes',
field=models.TextField(blank=True, help_text='Extra build notes'),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 2.2.9 on 2020-02-10 10:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0008_auto_20200201_1247'),
]
operations = [
migrations.AlterField(
model_name='build',
name='creation_date',
field=models.DateField(auto_now_add=True),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 2.2.9 on 2020-03-18 10:27
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('build', '0009_auto_20200210_1032'),
]
operations = [
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'is_template': False, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 2.2.10 on 2020-04-06 01:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('build', '0010_auto_20200318_1027'),
]
operations = [
migrations.RenameField(
model_name='build',
old_name='URL',
new_name='link',
),
]
@@ -1,20 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-24 22:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('order', '0029_auto_20200423_1042'),
('build', '0011_auto_20200406_0123'),
]
operations = [
migrations.AddField(
model_name='build',
name='sales_order',
field=models.ForeignKey(blank=True, help_text='Sales Order to which this build is allocated', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds', to='order.SalesOrder'),
),
]
@@ -1,46 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-25 05:07
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
atomic = False
dependencies = [
('build', '0012_build_sales_order'),
]
operations = [
migrations.AddField(
model_name='build',
name='level',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='build',
name='lft',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='build',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build'),
),
migrations.AddField(
model_name='build',
name='rght',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='build',
name='tree_id',
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
preserve_default=False,
),
]
@@ -1,70 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-25 12:43
import InvenTree.fields
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('part', '0035_auto_20200406_0045'),
('stock', '0031_auto_20200422_0209'),
('order', '0029_auto_20200423_1042'),
('build', '0013_auto_20200425_0507'),
]
operations = [
migrations.AlterField(
model_name='build',
name='batch',
field=models.CharField(blank=True, help_text='Batch code for this build output', max_length=100, null=True, verbose_name='Batch Code'),
),
migrations.AlterField(
model_name='build',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', verbose_name='External Link'),
),
migrations.AlterField(
model_name='build',
name='notes',
field=models.TextField(blank=True, help_text='Extra build notes', verbose_name='Notes'),
),
migrations.AlterField(
model_name='build',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'),
),
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'is_template': False, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part', verbose_name='Part'),
),
migrations.AlterField(
model_name='build',
name='quantity',
field=models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'),
),
migrations.AlterField(
model_name='build',
name='sales_order',
field=models.ForeignKey(blank=True, help_text='Sales Order to which this build is allocated', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds', to='order.SalesOrder', verbose_name='Sales Order Reference'),
),
migrations.AlterField(
model_name='build',
name='status',
field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'),
),
migrations.AlterField(
model_name='build',
name='take_from',
field=models.ForeignKey(blank=True, help_text='Select location to take stock from for this build (leave blank to take from any stock location)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sourcing_builds', to='stock.StockLocation', verbose_name='Source Location'),
),
migrations.AlterField(
model_name='build',
name='title',
field=models.CharField(help_text='Brief description of the build', max_length=100, verbose_name='Build Title'),
),
]
@@ -1,26 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-25 13:50
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('build', '0014_auto_20200425_1243'),
]
operations = [
migrations.AlterField(
model_name='build',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, help_text='Parent build to which this build is allocated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'),
),
migrations.AlterField(
model_name='builditem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, help_text='Stock quantity to allocate to build', max_digits=15, validators=[django.core.validators.MinValueValidator(0)]),
),
]
@@ -8,7 +8,7 @@ class Migration(migrations.Migration):
dependencies = [
('stock', '0033_auto_20200426_0539'),
('build', '0015_auto_20200425_1350'),
('build', '0006_auto_20190913_1407_squashed_0015_auto_20200425_1350'),
]
operations = [
@@ -1,20 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-26 06:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0034_auto_20200426_0602'),
('build', '0016_auto_20200426_0551'),
]
operations = [
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Stock Item to allocate to build', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'),
),
]
@@ -0,0 +1,323 @@
# Generated by Django 5.2.17 on 2026-09-09 00:01
import InvenTree.fields
import build.status_codes
import build.validators
import django.core.validators
import django.db.models.deletion
import generic.states.fields
import generic.states.validators
import mptt.fields
from django.conf import settings
from django.db import migrations, models
def update_build_reference(apps, schema_editor):
"""Update the build order reference.
Ref: https://github.com/inventree/InvenTree/pull/3267
Performs the following steps:
- Extract existing 'prefix' value
- Generate a build order pattern based on the prefix value
- Update any existing build order references with the specified prefix
"""
InvenTreeSetting = apps.get_model('common', 'inventreesetting')
try:
prefix = InvenTreeSetting.objects.get(key='BUILDORDER_REFERENCE_PREFIX').value
except Exception:
prefix = 'BO-'
pattern = prefix + '{ref:04d}'
try:
setting = InvenTreeSetting.objects.get(key='BUILDORDER_REFERENCE_PATTERN')
setting.value = pattern
setting.save()
except InvenTreeSetting.DoesNotExist:
InvenTreeSetting.objects.create(key='BUILDORDER_REFERENCE_PATTERN', value=pattern)
Build = apps.get_model('build', 'build')
for build in Build.objects.all():
if not build.reference.startswith(prefix):
build.reference = prefix + build.reference
build.save()
class Migration(migrations.Migration):
atomic = False
replaces = [('build', '0017_auto_20200426_0612'), ('build', '0018_build_reference'), ('build', '0019_auto_20201019_1302'), ('build', '0020_auto_20201019_1325'), ('build', '0021_auto_20201020_0908_squashed_0026_auto_20201023_1228'), ('build', '0022_buildorderattachment'), ('build', '0023_auto_20201110_0911'), ('build', '0024_auto_20201201_1023'), ('build', '0025_build_target_date'), ('build', '0026_auto_20210216_1539'), ('build', '0027_auto_20210404_2016'), ('build', '0028_builditem_bom_item'), ('build', '0029_auto_20210601_1525'), ('build', '0030_alter_build_reference'), ('build', '0031_build_reference_int'), ('build', '0032_auto_20211014_0632'), ('build', '0033_auto_20211128_0151'), ('build', '0034_alter_build_reference_int'), ('build', '0035_alter_build_notes'), ('build', '0036_auto_20220707_1101'), ('build', '0037_build_priority'), ('build', '0038_alter_build_responsible'), ('build', '0039_auto_20230317_0816'), ('build', '0040_auto_20230404_1310'), ('build', '0041_alter_build_title'), ('build', '0042_alter_build_notes'), ('build', '0043_buildline'), ('build', '0044_auto_20230528_1410'), ('build', '0045_builditem_build_line'), ('build', '0046_auto_20230606_1033'), ('build', '0047_auto_20230606_1058'), ('build', '0048_build_project_code'), ('build', '0049_alter_builditem_build_line'), ('build', '0050_auto_20240508_0138'), ('build', '0051_delete_buildorderattachment'), ('build', '0052_build_status_custom_key_alter_build_status'), ('build', '0053_alter_build_part'), ('build', '0054_build_start_date'), ('build', '0055_auto_20250221_1230'), ('build', '0056_alter_build_link'), ('build', '0057_build_external'), ('build', '0058_buildline_consumed')]
dependencies = [
('build', '0016_auto_20200426_0551'),
('common', '0019_projectcode_metadata'),
('part', '0051_bomitem_optional'),
('part', '0066_bomitem_allow_variants'),
('part', '0109_auto_20230517_1048'),
('stock', '0034_auto_20200426_0602'),
('stock', '0052_stockitem_is_building'),
('stock', '0054_remove_stockitem_build_order'),
('stock', '0058_stockitem_packaging'),
('users', '0005_owner_model'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Stock Item to allocate to build', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.stockitem'),
),
migrations.AddField(
model_name='build',
name='reference',
field=models.CharField(blank=True, help_text='Build Order Reference', max_length=64, verbose_name='Reference'),
),
migrations.AlterField(
model_name='build',
name='reference',
field=models.CharField(help_text='Build Order Reference', max_length=64, unique=True, verbose_name='Reference'),
),
migrations.AlterModelOptions(
name='build',
options={'verbose_name': 'Build Order', 'verbose_name_plural': 'Build Orders'},
),
migrations.AlterField(
model_name='build',
name='reference',
field=models.CharField(default=build.validators.generate_next_build_reference, help_text='Build Order Reference', max_length=64, unique=True, validators=[build.validators.validate_build_order_reference], verbose_name='Reference'),
),
migrations.AlterField(
model_name='build',
name='title',
field=models.CharField(help_text='Brief description of the build', max_length=100, verbose_name='Description'),
),
migrations.AddField(
model_name='build',
name='destination',
field=models.ForeignKey(blank=True, help_text='Select location where the completed items will be stored', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_builds', to='stock.stocklocation', verbose_name='Destination Location'),
),
migrations.AlterField(
model_name='build',
name='status',
field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Production'), (25, 'On Hold'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'),
),
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.part', verbose_name='Part'),
),
migrations.AddField(
model_name='build',
name='completed',
field=models.PositiveIntegerField(default=0, help_text='Number of stock items which have been completed', verbose_name='Completed items'),
),
migrations.AlterField(
model_name='build',
name='quantity',
field=models.PositiveIntegerField(default=1, help_text='Number of stock items to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'),
),
migrations.AlterField(
model_name='build',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, help_text='Build Order to which this build is allocated', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='build.build', verbose_name='Parent Build'),
),
migrations.AddField(
model_name='build',
name='target_date',
field=models.DateField(blank=True, help_text='Target date for build completion. Build will be overdue after this date.', null=True, verbose_name='Target completion date'),
),
migrations.AlterField(
model_name='build',
name='completed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_completed', to=settings.AUTH_USER_MODEL, verbose_name='completed by'),
),
migrations.AlterField(
model_name='build',
name='completion_date',
field=models.DateField(blank=True, null=True, verbose_name='Completion Date'),
),
migrations.AlterField(
model_name='build',
name='creation_date',
field=models.DateField(auto_now_add=True, verbose_name='Creation Date'),
),
migrations.AddField(
model_name='build',
name='issued_by',
field=models.ForeignKey(blank=True, help_text='User who issued this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_issued', to=settings.AUTH_USER_MODEL, verbose_name='Issued by'),
),
migrations.AddField(
model_name='build',
name='responsible',
field=models.ForeignKey(blank=True, help_text='User responsible for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_responsible', to='users.owner', verbose_name='Responsible'),
),
migrations.AddField(
model_name='builditem',
name='install_into',
field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.stockitem', verbose_name='Install into'),
),
migrations.AlterField(
model_name='builditem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, help_text='Stock quantity to allocate to build', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
),
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.stockitem', verbose_name='Stock Item'),
),
migrations.CreateModel(
name='BuildOrderAttachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attachment', models.FileField(help_text='Select file to attach', upload_to='attachments', verbose_name='Attachment')),
('comment', models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment')),
('upload_date', models.DateField(auto_now_add=True, null=True, verbose_name='upload date')),
('build', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='build.build')),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='build',
name='reference_int',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='buildorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=2000, null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to='attachments', verbose_name='Attachment'),
),
migrations.AlterField(
model_name='build',
name='reference_int',
field=models.BigIntegerField(default=0),
),
migrations.AlterField(
model_name='build',
name='notes',
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Extra build notes', max_length=50000, null=True, verbose_name='Notes'),
),
migrations.RunPython(code=update_build_reference, reverse_code=migrations.RunPython.noop),
migrations.AddField(
model_name='build',
name='priority',
field=models.PositiveIntegerField(default=0, help_text='Priority of this build order', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Priority'),
),
migrations.AlterField(
model_name='build',
name='responsible',
field=models.ForeignKey(blank=True, help_text='User or group responsible for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_responsible', to='users.owner', verbose_name='Responsible'),
),
migrations.AddField(
model_name='build',
name='metadata',
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
),
migrations.AddField(
model_name='builditem',
name='metadata',
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
),
migrations.AddField(
model_name='build',
name='barcode_data',
field=models.CharField(blank=True, help_text='Third party barcode data', max_length=500, verbose_name='Barcode Data'),
),
migrations.AddField(
model_name='build',
name='barcode_hash',
field=models.CharField(blank=True, help_text='Unique hash of barcode data', max_length=128, verbose_name='Barcode Hash'),
),
migrations.AlterField(
model_name='build',
name='title',
field=models.CharField(blank=True, help_text='Brief description of the build (optional)', max_length=100, verbose_name='Description'),
),
migrations.AlterField(
model_name='build',
name='notes',
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Markdown notes (optional)', max_length=50000, null=True, verbose_name='Notes'),
),
migrations.CreateModel(
name='BuildLine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=5, default=1, help_text='Required quantity for build order', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity')),
('bom_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='build_lines', to='part.bomitem')),
('build', models.ForeignKey(help_text='Build object', on_delete=django.db.models.deletion.CASCADE, related_name='build_lines', to='build.build')),
],
options={
'unique_together': {('build', 'bom_item')},
'verbose_name': 'Build Order Line Item',
},
),
migrations.AddField(
model_name='builditem',
name='build_line',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='build.buildline'),
),
migrations.AlterUniqueTogether(
name='builditem',
unique_together={('build_line', 'stock_item', 'install_into')},
),
migrations.RemoveField(
model_name='builditem',
name='build',
),
migrations.AddField(
model_name='build',
name='project_code',
field=models.ForeignKey(blank=True, help_text='Project code for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.projectcode', verbose_name='Project Code'),
),
migrations.DeleteModel(
name='BuildOrderAttachment',
),
migrations.AddField(
model_name='build',
name='status_custom_key',
field=generic.states.fields.ExtraInvenTreeCustomStatusModelField(blank=True, default=None, help_text='Additional status information for this item', null=True, validators=[generic.states.validators.CustomStatusCodeValidator(status_class=build.status_codes.BuildStatus)], verbose_name='Custom status key'),
),
migrations.AlterField(
model_name='build',
name='status',
field=generic.states.fields.InvenTreeCustomStatusModelField(choices=[(10, 'Pending'), (20, 'Production'), (25, 'On Hold'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0), generic.states.validators.CustomStatusCodeValidator(status_class=build.status_codes.BuildStatus)], verbose_name='Build Status'),
),
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'assembly': True}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.part', verbose_name='Part'),
),
migrations.AddField(
model_name='build',
name='start_date',
field=models.DateField(blank=True, help_text='Scheduled start date for this build order', null=True, verbose_name='Build start date'),
),
migrations.AlterField(
model_name='build',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=2000, verbose_name='External Link'),
),
migrations.AddField(
model_name='build',
name='external',
field=models.BooleanField(default=False, help_text='This build order is fulfilled externally', verbose_name='External Build'),
),
migrations.AddField(
model_name='buildline',
name='consumed',
field=models.DecimalField(decimal_places=5, default=0, help_text='Quantity of consumed stock', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Consumed'),
),
]
@@ -1,60 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-19 11:25
from django.db import migrations, models
def add_default_reference(apps, schema_editor):
"""
Add a "default" build-order reference for any existing build orders.
Best we can do is use the PK of the build order itself.
"""
Build = apps.get_model('build', 'build')
count = 0
for build in Build.objects.all():
build.reference = str(build.pk)
build.save()
count += 1
if count > 0:
print(f"\nUpdated build reference for {count} existing BuildOrder objects")
class Migration(migrations.Migration):
atomic = False
dependencies = [
('build', '0017_auto_20200426_0612'),
]
operations = [
# Initial operation - create a 'reference' field for the Build object:
migrations.AddField(
model_name='build',
name='reference',
field=models.CharField(help_text='Build Order Reference', blank=True, max_length=64, unique=False, verbose_name='Reference'),
),
# Auto-populate the new reference field for any existing build order objects
migrations.RunPython(
add_default_reference,
reverse_code=migrations.RunPython.noop
),
# Now that each build has a non-empty, unique reference, update the field requirements!
migrations.AlterField(
model_name='build',
name='reference',
field=models.CharField(
help_text='Build Order Reference',
max_length=64,
blank=False,
unique=True,
verbose_name='Reference'
)
)
]
@@ -1,23 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-19 13:02
import build.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0018_build_reference'),
]
operations = [
migrations.AlterModelOptions(
name='build',
options={'verbose_name': 'Build Order', 'verbose_name_plural': 'Build Orders'},
),
migrations.AlterField(
model_name='build',
name='reference',
field=models.CharField(help_text='Build Order Reference', max_length=64, unique=True, validators=[build.validators.validate_build_order_reference], verbose_name='Reference'),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-19 13:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0019_auto_20201019_1302'),
]
operations = [
migrations.AlterField(
model_name='build',
name='title',
field=models.CharField(help_text='Brief description of the build', max_length=100, verbose_name='Description'),
),
]
@@ -1,66 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-25 21:33
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
from build.status_codes import BuildStatus
class Migration(migrations.Migration):
replaces = [('build', '0021_auto_20201020_0908'), ('build', '0022_auto_20201020_0953'), ('build', '0023_auto_20201020_1009'), ('build', '0024_auto_20201020_1144'), ('build', '0025_auto_20201020_1248'), ('build', '0026_auto_20201023_1228')]
dependencies = [
('stock', '0052_stockitem_is_building'),
('build', '0020_auto_20201019_1325'),
('part', '0051_bomitem_optional'),
]
operations = [
migrations.AddField(
model_name='builditem',
name='install_into',
field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.StockItem'),
),
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'),
),
migrations.AddField(
model_name='build',
name='destination',
field=models.ForeignKey(blank=True, help_text='Select location where the completed items will be stored', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_builds', to='stock.StockLocation', verbose_name='Destination Location'),
),
migrations.AlterField(
model_name='build',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, help_text='Build Order to which this build is allocated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'),
),
migrations.AlterField(
model_name='build',
name='status',
field=models.PositiveIntegerField(choices=BuildStatus.items(), default=BuildStatus.PENDING.value, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'),
),
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part', verbose_name='Part'),
),
migrations.AddField(
model_name='build',
name='completed',
field=models.PositiveIntegerField(default=0, help_text='Number of stock items which have been completed', verbose_name='Completed items'),
),
migrations.AlterField(
model_name='build',
name='quantity',
field=models.PositiveIntegerField(default=1, help_text='Number of stock items to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'),
),
migrations.AlterUniqueTogether(
name='builditem',
unique_together={('build', 'stock_item', 'install_into')},
),
]
@@ -1,31 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-26 04:17
import InvenTree.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('build', '0021_auto_20201020_0908_squashed_0026_auto_20201023_1228'),
]
operations = [
migrations.CreateModel(
name='BuildOrderAttachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attachment', models.FileField(help_text='Select file to attach', upload_to='attachments')),
('comment', models.CharField(blank=True, help_text='File comment', max_length=100)),
('upload_date', models.DateField(auto_now_add=True, null=True)),
('build', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='build.Build')),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]
@@ -1,20 +0,0 @@
# Generated by Django 3.0.7 on 2020-11-10 09:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0054_remove_stockitem_build_order'),
('build', '0022_buildorderattachment'),
]
operations = [
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'),
),
]
@@ -1,20 +0,0 @@
# Generated by Django 3.0.7 on 2020-11-30 23:23
from django.db import migrations
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('build', '0023_auto_20201110_0911'),
]
operations = [
migrations.AlterField(
model_name='build',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, help_text='Build Order to which this build is allocated', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='build.Build', verbose_name='Parent Build'),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.0.7 on 2020-12-15 12:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0024_auto_20201201_1023'),
]
operations = [
migrations.AddField(
model_name='build',
name='target_date',
field=models.DateField(blank=True, help_text='Target date for build completion. Build will be overdue after this date.', null=True, verbose_name='Target completion date'),
),
]
@@ -1,27 +0,0 @@
# Generated by Django 3.0.7 on 2021-02-16 04:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0005_owner_model'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('build', '0025_build_target_date'),
]
operations = [
migrations.AddField(
model_name='build',
name='issued_by',
field=models.ForeignKey(blank=True, help_text='User who issued this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_issued', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='build',
name='responsible',
field=models.ForeignKey(blank=True, help_text='User responsible for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_responsible', to='users.Owner'),
),
]
@@ -1,85 +0,0 @@
# Generated by Django 3.0.7 on 2021-04-04 20:16
import InvenTree.models
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0058_stockitem_packaging'),
('users', '0005_owner_model'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('build', '0026_auto_20210216_1539'),
]
operations = [
migrations.AlterField(
model_name='build',
name='completed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_completed', to=settings.AUTH_USER_MODEL, verbose_name='completed by'),
),
migrations.AlterField(
model_name='build',
name='completion_date',
field=models.DateField(blank=True, null=True, verbose_name='Completion Date'),
),
migrations.AlterField(
model_name='build',
name='creation_date',
field=models.DateField(auto_now_add=True, verbose_name='Creation Date'),
),
migrations.AlterField(
model_name='build',
name='issued_by',
field=models.ForeignKey(blank=True, help_text='User who issued this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_issued', to=settings.AUTH_USER_MODEL, verbose_name='Issued by'),
),
migrations.AlterField(
model_name='build',
name='responsible',
field=models.ForeignKey(blank=True, help_text='User responsible for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_responsible', to='users.Owner', verbose_name='Responsible'),
),
migrations.AlterField(
model_name='builditem',
name='build',
field=models.ForeignKey(help_text='Build to allocate parts', on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build', verbose_name='Build'),
),
migrations.AlterField(
model_name='builditem',
name='install_into',
field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.StockItem', verbose_name='Install into'),
),
migrations.AlterField(
model_name='builditem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, help_text='Stock quantity to allocate to build', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
),
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem', verbose_name='Stock Item'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='attachment',
field=models.FileField(help_text='Select file to attach', upload_to='attachments', verbose_name='Attachment'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='comment',
field=models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='upload_date',
field=models.DateField(auto_now_add=True, null=True, verbose_name='upload date'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='user',
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
]
@@ -1,20 +0,0 @@
# Generated by Django 3.2 on 2021-06-01 05:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0066_bomitem_allow_variants'),
('build', '0027_auto_20210404_2016'),
]
operations = [
migrations.AddField(
model_name='builditem',
name='bom_item',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='allocate_build_items', to='part.bomitem'),
),
]
@@ -1,60 +0,0 @@
# Generated by Django 3.2 on 2021-06-01 05:25
import logging
from django.db import migrations
logger = logging.getLogger('inventree')
def assign_bom_items(apps, schema_editor):
"""
Run through existing BuildItem objects,
and assign a matching BomItem
"""
BuildItem = apps.get_model('build', 'builditem')
BomItem = apps.get_model('part', 'bomitem')
Part = apps.get_model('part', 'part')
count_valid = 0
count_total = 0
for build_item in BuildItem.objects.all(): # pragma: no cover
# Try to find a BomItem which matches the BuildItem
# Note: Before this migration, variant stock assignment was not allowed,
# so BomItem lookup should be pretty easy
if count_total == 0:
# First time around
logger.info("Assigning BomItems to existing BuildItem objects")
count_total += 1
try:
bom_item = BomItem.objects.get(
part__id=build_item.build.part.pk,
sub_part__id=build_item.stock_item.part.pk,
)
build_item.bom_item = bom_item
build_item.save()
count_valid += 1
except BomItem.DoesNotExist:
pass
if count_total > 0: # pragma: no cover
logger.info(f"Assigned BomItem for {count_valid}/{count_total} entries")
class Migration(migrations.Migration):
dependencies = [
('build', '0028_builditem_bom_item'),
]
operations = []
@@ -1,20 +0,0 @@
# Generated by Django 3.2.4 on 2021-07-08 14:14
import build.validators
import build.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0029_auto_20210601_1525'),
]
operations = [
migrations.AlterField(
model_name='build',
name='reference',
field=models.CharField(default=build.validators.generate_next_build_reference, help_text='Build Order Reference', max_length=64, unique=True, validators=[build.validators.validate_build_order_reference], verbose_name='Reference'),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.2.5 on 2021-10-14 06:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0030_alter_build_reference'),
]
operations = [
migrations.AddField(
model_name='build',
name='reference_int',
field=models.IntegerField(default=0),
),
]
@@ -1,48 +0,0 @@
# Generated by Django 3.2.5 on 2021-10-14 06:32
import re
from django.db import migrations
def build_refs(apps, schema_editor):
"""
Rebuild the integer "reference fields" for existing Build objects
"""
BuildOrder = apps.get_model('build', 'build')
for build in BuildOrder.objects.all():
ref = 0
result = re.match(r"^(\d+)", build.reference)
if result and len(result.groups()) == 1:
try:
ref = int(result.groups()[0])
except Exception: # pragma: no cover
ref = 0
# Clip integer value to ensure it does not overflow database field
if ref > 0x7fffffff:
ref = 0x7fffffff
build.reference_int = ref
build.save()
class Migration(migrations.Migration):
atomic = False
dependencies = [
('build', '0031_build_reference_int'),
]
operations = [
migrations.RunPython(
build_refs,
reverse_code=migrations.RunPython.noop
)
]
@@ -1,25 +0,0 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0032_auto_20211014_0632'),
]
operations = [
migrations.AddField(
model_name='buildorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to='attachments', verbose_name='Attachment'),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.2.5 on 2021-12-01 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0033_auto_20211128_0151'),
]
operations = [
migrations.AlterField(
model_name='build',
name='reference_int',
field=models.BigIntegerField(default=0),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 3.2.13 on 2022-06-20 07:28
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('build', '0034_alter_build_reference_int'),
]
operations = [
migrations.AlterField(
model_name='build',
name='notes',
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Extra build notes', max_length=50000, null=True, verbose_name='Notes'),
),
]
@@ -1,64 +0,0 @@
# Generated by Django 3.2.14 on 2022-07-07 11:01
from django.db import migrations
def update_build_reference(apps, schema_editor):
"""Update the build order reference.
Ref: https://github.com/inventree/InvenTree/pull/3267
Performs the following steps:
- Extract existing 'prefix' value
- Generate a build order pattern based on the prefix value
- Update any existing build order references with the specified prefix
"""
InvenTreeSetting = apps.get_model('common', 'inventreesetting')
try:
prefix = InvenTreeSetting.objects.get(key='BUILDORDER_REFERENCE_PREFIX').value
except Exception:
prefix = 'BO-'
# Construct a reference pattern
pattern = prefix + '{ref:04d}'
# Create or update the BuildOrder.reference pattern
try:
setting = InvenTreeSetting.objects.get(key='BUILDORDER_REFERENCE_PATTERN')
setting.value = pattern
setting.save()
except InvenTreeSetting.DoesNotExist:
setting = InvenTreeSetting.objects.create(
key='BUILDORDER_REFERENCE_PATTERN',
value=pattern,
)
# Update any existing build order references with the prefix
Build = apps.get_model('build', 'build')
n = 0
for build in Build.objects.all():
if not build.reference.startswith(prefix):
build.reference = prefix + build.reference
build.save()
n += 1
if n > 0:
print(f"Updated reference field for {n} BuildOrder objects")
class Migration(migrations.Migration):
dependencies = [
('build', '0035_alter_build_notes'),
]
operations = [
migrations.RunPython(
update_build_reference,
reverse_code=migrations.RunPython.noop,
)
]
@@ -1,19 +0,0 @@
# Generated by Django 3.2.16 on 2023-01-17 20:37
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0036_auto_20220707_1101'),
]
operations = [
migrations.AddField(
model_name='build',
name='priority',
field=models.PositiveIntegerField(default=0, help_text='Priority of this build order', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Priority'),
),
]
@@ -1,20 +0,0 @@
# Generated by Django 3.2.16 on 2023-02-09 23:52
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0005_owner_model'),
('build', '0037_build_priority'),
]
operations = [
migrations.AlterField(
model_name='build',
name='responsible',
field=models.ForeignKey(blank=True, help_text='User or group responsible for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_responsible', to='users.owner', verbose_name='Responsible'),
),
]
@@ -1,23 +0,0 @@
# Generated by Django 3.2.18 on 2023-03-17 08:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0038_alter_build_responsible'),
]
operations = [
migrations.AddField(
model_name='build',
name='metadata',
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
),
migrations.AddField(
model_name='builditem',
name='metadata',
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
),
]
@@ -1,23 +0,0 @@
# Generated by Django 3.2.18 on 2023-04-04 13:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0039_auto_20230317_0816'),
]
operations = [
migrations.AddField(
model_name='build',
name='barcode_data',
field=models.CharField(blank=True, help_text='Third party barcode data', max_length=500, verbose_name='Barcode Data'),
),
migrations.AddField(
model_name='build',
name='barcode_hash',
field=models.CharField(blank=True, help_text='Unique hash of barcode data', max_length=128, verbose_name='Barcode Hash'),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.2.18 on 2023-04-12 17:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0040_auto_20230404_1310'),
]
operations = [
migrations.AlterField(
model_name='build',
name='title',
field=models.CharField(blank=True, help_text='Brief description of the build (optional)', max_length=100, verbose_name='Description'),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 3.2.18 on 2023-04-19 00:37
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('build', '0041_alter_build_title'),
]
operations = [
migrations.AlterField(
model_name='build',
name='notes',
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Markdown notes (optional)', max_length=50000, null=True, verbose_name='Notes'),
),
]
@@ -1,29 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-19 06:04
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0109_auto_20230517_1048'),
('build', '0042_alter_build_notes'),
]
operations = [
migrations.CreateModel(
name='BuildLine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=5, default=1, help_text='Required quantity for build order', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity')),
('bom_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='build_lines', to='part.bomitem')),
('build', models.ForeignKey(help_text='Build object', on_delete=django.db.models.deletion.CASCADE, related_name='build_lines', to='build.build')),
],
options={
'unique_together': {('build', 'bom_item')},
'verbose_name': 'Build Order Line Item',
},
),
]
@@ -1,97 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-28 14:10
from django.db import migrations
def get_bom_items_for_part(part, Part, BomItem):
""" Return a list of all BOM items for a given part.
Note that we cannot use the ORM here (as we are inside a data migration),
so we *copy* the logic from the Part class.
This is a snapshot of the Part.get_bom_items() method as of 2023-05-29
"""
bom_items = set()
# Get all BOM items which directly reference the part
for bom_item in BomItem.objects.filter(part=part):
bom_items.add(bom_item)
# Get all BOM items which are inherited by the part
parents = Part.objects.filter(
tree_id=part.tree_id,
level__lt=part.level,
lft__lt=part.lft,
rght__gt=part.rght
)
for bom_item in BomItem.objects.filter(part__in=parents, inherited=True):
bom_items.add(bom_item)
return list(bom_items)
def add_lines_to_builds(apps, schema_editor):
"""Create BuildOrderLine objects for existing build orders"""
# Get database models
Build = apps.get_model("build", "Build")
BuildLine = apps.get_model("build", "BuildLine")
Part = apps.get_model("part", "Part")
BomItem = apps.get_model("part", "BomItem")
build_lines = []
builds = Build.objects.all()
if builds.count() > 0:
print(f"Creating BuildOrderLine objects for {builds.count()} existing builds")
for build in builds:
# Create a BuildOrderLine for each BuildItem
bom_items = get_bom_items_for_part(build.part, Part, BomItem)
for item in bom_items:
build_lines.append(
BuildLine(
build=build,
bom_item=item,
quantity=item.quantity * build.quantity,
)
)
if len(build_lines) > 0:
# Construct the new BuildLine objects
BuildLine.objects.bulk_create(build_lines)
print(f"Created {len(build_lines)} BuildOrderLine objects for existing builds")
def remove_build_lines(apps, schema_editor):
"""Remove BuildOrderLine objects from the database"""
# Get database models
BuildLine = apps.get_model("build", "BuildLine")
n = BuildLine.objects.all().count()
BuildLine.objects.all().delete()
if n > 0:
print(f"Removed {n} BuildOrderLine objects")
class Migration(migrations.Migration):
dependencies = [
('build', '0043_buildline'),
]
operations = [
migrations.RunPython(
add_lines_to_builds,
reverse_code=remove_build_lines,
),
]
@@ -1,19 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-06 10:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('build', '0044_auto_20230528_1410'),
]
operations = [
migrations.AddField(
model_name='builditem',
name='build_line',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='allocations', to='build.buildline'),
),
]
@@ -1,95 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-06 10:33
import logging
from django.db import migrations
logger = logging.getLogger('inventree')
def add_build_line_links(apps, schema_editor):
"""Data migration to add links between BuildLine and BuildItem objects.
Associated model types:
Build: A "Build Order"
BomItem: An individual line in the BOM for Build.part
BuildItem: An individual stock allocation against the Build Order
BuildLine: (new model) an individual line in the Build Order
Goals:
- Find all BuildItem objects which are associated with a Build
- Link them against the relevant BuildLine object
- The BuildLine objects should have been created in 0044_auto_20230528_1410.py
"""
BuildItem = apps.get_model("build", "BuildItem")
BuildLine = apps.get_model("build", "BuildLine")
# Find any existing BuildItem objects
build_items = BuildItem.objects.all()
n_missing = 0
for item in build_items:
# Find the relevant BuildLine object
line = BuildLine.objects.filter(
build=item.build,
bom_item=item.bom_item
).first()
if line is None:
logger.warning(f"BuildLine does not exist for BuildItem {item.pk}")
n_missing += 1
if item.build is None or item.bom_item is None:
continue
# Create one!
line = BuildLine.objects.create(
build=item.build,
bom_item=item.bom_item,
quantity=item.bom_item.quantity * item.build.quantity
)
# Link the BuildItem to the BuildLine
# In the next data migration, we remove the 'build' and 'bom_item' fields from BuildItem
item.build_line = line
item.save()
if build_items.count() > 0:
logger.info(f"add_build_line_links: Updated {build_items.count()} BuildItem objects (added {n_missing})")
def reverse_build_links(apps, schema_editor):
"""Reverse data migration from add_build_line_links
Basically, iterate through each BuildItem and update the links based on the BuildLine
"""
BuildItem = apps.get_model("build", "BuildItem")
items = BuildItem.objects.all()
for item in items:
item.build = item.build_line.build
item.bom_item = item.build_line.bom_item
item.save()
if items.count() > 0:
logger.info(f"reverse_build_links: Updated {items.count()} BuildItem objects")
class Migration(migrations.Migration):
dependencies = [
('build', '0045_builditem_build_line'),
]
operations = [
migrations.RunPython(
add_build_line_links,
reverse_code=reverse_build_links,
)
]
@@ -1,26 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-06 10:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0101_stockitemtestresult_metadata'),
('build', '0046_auto_20230606_1033'),
]
operations = [
migrations.AlterUniqueTogether(
name='builditem',
unique_together={('build_line', 'stock_item', 'install_into')},
),
migrations.RemoveField(
model_name='builditem',
name='bom_item',
),
migrations.RemoveField(
model_name='builditem',
name='build',
),
]
@@ -1,20 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-14 09:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('common', '0019_projectcode_metadata'),
('build', '0047_auto_20230606_1058'),
]
operations = [
migrations.AddField(
model_name='build',
name='project_code',
field=models.ForeignKey(blank=True, help_text='Project code for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.projectcode', verbose_name='Project Code'),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 4.2.12 on 2024-05-08 01:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('build', '0048_build_project_code'),
]
operations = [
migrations.AlterField(
model_name='builditem',
name='build_line',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='build.buildline'),
),
]
@@ -1,26 +0,0 @@
# Generated by Django 4.2.12 on 2024-05-08 01:38
from django.db import migrations
def forward(apps, schema_editor):
"""Find and delete any BuildItem instances which have a null BuildLine field."""
BuildItem = apps.get_model('build', 'BuildItem')
items = BuildItem.objects.filter(build_line=None)
if items.count() > 0:
print(f"Deleting {items.count()} BuildItem objects with null BuildLine field")
items.delete()
class Migration(migrations.Migration):
dependencies = [
('build', '0049_alter_builditem_build_line'),
]
operations = [
migrations.RunPython(forward, reverse_code=migrations.RunPython.noop),
]
@@ -1,21 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-09 09:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('build', '0050_auto_20240508_0138'),
('common', '0026_auto_20240608_1238'),
('company', '0069_company_active'),
('order', '0099_alter_salesorder_status'),
('part', '0123_parttesttemplate_choices'),
('stock', '0110_alter_stockitemtestresult_finished_datetime_and_more')
]
operations = [
migrations.DeleteModel(
name='BuildOrderAttachment',
),
]
@@ -1,50 +0,0 @@
# Generated by Django 4.2.14 on 2024-08-07 22:40
import django.core.validators
from django.db import migrations
import generic.states.fields
import generic.states.validators
import InvenTree.status_codes
class Migration(migrations.Migration):
dependencies = [
("build", "0051_delete_buildorderattachment"),
]
operations = [
migrations.AddField(
model_name="build",
name="status_custom_key",
field=generic.states.fields.ExtraInvenTreeCustomStatusModelField(
blank=True,
default=None,
help_text="Additional status information for this item",
null=True,
verbose_name="Custom status key",
validators=[
generic.states.validators.CustomStatusCodeValidator(
status_class=InvenTree.status_codes.BuildStatus
),
]
),
),
migrations.AlterField(
model_name="build",
name="status",
field=generic.states.fields.InvenTreeCustomStatusModelField(
choices=InvenTree.status_codes.BuildStatus.items(),
default=10,
help_text="Build status code",
validators=[
django.core.validators.MinValueValidator(0),
generic.states.validators.CustomStatusCodeValidator(
status_class=InvenTree.status_codes.BuildStatus
),
],
verbose_name="Build Status",
),
),
]
@@ -1,20 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-16 06:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0130_alter_parttesttemplate_part'),
('build', '0052_build_status_custom_key_alter_build_status'),
]
operations = [
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'assembly': True}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.part', verbose_name='Part'),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 4.2.18 on 2025-01-20 02:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0053_alter_build_part'),
]
operations = [
migrations.AddField(
model_name='build',
name='start_date',
field=models.DateField(blank=True, help_text='Scheduled start date for this build order', null=True, verbose_name='Build start date'),
),
]
@@ -1,21 +0,0 @@
# Generated by Django 4.2.19 on 2025-02-21 12:30
import InvenTree.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("build", "0054_build_start_date"),
]
operations = [
migrations.AlterField(
model_name='build',
name='link',
field=models.TextField(
null=True, blank=True
) # Temporary change to force new ALTER COLUMN operation in the next migration
),
]
@@ -1,24 +0,0 @@
# Generated by Django 4.2.19 on 2025-02-21 13:46
import InvenTree.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("build", "0055_auto_20250221_1230"),
]
operations = [
migrations.AlterField(
model_name="build",
name="link",
field=InvenTree.fields.InvenTreeURLField(
blank=True,
help_text="Link to external URL",
max_length=2000,
verbose_name="External Link",
),
),
]
@@ -1,22 +0,0 @@
# Generated by Django 4.2.20 on 2025-03-13 22:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("build", "0056_alter_build_link"),
]
operations = [
migrations.AddField(
model_name="build",
name="external",
field=models.BooleanField(
default=False,
help_text="This build order is fulfilled externally",
verbose_name="External Build",
),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 4.2.15 on 2024-09-26 10:11
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0057_build_external'),
]
operations = [
migrations.AddField(
model_name='buildline',
name='consumed',
field=models.DecimalField(decimal_places=5, default=0, help_text='Quantity of consumed stock', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Consumed'),
),
]
+7 -218
View File
@@ -16,7 +16,13 @@ class TestForwardMigrations(MigratorTestCase):
Part = self.old_state.apps.get_model('part', 'part')
buildable_part = Part.objects.create(
name='Widget', description='Buildable Part', active=True
name='Widget',
description='Buildable Part',
active=True,
level=0,
tree_id=0,
lft=0,
rght=0,
)
Build = self.old_state.apps.get_model('build', 'build')
@@ -41,220 +47,3 @@ class TestForwardMigrations(MigratorTestCase):
part.save()
part.assembly = False
part.save()
class TestReferenceMigration(MigratorTestCase):
"""Test custom migration which adds 'reference' field to Build model."""
migrate_from = ('build', unit_test.getOldestMigrationFile('build'))
migrate_to = ('build', '0018_build_reference')
def prepare(self):
"""Create some builds."""
Part = self.old_state.apps.get_model('part', 'part')
part = Part.objects.create(name='Part', description='A test part')
Build = self.old_state.apps.get_model('build', 'build')
Build.objects.create(part=part, title='My very first build', quantity=10)
Build.objects.create(part=part, title='My very second build', quantity=10)
Build.objects.create(part=part, title='My very third build', quantity=10)
# Ensure that the builds *do not* have a 'reference' field
for build in Build.objects.all():
with self.assertRaises(AttributeError):
print(build.reference)
def test_build_reference(self):
"""Test that the build reference is correctly assigned to the PK of the Build."""
Build = self.new_state.apps.get_model('build', 'build')
self.assertEqual(Build.objects.count(), 3)
# Check that the build reference is properly assigned
for build in Build.objects.all():
self.assertEqual(str(build.reference), str(build.pk))
class TestReferencePatternMigration(MigratorTestCase):
"""Unit test for data migration which converts reference to new format.
Ref: https://github.com/inventree/InvenTree/pull/3267
"""
migrate_from = ('build', '0019_auto_20201019_1302')
migrate_to = ('build', '0037_build_priority')
def prepare(self):
"""Create some initial data prior to migration."""
Setting = self.old_state.apps.get_model('common', 'inventreesetting')
# Create a custom existing prefix so we can confirm the operation is working
Setting.objects.create(key='BUILDORDER_REFERENCE_PREFIX', value='BuildOrder-')
Part = self.old_state.apps.get_model('part', 'part')
assembly = Part.objects.create(
name='Assy 1', description='An assembly', level=0, lft=0, rght=0, tree_id=0
)
Build = self.old_state.apps.get_model('build', 'build')
for idx in range(1, 11):
Build.objects.create(
part=assembly,
title=f'Build {idx}',
quantity=idx,
reference=f'{idx + 100}',
level=0,
lft=0,
rght=0,
tree_id=0,
)
def test_reference_migration(self):
"""Test that the reference fields have been correctly updated."""
Build = self.new_state.apps.get_model('build', 'build')
for build in Build.objects.all():
self.assertTrue(build.reference.startswith('BuildOrder-'))
Setting = self.new_state.apps.get_model('common', 'inventreesetting')
pattern = Setting.objects.get(key='BUILDORDER_REFERENCE_PATTERN')
self.assertEqual(pattern.value, 'BuildOrder-{ref:04d}')
class TestBuildLineCreation(MigratorTestCase):
"""Test that build lines are correctly created for existing builds.
Ref: https://github.com/inventree/InvenTree/pull/4855
This PR added the 'BuildLine' model, which acts as a link between a Build and a BomItem.
- Migration 0044 creates BuildLine objects for existing builds.
- Migration 0046 links any existing BuildItem objects to corresponding BuildLine
"""
migrate_from = ('build', '0041_alter_build_title')
migrate_to = ('build', '0047_auto_20230606_1058')
def prepare(self):
"""Create data to work with."""
# Model references
Part = self.old_state.apps.get_model('part', 'part')
BomItem = self.old_state.apps.get_model('part', 'bomitem')
Build = self.old_state.apps.get_model('build', 'build')
BuildItem = self.old_state.apps.get_model('build', 'builditem')
StockItem = self.old_state.apps.get_model('stock', 'stockitem')
# The "BuildLine" model does not exist yet
with self.assertRaises(LookupError):
self.old_state.apps.get_model('build', 'buildline')
# Create a part
assembly = Part.objects.create(
name='Assembly',
description='An assembly',
assembly=True,
level=0,
lft=0,
rght=0,
tree_id=0,
)
# Create components
for idx in range(1, 11):
part = Part.objects.create(
name=f'Part {idx}',
description=f'Part {idx}',
level=0,
lft=0,
rght=0,
tree_id=0,
)
# Create plentiful stock
StockItem.objects.create(
part=part, quantity=1000, level=0, lft=0, rght=0, tree_id=0
)
# Create a BOM item
BomItem.objects.create(
part=assembly, sub_part=part, quantity=idx, reference=f'REF-{idx}'
)
# Create some builds
for idx in range(1, 4):
build = Build.objects.create(
part=assembly,
title=f'Build {idx}',
quantity=idx * 10,
reference=f'REF-{idx}',
level=0,
lft=0,
rght=0,
tree_id=0,
)
# Allocate stock to the build
for bom_item in BomItem.objects.all():
stock_item = StockItem.objects.get(part=bom_item.sub_part)
BuildItem.objects.create(
build=build,
bom_item=bom_item,
stock_item=stock_item,
quantity=bom_item.quantity,
)
def test_build_line_creation(self):
"""Test that the BuildLine objects have been created correctly."""
Build = self.new_state.apps.get_model('build', 'build')
BomItem = self.new_state.apps.get_model('part', 'bomitem')
BuildLine = self.new_state.apps.get_model('build', 'buildline')
BuildItem = self.new_state.apps.get_model('build', 'builditem')
StockItem = self.new_state.apps.get_model('stock', 'stockitem')
# There should be 3x builds
self.assertEqual(Build.objects.count(), 3)
# 10x BOMItem objects
self.assertEqual(BomItem.objects.count(), 10)
# 10x StockItem objects
self.assertEqual(StockItem.objects.count(), 10)
# And 30x BuildLine items (1 for each BomItem for each Build)
self.assertEqual(BuildLine.objects.count(), 30)
# And 30x BuildItem objects (1 for each BomItem for each Build)
self.assertEqual(BuildItem.objects.count(), 30)
# Check that each BuildItem has been linked to a BuildLine
for item in BuildItem.objects.all():
self.assertIsNotNone(item.build_line)
self.assertEqual(item.stock_item.part, item.build_line.bom_item.sub_part)
item = BuildItem.objects.first()
# Check that the "build" field has been removed
with self.assertRaises(AttributeError):
item.build
# Check that the "bom_item" field has been removed
with self.assertRaises(AttributeError):
item.bom_item
# Check that each BuildLine is correctly configured
for line in BuildLine.objects.all():
# Check that the quantity is correct
self.assertEqual(
line.quantity, line.build.quantity * line.bom_item.quantity
)
# Check that the linked parts are correct
self.assertEqual(line.build.part, line.bom_item.part)
@@ -1,51 +0,0 @@
# Generated by Django 2.2.4 on 2019-09-02 23:02
import django.core.validators
from django.db import migrations, models
class CreateModelOrSkip(migrations.CreateModel):
"""Custom migration operation to create a model if it does not already exist.
- If the model already exists, the migration is skipped
- This class has been added to deal with some errors being thrown in CI tests
- The 'common_currency' table doesn't exist anymore anyway!
- In the future, these migrations will be squashed
"""
def database_forwards(self, app_label, schema_editor, from_state, to_state) -> None:
"""Forwards migration *attempts* to create the model, but will fail gracefully if it already exists"""
try:
super().database_forwards(app_label, schema_editor, from_state, to_state)
except Exception: # pragma: no cover
pass
def state_forwards(self, app_label, state) -> None:
try:
super().state_forwards(app_label, state)
except Exception: # pragma: no cover
pass
class Migration(migrations.Migration):
initial = True
atomic = False
dependencies = [
]
operations = [
CreateModelOrSkip(
name='Currency',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('symbol', models.CharField(help_text='Currency Symbol e.g. $', max_length=10)),
('suffix', models.CharField(help_text='Currency Suffix e.g. AUD', max_length=10, unique=True)),
('description', models.CharField(help_text='Currency Description', max_length=100)),
('value', models.DecimalField(decimal_places=5, help_text='Currency Value', max_digits=10, validators=[django.core.validators.MinValueValidator(1e-05), django.core.validators.MaxValueValidator(100000)])),
('base', models.BooleanField(default=False, help_text='Use this currency as the base currency')),
],
),
]
@@ -0,0 +1,127 @@
# Generated by Django 5.2.17 on 2026-09-09 23:26
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
("common", "0001_initial"),
("common", "0002_auto_20190902_2304"),
("common", "0003_auto_20190902_2310"),
("common", "0004_inventreesetting"),
("common", "0005_auto_20190915_1256"),
("common", "0006_auto_20200203_0951"),
("common", "0007_colortheme"),
]
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Currency",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"symbol",
models.CharField(help_text="Currency Symbol e.g. $", max_length=10),
),
(
"suffix",
models.CharField(
help_text="Currency Suffix e.g. AUD", max_length=10, unique=True
),
),
(
"description",
models.CharField(help_text="Currency Description", max_length=100),
),
(
"value",
models.DecimalField(
decimal_places=5,
default=1.0,
help_text="Currency Value",
max_digits=10,
validators=[
django.core.validators.MinValueValidator(1e-05),
django.core.validators.MaxValueValidator(100000),
],
),
),
(
"base",
models.BooleanField(
default=False,
help_text="Use this currency as the base currency",
),
),
],
options={
"verbose_name_plural": "Currencies",
},
),
migrations.CreateModel(
name="InvenTreeSetting",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"key",
models.CharField(
help_text="Settings key", max_length=50, unique=True
),
),
(
"value",
models.CharField(
blank=True, help_text="Settings value", max_length=200
),
),
(
"description",
models.CharField(
blank=True, help_text="Settings description", max_length=200
),
),
],
options={
"verbose_name": "InvenTree Setting",
"verbose_name_plural": "InvenTree Settings",
},
),
migrations.CreateModel(
name="ColorTheme",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(blank=True, default="", max_length=20)),
("user", models.CharField(max_length=150, unique=True)),
],
),
]
@@ -1,17 +0,0 @@
# Generated by Django 2.2.4 on 2019-09-02 23:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='currency',
options={'verbose_name_plural': 'Currencies'},
),
]
@@ -1,19 +0,0 @@
# Generated by Django 2.2.4 on 2019-09-02 23:10
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0002_auto_20190902_2304'),
]
operations = [
migrations.AlterField(
model_name='currency',
name='value',
field=models.DecimalField(decimal_places=5, default=1.0, help_text='Currency Value', max_digits=10, validators=[django.core.validators.MinValueValidator(1e-05), django.core.validators.MaxValueValidator(100000)]),
),
]
@@ -1,21 +0,0 @@
# Generated by Django 2.2.5 on 2019-09-15 12:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0003_auto_20190902_2310'),
]
operations = [
migrations.CreateModel(
name='InvenTreeSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(help_text='Settings key', max_length=50, unique=True)),
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
],
),
]
@@ -1,23 +0,0 @@
# Generated by Django 2.2.5 on 2019-09-15 12:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0004_inventreesetting'),
]
operations = [
migrations.AddField(
model_name='inventreesetting',
name='description',
field=models.CharField(blank=True, help_text='Settings description', max_length=200),
),
migrations.AlterField(
model_name='inventreesetting',
name='key',
field=models.CharField(help_text='Settings key', max_length=50, unique=True),
),
]
@@ -1,17 +0,0 @@
# Generated by Django 2.2.9 on 2020-02-03 09:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0005_auto_20190915_1256'),
]
operations = [
migrations.AlterModelOptions(
name='inventreesetting',
options={'verbose_name': 'InvenTree Setting', 'verbose_name_plural': 'InvenTree Settings'},
),
]
@@ -1,21 +0,0 @@
# Generated by Django 3.0.7 on 2020-09-09 19:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0006_auto_20200203_0951'),
]
operations = [
migrations.CreateModel(
name='ColorTheme',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, default='', max_length=20)),
('user', models.CharField(max_length=150, unique=True)),
],
),
]
@@ -1,17 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-19 13:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0007_colortheme'),
]
operations = [
migrations.RemoveField(
model_name='inventreesetting',
name='description',
),
]
@@ -0,0 +1,404 @@
# Generated by Django 5.2.17 on 2026-09-08 23:18
import InvenTree.config
import InvenTree.fields
import InvenTree.models
import common.models
import common.validators
import django.db.models.deletion
import taggit.managers
import uuid
from django.conf import settings
from django.db import migrations, models
from moneyed import CURRENCIES
def set_default_currency(apps, schema_editor):
"""Migrate the currency setting from config.yml to db."""
from common.currency import currency_codes
from common.models import InvenTreeSetting
base_currency = InvenTree.config.get_setting(
'INVENTREE_BASE_CURRENCY', 'base_currency', 'USD'
)
if base_currency not in currency_codes():
base_currency = currency_codes()[0] if currency_codes() else 'USD'
InvenTreeSetting.set_setting(
'INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True
)
def set_currencies(apps, schema_editor):
"""Set the default currency codes.
Ref: https://github.com/inventree/InvenTree/pull/7390
Transfers any currency codes configured externally (config file /
environment variable) into the database settings model.
"""
InvenTreeSetting = apps.get_model('common', 'InvenTreeSetting')
key = 'CURRENCY_CODES'
codes = InvenTree.config.get_setting('INVENTREE_CURRENCIES', 'currencies', None)
if codes is None:
return
if isinstance(codes, str):
codes = codes.split(',')
valid_codes = {code.strip().upper() for code in codes if code.strip().upper() in CURRENCIES}
if len(valid_codes) == 0:
return
value = ','.join(valid_codes)
setting = InvenTreeSetting.objects.filter(key=key).first()
if setting:
setting.value = value
setting.save()
else:
InvenTreeSetting(key=key, value=value).save()
class Migration(migrations.Migration):
replaces = [('common', '0008_remove_inventreesetting_description'), ('common', '0009_delete_currency'), ('common', '0010_migrate_currency_setting'), ('common', '0011_auto_20210722_2114'), ('common', '0012_notificationentry'), ('common', '0013_webhookendpoint_webhookmessage'), ('common', '0014_notificationmessage'), ('common', '0015_newsfeedentry'), ('common', '0016_alter_notificationentry_updated'), ('common', '0017_notesimage'), ('common', '0018_projectcode'), ('common', '0019_projectcode_metadata'), ('common', '0020_customunit'), ('common', '0021_auto_20230805_1748'), ('common', '0022_projectcode_responsible'), ('common', '0023_auto_20240602_1332'), ('common', '0024_notesimage_model_id_notesimage_model_type'), ('common', '0025_attachment'), ('common', '0026_auto_20240608_1238'), ('common', '0027_alter_customunit_symbol'), ('common', '0028_colortheme_user_obj'), ('common', '0029_inventreecustomuserstatemodel'), ('common', '0030_barcodescanresult'), ('common', '0031_auto_20241026_0024'), ('common', '0032_selectionlist_selectionlistentry_and_more'), ('common', '0033_delete_colortheme'), ('common', '0034_alter_inventreecustomuserstatemodel_unique_together_and_more'), ('common', '0035_auto_20250221_1513'), ('common', '0036_alter_attachment_link'), ('common', '0037_dataoutput'), ('common', '0038_alter_attachment_model_type'), ('common', '0039_emailthread_emailmessage')]
dependencies = [
('common', '0001_squashed_0007_colortheme'),
('company', '0027_remove_supplierpricebreak_currency'),
('contenttypes', '0002_remove_content_type_name'),
('part', '0057_remove_partsellpricebreak_currency'),
('plugin', '0009_alter_pluginconfig_key'),
('users', '0010_alter_apitoken_key'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='inventreesetting',
name='description',
),
migrations.DeleteModel(
name='Currency',
),
migrations.RunPython(code=set_default_currency, reverse_code=migrations.RunPython.noop),
migrations.CreateModel(
name='WebhookEndpoint',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('endpoint_id', models.CharField(default=uuid.uuid4, editable=False, help_text='Endpoint at which this webhook is received', max_length=255, verbose_name='Endpoint')),
('name', models.CharField(blank=True, help_text='Name for this webhook', max_length=255, null=True, verbose_name='Name')),
('active', models.BooleanField(default=True, help_text='Is this webhook active', verbose_name='Active')),
('token', models.CharField(blank=True, default=uuid.uuid4, help_text='Token for access', max_length=255, null=True, verbose_name='Token')),
('secret', models.CharField(blank=True, help_text='Shared secret for HMAC', max_length=255, null=True, verbose_name='Secret')),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
migrations.CreateModel(
name='WebhookMessage',
fields=[
('message_id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for this message', primary_key=True, serialize=False, verbose_name='Message ID')),
('host', models.CharField(editable=False, help_text='Host from which this message was received', max_length=255, verbose_name='Host')),
('header', models.CharField(blank=True, editable=False, help_text='Header of this message', max_length=255, null=True, verbose_name='Header')),
('body', models.JSONField(blank=True, editable=False, help_text='Body of this message', null=True, verbose_name='Body')),
('worked_on', models.BooleanField(default=False, help_text='Was the work on this message finished?', verbose_name='Worked on')),
('endpoint', models.ForeignKey(blank=True, help_text='Endpoint on which this message was received', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.webhookendpoint', verbose_name='Endpoint')),
],
),
migrations.CreateModel(
name='NotificationMessage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('target_object_id', models.PositiveIntegerField()),
('source_object_id', models.PositiveIntegerField(blank=True, null=True)),
('category', models.CharField(max_length=250)),
('name', models.CharField(max_length=250)),
('message', models.CharField(blank=True, max_length=250, null=True)),
('creation', models.DateTimeField(auto_now_add=True)),
('read', models.BooleanField(default=False)),
('source_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notification_source', to='contenttypes.contenttype')),
('target_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_target', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
migrations.CreateModel(
name='NewsFeedEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('feed_id', models.CharField(max_length=250, unique=True, verbose_name='Id')),
('title', models.CharField(max_length=250, verbose_name='Title')),
('link', models.URLField(max_length=250, verbose_name='Link')),
('published', models.DateTimeField(max_length=250, verbose_name='Published')),
('author', models.CharField(max_length=250, verbose_name='Author')),
('summary', models.CharField(max_length=250, verbose_name='Summary')),
('read', models.BooleanField(default=False, help_text='Was this news item read?', verbose_name='Read')),
],
),
migrations.CreateModel(
name='NotificationEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=250)),
('uid', models.IntegerField()),
('updated', models.DateTimeField(auto_now=True, help_text='Timestamp of last update', null=True, verbose_name='Updated')),
],
options={
'unique_together': {('key', 'uid')},
},
),
migrations.CreateModel(
name='NotesImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(help_text='Image file', upload_to=common.models.rename_notes_image, verbose_name='Image')),
('date', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='CustomUnit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Unit name', max_length=50, unique=True, verbose_name='Name')),
('symbol', models.CharField(blank=True, help_text='Optional unit symbol', max_length=10, unique=True, verbose_name='Symbol')),
('definition', models.CharField(help_text='Unit definition', max_length=50, verbose_name='Definition')),
],
options={
'verbose_name': 'Custom Unit',
},
),
migrations.AlterField(
model_name='inventreesetting',
name='value',
field=models.CharField(blank=True, help_text='Settings value', max_length=2000),
),
migrations.CreateModel(
name='InvenTreeUserSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.CharField(blank=True, help_text='Settings value', max_length=2000)),
('key', models.CharField(help_text='Settings key', max_length=50)),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'InvenTree User Setting',
'verbose_name_plural': 'InvenTree User Settings',
'constraints': [models.UniqueConstraint(fields=('key', 'user'), name='unique key and user')],
},
),
migrations.CreateModel(
name='ProjectCode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(help_text='Unique project code', max_length=50, unique=True, verbose_name='Project Code')),
('description', models.CharField(blank=True, help_text='Project description', max_length=200, verbose_name='Description')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('responsible', models.ForeignKey(blank=True, help_text='User or group responsible for this project', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_codes', to='users.owner', verbose_name='Responsible')),
],
options={
'verbose_name': 'Project Code',
},
),
migrations.RunPython(code=set_currencies, reverse_code=migrations.RunPython.noop),
migrations.AddField(
model_name='notesimage',
name='model_id',
field=models.IntegerField(blank=True, default=None, help_text='Target model ID for this image', null=True),
),
migrations.AddField(
model_name='notesimage',
name='model_type',
field=models.CharField(blank=True, help_text='Target model type for this image', max_length=100, null=True),
),
migrations.CreateModel(
name='Attachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('model_id', models.PositiveIntegerField()),
('attachment', models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=common.models.rename_attachment, verbose_name='Attachment')),
('link', InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=2000, null=True, verbose_name='Link')),
('comment', models.CharField(blank=True, help_text='Attachment comment', max_length=250, verbose_name='Comment')),
('upload_date', models.DateField(auto_now_add=True, help_text='Date the file was uploaded', null=True, verbose_name='Upload date')),
('file_size', models.PositiveIntegerField(default=0, help_text='File size in bytes', verbose_name='File size')),
('model_type', models.CharField(help_text='Target model type for this image', max_length=100, validators=[common.validators.validate_attachment_model_type])),
('upload_user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
],
options={
'verbose_name': 'Attachment',
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.AlterField(
model_name='customunit',
name='symbol',
field=models.CharField(blank=True, help_text='Optional unit symbol', max_length=10, verbose_name='Symbol'),
),
migrations.AddField(
model_name='colortheme',
name='user_obj',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='InvenTreeCustomUserStateModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.IntegerField(help_text='Numerical value that will be saved in the models database', verbose_name='Value')),
('name', models.CharField(help_text='Name of the state', max_length=250, validators=[common.validators.validate_uppercase, common.validators.validate_variable_string], verbose_name='Name')),
('label', models.CharField(help_text='Label that will be displayed in the frontend', max_length=250, verbose_name='Label')),
('color', models.CharField(choices=[('primary', 'primary'), ('secondary', 'secondary'), ('success', 'success'), ('danger', 'danger'), ('warning', 'warning'), ('info', 'info'), ('dark', 'dark')], default='secondary', help_text='Color that will be displayed in the frontend', max_length=10, verbose_name='Color')),
('logical_key', models.IntegerField(help_text='State logical key that is equal to this custom state in business logic', verbose_name='Logical Key')),
('reference_status', models.CharField(help_text='Status set that is extended with this custom state', max_length=250, verbose_name='Reference Status Set')),
('model', models.ForeignKey(blank=True, help_text='Model this state is associated with', null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.contenttype', verbose_name='Model')),
],
options={
'verbose_name': 'Custom State',
'verbose_name_plural': 'Custom States',
'unique_together': {('reference_status', 'key'), ('reference_status', 'name')},
},
),
migrations.CreateModel(
name='BarcodeScanResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.CharField(help_text='Barcode data', max_length=250, verbose_name='Data')),
('timestamp', models.DateTimeField(auto_now_add=True, help_text='Date and time of the barcode scan', verbose_name='Timestamp')),
('endpoint', models.CharField(blank=True, help_text='URL endpoint which processed the barcode', max_length=250, null=True, verbose_name='Path')),
('context', models.JSONField(blank=True, help_text='Context data for the barcode scan', max_length=1000, null=True, verbose_name='Context')),
('response', models.JSONField(blank=True, help_text='Response data from the barcode scan', max_length=1000, null=True, verbose_name='Response')),
('result', models.BooleanField(default=False, help_text='Was the barcode scan successful?', verbose_name='Result')),
('user', models.ForeignKey(blank=True, help_text='User who scanned the barcode', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Barcode Scan',
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.CreateModel(
name='SelectionList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('name', models.CharField(help_text='Name of the selection list', max_length=100, unique=True, verbose_name='Name')),
('description', models.CharField(blank=True, help_text='Description of the selection list', max_length=250, verbose_name='Description')),
('locked', models.BooleanField(default=False, help_text='Is this selection list locked?', verbose_name='Locked')),
('active', models.BooleanField(default=True, help_text='Can this selection list be used?', verbose_name='Active')),
('source_string', models.CharField(blank=True, help_text='Optional string identifying the source used for this list', max_length=1000, verbose_name='Source String')),
('created', models.DateTimeField(auto_now_add=True, help_text='Date and time that the selection list was created', verbose_name='Created')),
('last_updated', models.DateTimeField(auto_now=True, help_text='Date and time that the selection list was last updated', verbose_name='Last Updated')),
],
options={
'verbose_name': 'Selection List',
'verbose_name_plural': 'Selection Lists',
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.CreateModel(
name='SelectionListEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.CharField(help_text='Value of the selection list entry', max_length=255, verbose_name='Value')),
('label', models.CharField(help_text='Label for the selection list entry', max_length=255, verbose_name='Label')),
('description', models.CharField(blank=True, help_text='Description of the selection list entry', max_length=250, verbose_name='Description')),
('active', models.BooleanField(default=True, help_text='Is this selection list entry active?', verbose_name='Active')),
('list', models.ForeignKey(blank=True, help_text='Selection list to which this entry belongs', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='common.selectionlist', verbose_name='Selection List')),
],
options={
'verbose_name': 'Selection List Entry',
'verbose_name_plural': 'Selection List Entries',
'unique_together': {('list', 'value')},
},
),
migrations.AddField(
model_name='selectionlist',
name='default',
field=models.ForeignKey(blank=True, help_text='Default entry for this selection list', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.selectionlistentry', verbose_name='Default Entry'),
),
migrations.AddField(
model_name='selectionlist',
name='source_plugin',
field=models.ForeignKey(blank=True, help_text='Plugin which provides the selection list', null=True, on_delete=django.db.models.deletion.SET_NULL, to='plugin.pluginconfig', verbose_name='Source Plugin'),
),
migrations.DeleteModel(
name='ColorTheme',
),
migrations.AlterField(
model_name='attachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=2000, null=True, verbose_name='Link'),
),
migrations.CreateModel(
name='DataOutput',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('total', models.PositiveIntegerField(default=1)),
('progress', models.PositiveIntegerField(default=0)),
('complete', models.BooleanField(default=False)),
('output_type', models.CharField(blank=True, max_length=100, null=True)),
('template_name', models.CharField(blank=True, max_length=100, null=True)),
('plugin', models.CharField(blank=True, max_length=100, null=True)),
('output', models.FileField(blank=True, null=True, upload_to='data_output')),
('errors', models.JSONField(blank=True, null=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterField(
model_name='attachment',
name='model_type',
field=models.CharField(help_text='Target model type for image', max_length=100, validators=[common.validators.validate_attachment_model_type], verbose_name='Model type'),
),
migrations.CreateModel(
name='EmailThread',
fields=[
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('key', models.CharField(blank=True, help_text='Unique key for this thread (used to identify the thread)', max_length=250, null=True, verbose_name='Key')),
('global_id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for this thread', primary_key=True, serialize=False, verbose_name='Global ID')),
('started_internal', models.BooleanField(default=False, help_text='Was this thread started internally?', verbose_name='Started Internal')),
('created', models.DateTimeField(auto_now_add=True, help_text='Date and time that the thread was created', verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, help_text='Date and time that the thread was last updated', verbose_name='Updated')),
],
options={
'verbose_name': 'Email Thread',
'verbose_name_plural': 'Email Threads',
'ordering': ['-updated'],
'unique_together': {('key', 'global_id')},
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.CreateModel(
name='EmailMessage',
fields=[
('global_id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for this message', primary_key=True, serialize=False, unique=True, verbose_name='Global ID')),
('message_id_key', models.CharField(blank=True, help_text='Identifier for this message (might be supplied by external system)', max_length=250, null=True, verbose_name='Message ID')),
('thread_id_key', models.CharField(blank=True, help_text='Identifier for this message thread (might be supplied by external system)', max_length=250, null=True, verbose_name='Thread ID')),
('subject', models.CharField(max_length=250)),
('body', models.TextField()),
('to', models.EmailField(max_length=254)),
('sender', models.EmailField(max_length=254)),
('status', models.CharField(blank=True, choices=[('A', 'Announced'), ('S', 'Sent'), ('F', 'Failed'), ('D', 'Delivered'), ('R', 'Read'), ('C', 'Confirmed')], max_length=50, null=True)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('headers', models.JSONField(blank=True, null=True)),
('full_message', models.TextField(blank=True, null=True)),
('direction', models.CharField(blank=True, choices=[('I', 'Inbound'), ('O', 'Outbound')], max_length=50, null=True)),
('priority', models.IntegerField(choices=[(0, 'None'), (1, 'Very High'), (2, 'High'), (3, 'Normal'), (4, 'Low'), (5, 'Very Low')], verbose_name='Priority')),
('delivery_options', models.JSONField(blank=True, null=True)),
('error_code', models.CharField(blank=True, max_length=50, null=True)),
('error_message', models.TextField(blank=True, null=True)),
('error_timestamp', models.DateTimeField(blank=True, null=True)),
('thread', models.ForeignKey(blank=True, help_text='Linked thread for this message', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='messages', to='common.emailthread', verbose_name='Thread')),
],
options={
'verbose_name': 'Email Message',
'verbose_name_plural': 'Email Messages',
},
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.0.7 on 2020-11-10 11:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('company', '0027_remove_supplierpricebreak_currency'),
('part', '0057_remove_partsellpricebreak_currency'),
('common', '0008_remove_inventreesetting_description'),
]
operations = [
migrations.DeleteModel(
name='Currency',
),
]
@@ -1,33 +0,0 @@
# Generated by Django 3.2.4 on 2021-07-01 15:39
from django.db import migrations
from common.models import InvenTreeSetting
from InvenTree.config import get_setting
def set_default_currency(apps, schema_editor):
""" migrate the currency setting from config.yml to db """
# get value from settings-file
base_currency = get_setting('INVENTREE_BASE_CURRENCY', 'base_currency', 'USD')
from common.currency import currency_codes
# check if value is valid
if base_currency not in currency_codes():
if len (currency_codes()) > 0:
base_currency = currency_codes()[0]
else:
base_currency = 'USD'
# write to database
InvenTreeSetting.set_setting('INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True)
class Migration(migrations.Migration):
dependencies = [
('common', '0009_delete_currency'),
]
operations = [
migrations.RunPython(set_default_currency, reverse_code=migrations.RunPython.noop),
]
@@ -1,33 +0,0 @@
# Generated by Django 3.2.4 on 2021-07-22 21:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('common', '0010_migrate_currency_setting'),
]
operations = [
migrations.CreateModel(
name='InvenTreeUserSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
('key', models.CharField(help_text='Settings key', max_length=50)),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'InvenTree User Setting',
'verbose_name_plural': 'InvenTree User Settings',
},
),
migrations.AddConstraint(
model_name='inventreeusersetting',
constraint=models.UniqueConstraint(fields=('key', 'user'), name='unique key and user'),
),
]
@@ -1,25 +0,0 @@
# Generated by Django 3.2.5 on 2021-11-03 13:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0011_auto_20210722_2114'),
]
operations = [
migrations.CreateModel(
name='NotificationEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=250)),
('uid', models.IntegerField()),
('updated', models.DateTimeField(auto_now=True)),
],
options={
'unique_together': {('key', 'uid')},
},
),
]
@@ -1,40 +0,0 @@
# Generated by Django 3.2.5 on 2021-11-19 21:34
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('common', '0012_notificationentry'),
]
operations = [
migrations.CreateModel(
name='WebhookEndpoint',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('endpoint_id', models.CharField(default=uuid.uuid4, editable=False, help_text='Endpoint at which this webhook is received', max_length=255, verbose_name='Endpoint')),
('name', models.CharField(blank=True, help_text='Name for this webhook', max_length=255, null=True, verbose_name='Name')),
('active', models.BooleanField(default=True, help_text='Is this webhook active', verbose_name='Active')),
('token', models.CharField(blank=True, default=uuid.uuid4, help_text='Token for access', max_length=255, null=True, verbose_name='Token')),
('secret', models.CharField(blank=True, help_text='Shared secret for HMAC', max_length=255, null=True, verbose_name='Secret')),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
migrations.CreateModel(
name='WebhookMessage',
fields=[
('message_id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for this message', primary_key=True, serialize=False, verbose_name='Message ID')),
('host', models.CharField(editable=False, help_text='Host from which this message was received', max_length=255, verbose_name='Host')),
('header', models.CharField(blank=True, editable=False, help_text='Header of this message', max_length=255, null=True, verbose_name='Header')),
('body', models.JSONField(blank=True, editable=False, help_text='Body of this message', null=True, verbose_name='Body')),
('worked_on', models.BooleanField(default=False, help_text='Was the work on this message finished?', verbose_name='Worked on')),
('endpoint', models.ForeignKey(blank=True, help_text='Endpoint on which this message was received', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.webhookendpoint', verbose_name='Endpoint')),
],
),
]
@@ -1,33 +0,0 @@
# Generated by Django 3.2.5 on 2022-02-13 03:09
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('common', '0013_webhookendpoint_webhookmessage'),
]
operations = [
migrations.CreateModel(
name='NotificationMessage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('target_object_id', models.PositiveIntegerField()),
('source_object_id', models.PositiveIntegerField(blank=True, null=True)),
('category', models.CharField(max_length=250)),
('name', models.CharField(max_length=250)),
('message', models.CharField(blank=True, max_length=250, null=True)),
('creation', models.DateTimeField(auto_now_add=True)),
('read', models.BooleanField(default=False)),
('source_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notification_source', to='contenttypes.contenttype')),
('target_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_target', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
]
@@ -1,26 +0,0 @@
# Generated by Django 3.2.14 on 2022-07-31 19:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0014_notificationmessage'),
]
operations = [
migrations.CreateModel(
name='NewsFeedEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('feed_id', models.CharField(max_length=250, unique=True, verbose_name='Id')),
('title', models.CharField(max_length=250, verbose_name='Title')),
('link', models.URLField(max_length=250, verbose_name='Link')),
('published', models.DateTimeField(max_length=250, verbose_name='Published')),
('author', models.CharField(max_length=250, verbose_name='Author')),
('summary', models.CharField(max_length=250, verbose_name='Summary')),
('read', models.BooleanField(default=False, help_text='Was this news item read?', verbose_name='Read')),
],
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.2.16 on 2023-01-15 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0015_newsfeedentry'),
]
operations = [
migrations.AlterField(
model_name='notificationentry',
name='updated',
field=models.DateTimeField(auto_now=True, help_text='Timestamp of last update', null=True, verbose_name='Updated'),
),
]
@@ -1,27 +0,0 @@
# Generated by Django 3.2.18 on 2023-04-17 05:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import common.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('common', '0016_alter_notificationentry_updated'),
]
operations = [
migrations.CreateModel(
name='NotesImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(help_text='Image file', upload_to=common.models.rename_notes_image, verbose_name='Image')),
('date', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]
@@ -1,24 +0,0 @@
# Generated by Django 3.2.18 on 2023-04-19 02:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0017_notesimage'),
]
operations = [
migrations.CreateModel(
name='ProjectCode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(help_text='Unique project code', max_length=50, unique=True, verbose_name='Project Code')),
('description', models.CharField(blank=True, help_text='Project description', max_length=200, verbose_name='Description')),
],
options={
'verbose_name': 'Project Code',
},
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.2.18 on 2023-04-19 13:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0018_projectcode'),
]
operations = [
migrations.AddField(
model_name='projectcode',
name='metadata',
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
),
]
@@ -1,25 +0,0 @@
# Generated by Django 3.2.20 on 2023-07-18 11:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0019_projectcode_metadata'),
]
operations = [
migrations.CreateModel(
name='CustomUnit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Unit name', max_length=50, unique=True, verbose_name='Name')),
('symbol', models.CharField(blank=True, help_text='Optional unit symbol', max_length=10, unique=True, verbose_name='Symbol')),
('definition', models.CharField(help_text='Unit definition', max_length=50, verbose_name='Definition')),
],
options={
'verbose_name': 'Custom Unit',
},
),
]
@@ -1,23 +0,0 @@
# Generated by Django 3.2.20 on 2023-08-05 17:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0020_customunit'),
]
operations = [
migrations.AlterField(
model_name='inventreesetting',
name='value',
field=models.CharField(blank=True, help_text='Settings value', max_length=2000),
),
migrations.AlterField(
model_name='inventreeusersetting',
name='value',
field=models.CharField(blank=True, help_text='Settings value', max_length=2000),
),
]
@@ -1,20 +0,0 @@
# Generated by Django 3.2.23 on 2023-11-20 08:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0010_alter_apitoken_key'),
('common', '0021_auto_20230805_1748'),
]
operations = [
migrations.AddField(
model_name='projectcode',
name='responsible',
field=models.ForeignKey(blank=True, help_text='User or group responsible for this project', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_codes', to='users.owner', verbose_name='Responsible'),
),
]
@@ -1,78 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-02 13:32
from django.conf import settings
from django.db import migrations
from moneyed import CURRENCIES
import InvenTree.config
def set_currencies(apps, schema_editor):
"""Set the default currency codes.
Ref: https://github.com/inventree/InvenTree/pull/7390
Previously, the allowed currency codes were set in the external configuration
(e.g via the configuration file or environment variables).
Now, they are set in the database (via the InvenTreeSetting model).
So, this data migration exists to transfer any configured currency codes,
from the external configuration, into the database settings model.
"""
InvenTreeSetting = apps.get_model('common', 'InvenTreeSetting')
key = 'CURRENCY_CODES'
codes = InvenTree.config.get_setting('INVENTREE_CURRENCIES', 'currencies', None)
if codes is None:
# No currency codes are defined in the configuration file
return
if type(codes) == str:
codes = codes.split(',')
valid_codes = set()
for code in codes:
code = code.strip().upper()
if code in CURRENCIES:
valid_codes.add(code)
if len(valid_codes) == 0:
print(f"No currency codes found in configuration file - skipping migration")
return
value = ','.join(valid_codes)
if not settings.TESTING: # pragma: no cover
print(f"Found existing currency codes:", value)
setting = InvenTreeSetting.objects.filter(key=key).first()
if setting:
if not settings.TESTING: # pragma: no cover
print(f"- Updating existing setting for currency codes")
setting.value = value
setting.save()
else:
if not settings.TESTING: # pragma: no cover
print(f"- Creating new setting for currency codes")
setting = InvenTreeSetting(key=key, value=value)
setting.save()
class Migration(migrations.Migration):
dependencies = [
('common', '0022_projectcode_responsible'),
]
operations = [
migrations.RunPython(set_currencies, reverse_code=migrations.RunPython.noop)
]
@@ -1,25 +0,0 @@
# Generated by Django 4.2.12 on 2024-05-22 12:27
import common.validators
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0023_auto_20240602_1332'),
]
operations = [
migrations.AddField(
model_name='notesimage',
name='model_id',
field=models.IntegerField(blank=True, default=None, help_text='Target model ID for this image', null=True),
),
migrations.AddField(
model_name='notesimage',
name='model_type',
field=models.CharField(blank=True, null=True, help_text='Target model type for this image', max_length=100),
),
]
@@ -1,43 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-08 12:37
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import common.models
import common.validators
import InvenTree.fields
import InvenTree.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('common', '0024_notesimage_model_id_notesimage_model_type'),
]
operations = [
migrations.CreateModel(
name='Attachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('model_id', models.PositiveIntegerField()),
('attachment', models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=common.models.rename_attachment, verbose_name='Attachment')),
('link', InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link')),
('comment', models.CharField(blank=True, help_text='Attachment comment', max_length=250, verbose_name='Comment')),
('upload_date', models.DateField(auto_now_add=True, help_text='Date the file was uploaded', null=True, verbose_name='Upload date')),
('file_size', models.PositiveIntegerField(default=0, help_text='File size in bytes', verbose_name='File size')),
('model_type', models.CharField(help_text='Target model type for this image', max_length=100, validators=[common.validators.validate_attachment_model_type])),
('upload_user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'))
],
bases=(InvenTree.models.PluginValidationMixin, models.Model),
options={
'verbose_name': 'Attachment',
}
),
]
@@ -1,122 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-08 12:38
from django.db import migrations
from django.core.files.storage import default_storage
def get_legacy_models():
"""Return a set of legacy attachment models."""
# Legacy attachment types to convert:
# app_label, table name, target model, model ref
return [
('build', 'BuildOrderAttachment', 'build', 'build'),
('company', 'CompanyAttachment', 'company', 'company'),
('company', 'ManufacturerPartAttachment', 'manufacturerpart', 'manufacturer_part'),
('order', 'PurchaseOrderAttachment', 'purchaseorder', 'order'),
('order', 'SalesOrderAttachment', 'salesorder', 'order'),
('order', 'ReturnOrderAttachment', 'returnorder', 'order'),
('part', 'PartAttachment', 'part', 'part'),
('stock', 'StockItemAttachment', 'stockitem', 'stock_item')
]
def update_attachments(apps, schema_editor):
"""Migrate any existing attachment models to the new attachment table."""
Attachment = apps.get_model('common', 'attachment')
N = 0
for app, model, target_model, model_ref in get_legacy_models():
LegacyAttachmentModel = apps.get_model(app, model)
if LegacyAttachmentModel.objects.count() == 0:
continue
to_create = []
for attachment in LegacyAttachmentModel.objects.all():
# Find the size of the file (if exists)
if attachment.attachment and default_storage.exists(attachment.attachment.name):
try:
file_size = default_storage.size(attachment.attachment.name)
except NotImplementedError:
file_size = 0
else:
file_size = 0
to_create.append(
Attachment(
model_type=target_model,
model_id=getattr(attachment, model_ref).pk,
attachment=attachment.attachment,
link=attachment.link,
comment=attachment.comment,
upload_date=attachment.upload_date,
upload_user=attachment.user,
file_size=file_size
)
)
if len(to_create) > 0:
print(f"Migrating {len(to_create)} attachments for the legacy '{model}' model.")
Attachment.objects.bulk_create(to_create)
N += len(to_create)
# Check the correct number of Attachment objects has been created
assert(N == Attachment.objects.count())
def reverse_attachments(apps, schema_editor):
"""Reverse data migration, and map new Attachment model back to legacy models."""
Attachment = apps.get_model('common', 'attachment')
N = 0
for app, model, target_model, model_ref in get_legacy_models():
LegacyAttachmentModel = apps.get_model(app, model)
to_create = []
for attachment in Attachment.objects.filter(model_type=target_model):
TargetModel = apps.get_model(app, target_model)
data = {
'attachment': attachment.attachment,
'link': attachment.link,
'comment': attachment.comment,
'upload_date': attachment.upload_date,
'user': attachment.upload_user,
model_ref: TargetModel.objects.get(pk=attachment.model_id)
}
to_create.append(LegacyAttachmentModel(**data))
if len(to_create) > 0:
print(f"Reversing {len(to_create)} attachments for the legacy '{model}' model.")
LegacyAttachmentModel.objects.bulk_create(to_create)
N += len(to_create)
# Check the correct number of LegacyAttachmentModel objects has been created
assert(N == Attachment.objects.count())
class Migration(migrations.Migration):
dependencies = [
('build', '0050_auto_20240508_0138'),
('common', '0025_attachment'),
('company', '0069_company_active'),
('order', '0099_alter_salesorder_status'),
('part', '0123_parttesttemplate_choices'),
('stock', '0110_alter_stockitemtestresult_finished_datetime_and_more')
]
operations = [
migrations.RunPython(update_attachments, reverse_code=reverse_attachments),
]
@@ -1,18 +0,0 @@
# Generated by Django 4.2.12 on 2024-07-04 10:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0026_auto_20240608_1238'),
]
operations = [
migrations.AlterField(
model_name='customunit',
name='symbol',
field=models.CharField(blank=True, help_text='Optional unit symbol', max_length=10, verbose_name='Symbol'),
),
]
@@ -1,39 +0,0 @@
# Generated by Django 4.2.12 on 2024-07-04 10:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def migrate_userthemes(apps, schema_editor):
"""Mgrate text-based user references to ForeignKey references."""
ColorTheme = apps.get_model("common", "ColorTheme")
User = apps.get_model(settings.AUTH_USER_MODEL)
for theme in ColorTheme.objects.all():
try:
theme.user_obj = User.objects.get(username=theme.user)
theme.save()
except User.DoesNotExist:
pass
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("common", "0027_alter_customunit_symbol"),
]
operations = [
migrations.AddField(
model_name="colortheme",
name="user_obj",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
migrations.RunPython(migrate_userthemes, migrations.RunPython.noop),
]
@@ -1,97 +0,0 @@
# Generated by Django 4.2.14 on 2024-08-07 22:40
import django.db.models.deletion
from django.db import migrations, models
from common.models import state_color_mappings
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("common", "0028_colortheme_user_obj"),
]
operations = [
migrations.CreateModel(
name="InvenTreeCustomUserStateModel",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"key",
models.IntegerField(
help_text="Value that will be saved in the models database",
verbose_name="Key",
),
),
(
"name",
models.CharField(
help_text="Name of the state",
max_length=250,
verbose_name="Name",
),
),
(
"label",
models.CharField(
help_text="Label that will be displayed in the frontend",
max_length=250,
verbose_name="Label",
),
),
(
"color",
models.CharField(
choices=state_color_mappings(),
default="secondary",
help_text="Color that will be displayed in the frontend",
max_length=10,
verbose_name="Color",
),
),
(
"logical_key",
models.IntegerField(
help_text="State logical key that is equal to this custom state in business logic",
verbose_name="Logical Key",
),
),
(
"reference_status",
models.CharField(
help_text="Status set that is extended with this custom state",
max_length=250,
verbose_name="Reference Status Set",
),
),
(
"model",
models.ForeignKey(
blank=True,
help_text="Model this state is associated with",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="contenttypes.contenttype",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Custom State",
"verbose_name_plural": "Custom States",
"unique_together": {
("model", "reference_status", "key", "logical_key")
},
},
),
]
@@ -1,34 +0,0 @@
# Generated by Django 4.2.15 on 2024-09-21 06:05
import InvenTree.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('common', '0029_inventreecustomuserstatemodel'),
]
operations = [
migrations.CreateModel(
name='BarcodeScanResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.CharField(help_text='Barcode data', max_length=250, verbose_name='Data')),
('timestamp', models.DateTimeField(auto_now_add=True, help_text='Date and time of the barcode scan', verbose_name='Timestamp')),
('endpoint', models.CharField(blank=True, help_text='URL endpoint which processed the barcode', max_length=250, null=True, verbose_name='Path')),
('context', models.JSONField(blank=True, help_text='Context data for the barcode scan', max_length=1000, null=True, verbose_name='Context')),
('response', models.JSONField(blank=True, help_text='Response data from the barcode scan', max_length=1000, null=True, verbose_name='Response')),
('result', models.BooleanField(default=False, help_text='Was the barcode scan successful?', verbose_name='Result')),
('user', models.ForeignKey(blank=True, help_text='User who scanned the barcode', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Barcode Scan',
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
]
@@ -1,39 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-26 00:24
from django.conf import settings
from django.db import migrations
import logging
logger = logging.getLogger('inventree')
def update_news_feed_urls(apps, schema_editor):
"""Update and validate the news feed URLs."""
from common.models import NewsFeedEntry
n = 0
for entry in NewsFeedEntry.objects.all():
if entry.link and entry.link.startswith('/'):
entry.link = settings.INVENTREE_BASE_URL + entry.link
entry.save()
n += 1
if n > 0:
logger.info("Updated link for %s NewsFeedEntry objects", n)
class Migration(migrations.Migration):
dependencies = [
('common', '0030_barcodescanresult'),
]
operations = [
migrations.RunPython(
update_news_feed_urls,
reverse_code=migrations.RunPython.noop
)
]
@@ -1,191 +0,0 @@
# Generated by Django 4.2.16 on 2024-11-24 12:41
import django.db.models.deletion
from django.db import migrations, models
import InvenTree.models
class Migration(migrations.Migration):
dependencies = [
('plugin', '0009_alter_pluginconfig_key'),
('common', '0031_auto_20241026_0024'),
]
operations = [
migrations.CreateModel(
name='SelectionList',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'metadata',
models.JSONField(
blank=True,
help_text='JSON metadata field, for use by external plugins',
null=True,
verbose_name='Plugin Metadata',
),
),
(
'name',
models.CharField(
help_text='Name of the selection list',
max_length=100,
unique=True,
verbose_name='Name',
),
),
(
'description',
models.CharField(
blank=True,
help_text='Description of the selection list',
max_length=250,
verbose_name='Description',
),
),
(
'locked',
models.BooleanField(
default=False,
help_text='Is this selection list locked?',
verbose_name='Locked',
),
),
(
'active',
models.BooleanField(
default=True,
help_text='Can this selection list be used?',
verbose_name='Active',
),
),
(
'source_string',
models.CharField(
blank=True,
help_text='Optional string identifying the source used for this list',
max_length=1000,
verbose_name='Source String',
),
),
(
'created',
models.DateTimeField(
auto_now_add=True,
help_text='Date and time that the selection list was created',
verbose_name='Created',
),
),
(
'last_updated',
models.DateTimeField(
auto_now=True,
help_text='Date and time that the selection list was last updated',
verbose_name='Last Updated',
),
),
],
options={
'verbose_name': 'Selection List',
'verbose_name_plural': 'Selection Lists',
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.CreateModel(
name='SelectionListEntry',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'value',
models.CharField(
help_text='Value of the selection list entry',
max_length=255,
verbose_name='Value',
),
),
(
'label',
models.CharField(
help_text='Label for the selection list entry',
max_length=255,
verbose_name='Label',
),
),
(
'description',
models.CharField(
blank=True,
help_text='Description of the selection list entry',
max_length=250,
verbose_name='Description',
),
),
(
'active',
models.BooleanField(
default=True,
help_text='Is this selection list entry active?',
verbose_name='Active',
),
),
(
'list',
models.ForeignKey(
blank=True,
help_text='Selection list to which this entry belongs',
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='entries',
to='common.selectionlist',
verbose_name='Selection List',
),
),
],
options={
'verbose_name': 'Selection List Entry',
'verbose_name_plural': 'Selection List Entries',
'unique_together': {('list', 'value')},
},
),
migrations.AddField(
model_name='selectionlist',
name='default',
field=models.ForeignKey(
blank=True,
help_text='Default entry for this selection list',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='common.selectionlistentry',
verbose_name='Default Entry',
),
),
migrations.AddField(
model_name='selectionlist',
name='source_plugin',
field=models.ForeignKey(
blank=True,
help_text='Plugin which provides the selection list',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='plugin.pluginconfig',
verbose_name='Source Plugin',
),
),
]

Some files were not shown because too many files have changed in this diff Show More