Bid scope takeoff summaries

master
Pete Ley 5 months ago
parent 81cc371d18
commit 536baaaa97

@ -1,6 +1,7 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from functools import reduce
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import FieldPanel, FieldRowPanel
@ -22,7 +23,13 @@ class SalesRep(models.Model):
return self.name
class Bid(index.Indexed, ClusterableModel):
class ScopeSummaryMixin:
def summary(self):
summaries = [scope.summary() for scope in self.scopes.all()]
return reduce(BaseBidScope.sum_dicts, summaries, {})
class Bid(ScopeSummaryMixin, index.Indexed, ClusterableModel):
number = models.CharField(
max_length=16,
unique=True,
@ -84,6 +91,8 @@ class Bid(index.Indexed, ClusterableModel):
number=self.current_revision,
date=self.current_revision_date,
)
self.current_revision_date = timezone.now()
self.save()
for scope in self.scopes.all():
RevScope.objects.create(
revision=rev,
@ -98,7 +107,7 @@ class Bid(index.Indexed, ClusterableModel):
return f'{self.number} {self.name}'
class BidRevision(Orderable, ClusterableModel):
class BidRevision(ScopeSummaryMixin, Orderable, ClusterableModel):
class Meta:
ordering = ['-number']
@ -113,7 +122,7 @@ class BidRevision(Orderable, ClusterableModel):
return f'Rev {self.number}'
class Scope(Orderable, ClusterableModel):
class BaseBidScope(Orderable, ClusterableModel):
class Meta:
abstract = True
@ -128,87 +137,61 @@ class Scope(Orderable, ClusterableModel):
slab_construction = models.ForeignKey(SlabConstruction, on_delete=models.PROTECT)
takeoff = models.JSONField(null=True)
TAKEOFF_KEYS = {
'risers': 'Risers',
'flights': 'Flights',
'ex_treads': 'Extended Treads',
'haunches': 'Haunches',
'mid_landings': 'Mid Landings',
'floor_landings': 'Floor Landings',
'columns': 'Columns',
'cane_rail': 'Cane Rail',
'guard_rail': 'Guard Rail',
'center_rail': 'Center Rail',
'ex_sr': 'Extra Stair Rail',
'ex_wr': 'Extra Wall Rail',
'add_sr': 'Added Stair Rail',
'add_wr': 'Added Wall Rail',
'gates': 'Gates',
'standpipes': 'Standpipes',
}
TAKEOFF_SCHEMA = {
'type': 'array',
'items': {
'type': 'object',
'description': 'A bid scope takeoff',
'properties':
{'name': {'type': 'string'}} | {k: {'type': 'number'} for k in TAKEOFF_KEYS}
},
}
def sum_dicts(d1, d2):
return {
k: d1.get(k, 0) + d2.get(k, 0)
for k in (set(d1) | set(d2)) - set(['name'])
}
class BidScope(Scope):
def summary(self):
if self.takeoff:
return reduce(BaseBidScope.sum_dicts, self.takeoff, {})
return {}
class BidScope(BaseBidScope):
bid = ParentalKey(Bid, related_name='scopes')
def __str__(self):
return f'{self.bid} {self.name}'
class RevScope(Scope):
class RevScope(BaseBidScope):
revision = ParentalKey(BidRevision, related_name='scopes')
def __str__(self):
return f'{self.revision} {self.name}'
TAKEOFF_SCHEMA = {
'type': 'array',
'items': {
'type': 'object',
'description': 'A bid scope takeoff',
'properties': {
'name': {
'type': 'string'
},
# stair
'risers': {
'type': 'number'
},
'flights': {
'type': 'number'
},
'ex_treads': {
'type': 'number'
},
'haunches': {
'type': 'number'
},
# landing
'mid_landings': {
'type': 'number'
},
'floor_landings': {
'type': 'number'
},
'columns': {
'type': 'number'
},
# rail
'cane_rail': {
'type': 'number'
},
'guard_rail': {
'type': 'number'
},
'center_rail': {
'type': 'number'
},
'ex_sr': {
'type': 'number'
},
'ex_wr': {
'type': 'number'
},
'add_sr': {
'type': 'number'
},
'add_wr': {
'type': 'number'
},
'gates': {
'type': 'number'
},
# misc
'standpipes': {
'type': 'number'
},
}
}
}
class BidDocument(Orderable, ClusterableModel):
bid = ParentalKey(Bid, on_delete=models.CASCADE, related_name='documents')
doc = models.ForeignKey(Document, on_delete=models.CASCADE, verbose_name='document')

@ -1,8 +1,6 @@
{% if self.instance.pk %}
{% with pk=self.instance.pk %}
<fieldset>
<a href="#" id="takeoff-link-{{ pk }}">Edit Takeoff</a>
</fieldset>
<a href="#" id="takeoff-link-{{ pk }}">Edit Takeoff</a>
<script>
const takeoffLink{{ pk }} = document.querySelector("#takeoff-link-{{ pk }}")
takeoffLink{{ pk }}.addEventListener('click', () => {

@ -0,0 +1,83 @@
{% if self.instance.summary %}
{% with summary=self.instance.summary %}
<style>
dl { margin-right: 20px }
</style>
<div class="w-field__label">
<h2>Scope Summary</h2>
</div>
<div style="display: flex">
<dl>
{% if summary.risers %}
<dt>Risers</dt>
<dd>{{ summary.risers }}</dd>
{% endif %}
{% if summary.flights %}
<dt>Flights</dt>
<dd>{{ summary.flights }}</dd>
{% endif %}
{% if summary.ex_treads %}
<dt>Extended Treads</dt>
<dd>{{ summary.ex_treads }}</dd>
{% endif %}
{% if summary.haunches %}
<dt>Haunches</dt>
<dd>{{ summary.haunches }}</dd>
{% endif %}
</dl>
<dl>
{% if summary.mid_landings %}
<dt>Mid Landings</dt>
<dd>{{ summary.mid_landings }}</dd>
{% endif %}
{% if summary.floor_landings %}
<dt>Floor Landings</dt>
<dd>{{ summary.floor_landings }}</dd>
{% endif %}
{% if summary.columns %}
<dt>Columns</dt>
<dd>{{ summary.columns }}</dd>
{% endif %}
{% if summary.standpipes %}
<dt>Standpipes</dt>
<dd>{{ summary.ex_sr }}</dd>
{% endif %}
</dl>
<dl>
{% if summary.ex_sr %}
<dt>Extra Stair Rail</dt>
<dd>{{ summary.ex_sr }}</dd>
{% endif %}
{% if summary.ex_wr %}
<dt>Extra Wall Rail</dt>
<dd>{{ summary.ex_wr }}</dd>
{% endif %}
{% if summary.add_sr %}
<dt>Added Stair Rail</dt>
<dd>{{ summary.add_sr }}</dd>
{% endif %}
{% if summary.add_wr %}
<dt>Added Wall Rail</dt>
<dd>{{ summary.add_wr }}</dd>
{% endif %}
</dl>
<dl>
{% if summary.cane_rail %}
<dt>Cane Rail</dt>
<dd>{{ summary.cane_rail }}</dd>
{% endif %}
{% if summary.guard_rail %}
<dt>Guard Rail</dt>
<dd>{{ summary.guard_rail }}</dd>
{% endif %}
{% if summary.center_rail %}
<dt>Center Rail</dt>
<dd>{{ summary.center_rail }}</dd>
{% endif %}
</dl>
</div>
{% endwith %}
{% endif %}

@ -43,7 +43,6 @@ from .models.bid import (
BidDocument,
BidRevision,
BidScope,
TAKEOFF_SCHEMA,
)
@ -346,8 +345,9 @@ class BidViewSet(SnippetViewSet):
),
CurrentRevision(
[
FieldRowPanel([
MultiFieldPanel([
FieldPanel('current_revision_date', heading='Date'),
TemplatePanel('core/takeoff_summary.html'),
]),
InlinePanel(
'scopes',
@ -361,6 +361,7 @@ class BidViewSet(SnippetViewSet):
FieldPanel('dwg_ref'),
]),
TemplatePanel('core/takeoff_edit_link.html'),
TemplatePanel('core/takeoff_summary.html'),
],
heading='Scopes',
),
@ -372,7 +373,10 @@ class BidViewSet(SnippetViewSet):
ReadOnlyInlinePanel(
'revisions',
panels = [
FieldPanel('date'),
MultiFieldPanel([
FieldPanel('date'),
TemplatePanel('core/takeoff_summary.html'),
]),
ReadOnlyInlinePanel(
'scopes',
panels = [
@ -388,6 +392,7 @@ class BidViewSet(SnippetViewSet):
FieldPanel('grid', read_only=True),
FieldPanel('dwg_ref', read_only=True),
]),
TemplatePanel('core/takeoff_summary.html'),
],
),
],
@ -418,12 +423,12 @@ def edit_takeoff(request, pk):
if request.method == 'POST':
try:
takeoff = json.load(request)
validate(takeoff, TAKEOFF_SCHEMA)
validate(takeoff, BidScope.TAKEOFF_SCHEMA)
except ValidationError as e:
response = HttpResponseBadRequest()
response.reason_phrase = str(e)
return response
scope.takeoff = json.dumps(takeoff)
scope.takeoff = takeoff
scope.save()
return HttpResponse()
return render(request, 'core/takeoff_edit.html', context={'scope': scope})

@ -71,8 +71,9 @@ class DeleteLastBidRevMenuItem(BidRevActionMenuItem):
icon_name = 'cross'
def is_shown(self, context):
bid = context['instance']
return super().is_shown(context) and bid.revisions.count() > 0
if 'instance' in context:
bid = context['instance']
return super().is_shown(context) and bid.revisions.count() > 0
@hooks.register('register_snippet_action_menu_item')

@ -52,7 +52,9 @@
Complete commercial egress solutions. Call us!
<a href="tel:+18884778247">888.477.8247</a>
</p>
<a href="admin/">Log in</a>
{% if user.is_staff %}
<a href="admin">{% if user.is_authenticated %}Admin{% else %}Log in{% endif %}</a>
{% endif %}
</div>
</div>
</div>

Loading…
Cancel
Save