Add revision control for bids

master
Pete Ley 5 months ago
parent b422d36885
commit 838e5625f8

@ -1,8 +1,9 @@
# Generated by Django 4.2.6 on 2023-12-06 22:02
# Generated by Django 4.2.6 on 2023-12-11 17:32
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import modelcluster.fields
import wagtail.contrib.routable_page.models
import wagtail.fields
@ -14,9 +15,9 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0089_log_entry_data_json_null_to_object'),
('wagtailimages', '0025_alter_image_file_alter_rendition_file'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtailcore', '0089_log_entry_data_json_null_to_object'),
('wagtaildocs', '0012_uploadeddocument'),
]
@ -30,13 +31,27 @@ class Migration(migrations.Migration):
('city', models.CharField(blank=True, max_length=255, null=True)),
('province', models.CharField(blank=True, choices=[('AL', 'Alabama'), ('AK', 'Alaska'), ('AZ', 'Arizona'), ('AR', 'Arkansas'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DE', 'Delaware'), ('FL', 'Florida'), ('GA', 'Georgia'), ('HI', 'Hawaii'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('IA', 'Iowa'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('MA', 'Massachusetts'), ('MD', 'Maryland'), ('ME', 'Maine'), ('MI', 'Michigan'), ('MN', 'Minnesota'), ('MS', 'Mississippi'), ('MO', 'Missouri'), ('MT', 'Montana'), ('NE', 'Nebraska'), ('NV', 'Nevada'), ('NH', 'New Hampshire'), ('NJ', 'New Jersey'), ('NM', 'New Mexico'), ('NY', 'New York'), ('NC', 'North Carolina'), ('ND', 'North Dakota'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PA', 'Pennsylvania'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VT', 'Vermont'), ('VA', 'Virginia'), ('WA', 'Washington'), ('WV', 'West Virginia'), ('WI', 'Wisconsin'), ('WY', 'Wyoming')], max_length=255, null=True, verbose_name='state')),
('installed', models.BooleanField(default=False)),
('current_revision_date', models.DateField()),
('current_revision_date', models.DateField(default=django.utils.timezone.now)),
],
options={
'abstract': False,
},
bases=(wagtail.search.index.Indexed, models.Model),
),
migrations.CreateModel(
name='BidRevision',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
('number', models.IntegerField()),
('date', models.DateField()),
('bid', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.PROTECT, related_name='revisions', to='core.bid')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),
migrations.CreateModel(
name='BuildingCode',
fields=[
@ -292,6 +307,22 @@ class Migration(migrations.Migration):
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='RevScope',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
('name', models.CharField(max_length=255)),
('dwg_ref', models.CharField(blank=True, max_length=255, null=True, verbose_name='drawing reference')),
('grid', models.CharField(blank=True, max_length=16, null=True)),
('takeoff', models.JSONField(null=True)),
('revision', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='scopes', to='core.bidrevision')),
('slab_construction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.slabconstruction')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='project',
name='categories',
@ -374,21 +405,6 @@ class Migration(migrations.Migration):
('slab_construction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.slabconstruction')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),
migrations.CreateModel(
name='BidRevision',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
('number', models.IntegerField()),
('date', models.DateField()),
('bid', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.PROTECT, related_name='revisions', to='core.bid')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),

@ -1,5 +1,6 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import FieldPanel, FieldRowPanel
@ -71,17 +72,36 @@ class Bid(index.Indexed, ClusterableModel):
related_name='engineer_bids',
)
installed = models.BooleanField(default=False)
current_revision_date = models.DateField()
current_revision_date = models.DateField(default=timezone.now)
@property
def current_revision(self):
return 1 + self.revisions.count()
return self.revisions.count()
def make_new_revision(self):
rev = BidRevision.objects.create(
bid=self,
number=self.current_revision,
date=self.current_revision_date,
)
for scope in self.scopes.all():
RevScope.objects.create(
revision=rev,
name=scope.name,
dwg_ref=scope.dwg_ref,
grid=scope.grid,
slab_construction=scope.slab_construction,
takeoff=scope.takeoff,
)
def __str__(self):
return f'{self.number} {self.name}'
class BidRevision(Orderable, ClusterableModel):
class Meta:
ordering = ['-number']
bid = ParentalKey(Bid, on_delete=models.PROTECT, related_name='revisions')
number = models.IntegerField()
date = models.DateField()
@ -89,43 +109,14 @@ class BidRevision(Orderable, ClusterableModel):
def __str__(self):
return f'{self.bid.number} R{self.number}'
def is_current(self):
if self is self.bid.latest_revision():
return True
return False
def finalize_and_create_new(self):
if not self.is_current():
raise ValueError(f'{self} is not the current revision')
new_rev = self
new_rev.pk = None
new_rev.number = 1 + self.number
new_rev.save()
for scope in self.scopes.all():
new_scope = scope
new_scope.pk = None
new_scope.revision = new_rev
new_scope.save()
for floor in self.floors.all():
new_floor = floor
new_floor.pk = None
new_floor.scope = new_scope
new_floor.save()
return new_rev
def name(self):
return self.number
return f'Rev {self.number}'
def save(self, **kwargs):
super().save(**kwargs)
if self.is_current():
self.bid.current_revision_date = self.date
self.bid.save()
class Scope(Orderable, ClusterableModel):
class Meta:
abstract = True
class BidScope(Orderable, ClusterableModel):
# revision = ParentalKey(BidRevision, on_delete=models.PROTECT, related_name='scopes')
bid = ParentalKey(Bid, related_name='scopes')
name = models.CharField(max_length=255)
dwg_ref = models.CharField(
'drawing reference',
@ -137,10 +128,21 @@ class BidScope(Orderable, ClusterableModel):
slab_construction = models.ForeignKey(SlabConstruction, on_delete=models.PROTECT)
takeoff = models.JSONField(null=True)
class BidScope(Scope):
bid = ParentalKey(Bid, related_name='scopes')
def __str__(self):
return f'{self.bid} {self.name}'
class RevScope(Scope):
revision = ParentalKey(BidRevision, related_name='scopes')
def __str__(self):
return f'{self.revision} {self.name}'
TAKEOFF_SCHEMA = {
'type': 'array',
'items': {

@ -1,7 +1,14 @@
from wagtail.admin.panels import Panel, HelpPanel
from wagtail.admin.panels import (
Panel,
HelpPanel,
ObjectList,
InlinePanel,
FieldPanel,
)
from django.utils.functional import cached_property
class GenericPanel(Panel):
class TemplatePanel(Panel):
def __init__(self, template, **kwargs):
super().__init__(**kwargs)
self.template = template
@ -17,21 +24,29 @@ class GenericPanel(Panel):
self.template_name = self.panel.template
class PropertyRefPanel(HelpPanel):
def __init__(self, property_name, **kwargs):
super().__init__(**kwargs)
self.property_name = property_name
self.template = 'core/property_ref_panel.html'
def clone_kwargs(self):
kwargs = super().clone_kwargs()
kwargs['property_name'] = self.property_name
return kwargs
class BoundPanel(HelpPanel.BoundPanel):
class CurrentRevision(ObjectList):
class BoundPanel(ObjectList.BoundPanel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.property_name = self.panel.property_name
self.property_value = getattr(self.instance, self.property_name)
self.heading = f'Current Revision ({self.instance.current_revision})'
class ReadOnlyInlinePanel(InlinePanel):
@cached_property
def panel_definitions(self):
panels = []
for p in super().panel_definitions:
kwargs = p.clone_kwargs()
if 'read_only' in kwargs:
kwargs['read_only'] = True
panels.append(p.__class__(**kwargs))
return panels
class BoundPanel(InlinePanel.BoundPanel):
template_name = 'wagtailadmin/panels/readonly_inline_panel.html'
def get_context_data(self, parent_context=None):
context = super().get_context_data(parent_context)
context['can_order'] = False
context['can_delete'] = False
return context

@ -8,7 +8,9 @@
<button type="button" class="button button--icon text-replace white" data-inline-panel-child-move-up title="{% trans 'Move up' %}">{% icon name="arrow-up" %}</button>
<button type="button" class="button button--icon text-replace white" data-inline-panel-child-move-down title="{% trans 'Move down' %}">{% icon name="arrow-down" %}</button>
{% endif %}
<button type="button" class="button button--icon text-replace white" id="{{ child.form.DELETE.id_for_label }}-button" title="{% trans 'Delete' %}">{% icon name="bin" %}</button>
{% if can_delete %}
<button type="button" class="button button--icon text-replace white" id="{{ child.form.DELETE.id_for_label }}-button" title="{% trans 'Delete' %}">{% icon name="bin" %}</button>
{% endif %}
{% endfragment %}
{% fragment as heading %}
{{ child.instance.name }}

@ -0,0 +1,45 @@
{% load i18n l10n wagtailadmin_tags %}
{{ self.formset.management_form }}
{% if self.formset.non_form_errors %}
<div class="error-message">
{% for error in self.formset.non_form_errors %}
<span>{{ error|escape }}</span>
{% endfor %}
</div>
{% endif %}
{% if self.help_text %}
{% help_block status="info" %}{{ self.help_text }}{% endhelp_block %}
{% endif %}
<div id="id_{{ self.formset.prefix }}-FORMS">
{% comment %}
Child elements of this div will become orderable elements. Do not place additional
"furniture" elements here unless you intend them to be part of the child ordering.
{% endcomment %}
{% for child in self.children %}
{% include "wagtailadmin/panels/inline_panel_child.html" %}
{% endfor %}
</div>
<template id="id_{{ self.formset.prefix }}-EMPTY_FORM_TEMPLATE">
{% include "wagtailadmin/panels/inline_panel_child.html" with child=self.empty_child %}
</template>
{% block js_init %}
<script>
(function() {
var panel = new InlinePanel({
formsetPrefix: "id_{{ self.formset.prefix }}",
emptyChildFormPrefix: "{{ self.empty_child.form.prefix }}",
canOrder: {% if can_order %}true{% else %}false{% endif %},
maxForms: {{ self.formset.max_num|unlocalize }}
});
})();
</script>
{% endblock %}

@ -14,7 +14,6 @@ from wagtail.admin.panels import (
TabbedInterface,
ObjectList,
InlinePanel,
HelpPanel,
)
from wagtail.admin.views.reports import ReportView
from wagtail.admin.viewsets.chooser import ChooserViewSet
@ -24,6 +23,7 @@ from wagtail.snippets.views.snippets import (
CreateView,
EditView,
)
from .panels import TemplatePanel, CurrentRevision, ReadOnlyInlinePanel
from .models import (
Project,
ProjectCategory,
@ -304,11 +304,22 @@ class BidDocumentViewSet(SnippetViewSet):
]
from .panels import GenericPanel, PropertyRefPanel
class BidEditView(SnippetEditView):
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
if self.get_form().is_valid():
bid = self.get_object()
if 'create-bid-rev' in request.POST:
bid.make_new_revision()
elif 'delete-last-bid-rev' in request.POST:
bid.revisions.first().delete()
return response
class BidViewSet(SnippetViewSet):
model = Bid
icon = 'openquote'
edit_view_class = BidEditView
edit_handler = TabbedInterface([
ObjectList(
[
@ -341,10 +352,9 @@ class BidViewSet(SnippetViewSet):
],
heading='Supporting documents',
),
ObjectList(
CurrentRevision(
[
FieldRowPanel([
PropertyRefPanel('current_revision', heading='Current Revision'),
FieldPanel('current_revision_date', heading='Date'),
]),
InlinePanel(
@ -358,13 +368,41 @@ class BidViewSet(SnippetViewSet):
FieldPanel('grid'),
FieldPanel('dwg_ref'),
]),
GenericPanel('core/takeoff_edit_link.html'),
TemplatePanel('core/takeoff_edit_link.html'),
],
heading='Scopes',
),
],
heading='Current Revision',
),
ObjectList(
[
ReadOnlyInlinePanel(
'revisions',
panels = [
FieldPanel('date'),
ReadOnlyInlinePanel(
'scopes',
panels = [
FieldRowPanel([
FieldPanel('name', read_only=True),
FieldPanel(
'slab_construction',
widget=Select,
read_only=True,
),
]),
FieldRowPanel([
FieldPanel('grid', read_only=True),
FieldPanel('dwg_ref', read_only=True),
]),
],
),
],
),
],
heading='Past Revisions',
),
])

@ -4,6 +4,8 @@ from wagtail.admin.menu import AdminOnlyMenuItem
from wagtail.admin.userbar import AccessibilityItem
from wagtail.snippets.models import register_snippet
from wagtail.snippets.wagtail_hooks import SnippetsMenuItem
from wagtail.snippets.action_menu import ActionMenuItem
from .models.bid import Bid
from .views import (
ProjectViewSetGroup,
ProductViewSetGroup,
@ -45,3 +47,34 @@ def register_edit_takeoff():
name='edit_takeoff',
),
]
class BidRevActionMenuItem(ActionMenuItem):
def is_shown(self, context):
return context['model'] == Bid
class CreateBidRevMenuItem(BidRevActionMenuItem):
label = 'Create New Revision'
name = 'create-bid-rev'
icon_name = 'download'
@hooks.register('register_snippet_action_menu_item')
def register_finalize_bid_rev(whatisthisforthedocsdontmentionit):
return CreateBidRevMenuItem()
class DeleteLastBidRevMenuItem(BidRevActionMenuItem):
label = 'Delete Last Revision'
name = 'delete-last-bid-rev'
icon_name = 'cross'
def is_shown(self, context):
bid = context['instance']
return super().is_shown(context) and bid.revisions.count() > 0
@hooks.register('register_snippet_action_menu_item')
def register_delete_last_bid_rev(whatisthisforthedocsdontmentionit):
return DeleteLastBidRevMenuItem()

@ -3,9 +3,9 @@
set -xe
python manage.py migrate --noinput --verbosity 0
python manage.py loaddata products.json
python manage.py init_pages
python manage.py import_legacy --first-project 1680 > import.log
python manage.py loaddata products.json salesreps.json
export DJANGO_SUPERUSER_EMAIL=$1
export DJANGO_SUPERUSER_PASSWORD=$2

@ -6,6 +6,7 @@ certifi==2023.7.22
charset-normalizer==3.3.1
defusedxml==0.7.1
Django==4.2.6
django-extensions==3.2.3
django-filter==23.3
django-modelcluster==6.1
django-permissionedforms==0.1

@ -0,0 +1 @@
[{"model": "core.salesrep", "pk": 1, "fields": {"name": "Ed Gunning", "user": null}}, {"model": "core.salesrep", "pk": 2, "fields": {"name": "Tracy Goettsch", "user": 11}}]

@ -48,6 +48,7 @@ INSTALLED_APPS = [
"django.contrib.messages",
"django.contrib.staticfiles",
'django.contrib.humanize',
'django_extensions',
]
MIDDLEWARE = [

Loading…
Cancel
Save