A bunch of work toward the bid system

- lots of changes to snippets
- lots of work left to do but this commit is getting huge
master
Pete Ley 6 months ago
parent 311c8dc2fd
commit 3d1bbef4cb

@ -11,9 +11,9 @@ from core.models import (
Company,
Scope,
Finish,
StairProduct,
LandingProduct,
RailProduct,
StairTreadStyle,
LandingTreadStyle,
RailStyle,
MiscProduct,
)
@ -310,23 +310,23 @@ Import summary:
for name, parent in self._q(query):
if name == 'Specialty':
if parent == 1:
ptype = StairProduct
field = scope.stair_prods
ptype = StairTreadStyle
field = scope.stair_styles
elif parent == 3:
ptype = LandingProduct
field = scope.land_prods
ptype = LandingTreadStyle
field = scope.land_styles
elif parent == 4:
ptype = MiscProduct
field = scope.rail_prods
field = scope.rail_styles
elif re.match(r'^S[0-9]00', name):
ptype = StairProduct
field = scope.stair_prods
ptype = StairTreadStyle
field = scope.stair_styles
elif re.match(r'^L[0-9]00', name):
ptype = LandingProduct
field = scope.land_prods
ptype = LandingTreadStyle
field = scope.land_styles
elif re.match(r'^R[0-9]00', name):
ptype = RailProduct
field = scope.rail_prods
ptype = RailStyle
field = scope.rail_styles
else:
ptype = MiscProduct
field = scope.misc_prods

@ -1,4 +1,4 @@
# Generated by Django 4.2.6 on 2023-11-20 17:05
# Generated by Django 4.2.6 on 2023-11-22 18:16
from django.conf import settings
from django.db import migrations, models
@ -14,22 +14,81 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailimages', '0025_alter_image_file_alter_rendition_file'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtaildocs', '0012_uploadeddocument'),
('wagtailcore', '0089_log_entry_data_json_null_to_object'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtailimages', '0025_alter_image_file_alter_rendition_file'),
]
operations = [
migrations.CreateModel(
name='Company',
name='Bid',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.CharField(help_text='not including revision, e.g. "23-100"', max_length=16, unique=True)),
('name', models.CharField(max_length=255)),
('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')),
],
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)),
('revision', models.IntegerField()),
('date', models.DateField()),
('installed', models.BooleanField()),
('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='BidScope',
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, help_text='e.g. A401-1', max_length=255, null=True, verbose_name='drawing reference')),
('grid', models.CharField(blank=True, max_length=16, null=True)),
('revision', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.PROTECT, related_name='scopes', to='core.bidrevision')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),
migrations.CreateModel(
name='BuildingCode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(default=0, help_text='Lower numbers sort earlier')),
('name', models.CharField(max_length=255)),
('notes', wagtail.fields.RichTextField()),
],
options={
'ordering': ['sort_order', 'name'],
'abstract': False,
},
),
migrations.CreateModel(
name='Company',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
],
options={
'verbose_name_plural': 'companies',
'ordering': ['name'],
},
bases=(wagtail.search.index.Indexed, models.Model),
),
migrations.CreateModel(
name='Finish',
@ -55,12 +114,27 @@ class Migration(migrations.Migration):
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='LandingProduct',
name='LandingPostType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('sort_order', models.IntegerField(default=0, help_text='Lower numbers sort earlier')),
('description', models.CharField(max_length=255)),
('weight', models.DecimalField(decimal_places=1, help_text='pounds per foot', max_digits=6)),
],
options={
'ordering': ['sort_order', 'name'],
'abstract': False,
},
),
migrations.CreateModel(
name='LandingTreadStyle',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('sort_order', models.IntegerField(default=0, help_text='Lower numbers sort earlier')),
('description', wagtail.fields.RichTextField(blank=True, null=True)),
('base_price', models.DecimalField(blank=True, decimal_places=2, help_text='per square foot', max_digits=16, null=True)),
],
options={
'ordering': ['sort_order', 'name'],
@ -109,8 +183,8 @@ class Migration(migrations.Migration):
('award_date', models.DateField(blank=True, null=True)),
('complete_date', models.DateField(blank=True, null=True)),
('bid_number', models.CharField(blank=True, max_length=255, null=True)),
('contract_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)),
('actual_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True, verbose_name='closeout amount')),
('contract_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=16, null=True)),
('actual_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=16, null=True, verbose_name='closeout amount')),
('client_job_number', models.CharField(blank=True, max_length=255, null=True)),
('client_contract_no', models.CharField(blank=True, max_length=255, null=True, verbose_name='client contract number')),
('building_code', models.CharField(blank=True, max_length=255, null=True)),
@ -148,12 +222,29 @@ class Migration(migrations.Migration):
bases=(wagtail.contrib.routable_page.models.RoutablePageMixin, 'wagtailcore.page'),
),
migrations.CreateModel(
name='RailProduct',
name='RailStyle',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('sort_order', models.IntegerField(default=0, help_text='Lower numbers sort earlier')),
('description', wagtail.fields.RichTextField(blank=True, null=True)),
('base_price', models.DecimalField(blank=True, decimal_places=2, help_text='per linear foot', max_digits=16, null=True)),
('add_price_adj', models.DecimalField(blank=True, decimal_places=2, help_text='difference from base price for added/CIP, per linear foot', max_digits=16, null=True, verbose_name='add price adjust')),
],
options={
'ordering': ['sort_order', 'name'],
'abstract': False,
},
),
migrations.CreateModel(
name='SlabConstruction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(default=0, help_text='Lower numbers sort earlier')),
('name', models.CharField(max_length=255)),
('install_price', models.DecimalField(blank=True, decimal_places=2, max_digits=16, null=True)),
('uses_embeds', models.BooleanField()),
('uses_hangers', models.BooleanField()),
],
options={
'ordering': ['sort_order', 'name'],
@ -161,7 +252,7 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='StairProduct',
name='StairTreadStyle',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
@ -173,6 +264,71 @@ class Migration(migrations.Migration):
'abstract': False,
},
),
migrations.CreateModel(
name='StringerType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('sort_order', models.IntegerField(default=0, help_text='Lower numbers sort earlier')),
],
options={
'ordering': ['sort_order', 'name'],
'abstract': False,
},
),
migrations.CreateModel(
name='TakeoffFloor',
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(help_text='name, number, or designation', max_length=255)),
('risers', models.IntegerField(blank=True, default=0, help_text='count')),
('flights', models.IntegerField(blank=True, default=0, help_text='count')),
('ex_treads', models.IntegerField(blank=True, default=0, help_text='count', verbose_name='extended treads')),
('haunches', models.IntegerField(blank=True, default=0, help_text='count')),
('cane_rail', models.IntegerField(blank=True, default=0, help_text='linear feet')),
('guard_rail', models.IntegerField(blank=True, default=0, help_text='linear feet')),
('center_rail', models.IntegerField(blank=True, default=0, help_text='linear feet')),
('ex_sr', models.IntegerField(blank=True, default=0, help_text='linear feet, replaces wall rail', verbose_name='extra stair rail')),
('ex_wr', models.IntegerField(blank=True, default=0, help_text='linear feet, replaces stair rail', verbose_name='extra wall rail')),
('add_sr', models.IntegerField(blank=True, default=0, help_text='linear feet, e.g. at CIP, ramps, etc', verbose_name='additional stair rail')),
('add_wr', models.IntegerField(blank=True, default=0, help_text='linear feet, e.g. at CIP, ramps, etc', verbose_name='additional wall rail')),
('gates', models.IntegerField(blank=True, default=0, help_text='count')),
('standpipes', models.IntegerField(blank=True, default=0, help_text='count')),
('scope', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='floors', to='core.bidscope')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),
migrations.CreateModel(
name='TakeoffLanding',
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)),
('width', models.IntegerField(help_text='inches')),
('depth', models.IntegerField(help_text='inches')),
('post_count', models.IntegerField(blank=True, default=0)),
('post_length', models.IntegerField(blank=True, default=0, help_text='each post, linear feet')),
('floor', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='landings', to='core.takeofffloor')),
('post_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.landingposttype')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),
migrations.CreateModel(
name='StairPricePoint',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('width', models.IntegerField(help_text='inches')),
('price', models.DecimalField(decimal_places=2, help_text='per riser', max_digits=16)),
('stringer_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.stringertype')),
('tread_style', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.stairtreadstyle')),
],
),
migrations.CreateModel(
name='Scope',
fields=[
@ -188,15 +344,24 @@ class Migration(migrations.Migration):
('mid_landing_count', models.IntegerField(default=0)),
('floor_landing_count', models.IntegerField(default=0)),
('finishes', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.finish')),
('land_prods', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.landingproduct', verbose_name='landing products')),
('land_styles', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.landingtreadstyle', verbose_name='landing tread styles')),
('misc_prods', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.miscproduct', verbose_name='misc products')),
('project', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='scopes', to='core.project')),
('rail_prods', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.railproduct', verbose_name='rail products')),
('stair_prods', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.stairproduct', verbose_name='stair products')),
('rail_styles', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.railstyle', verbose_name='rail styles')),
('stair_styles', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.stairtreadstyle', verbose_name='stair tread styles')),
],
options={
'abstract': False,
'ordering': ['project__number', 'name'],
},
bases=(wagtail.search.index.Indexed, models.Model),
),
migrations.CreateModel(
name='SalesRep',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='project',
@ -243,6 +408,11 @@ class Migration(migrations.Migration):
name='ship_address',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ship_address', to='core.location'),
),
migrations.AddField(
model_name='landingtreadstyle',
name='stair_style',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.stairtreadstyle', verbose_name='corresponding stair tread style'),
),
migrations.CreateModel(
name='LandingPageImage',
fields=[
@ -262,4 +432,52 @@ class Migration(migrations.Migration):
('page', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailcore.page')),
],
),
migrations.AddField(
model_name='bidscope',
name='slab_construction',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.slabconstruction'),
),
migrations.CreateModel(
name='BidDocument',
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)),
('bid', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='core.bid')),
('doc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wagtaildocs.document')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),
migrations.AddField(
model_name='bid',
name='architect',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='architect_bids', to='core.company'),
),
migrations.AddField(
model_name='bid',
name='client',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='client_bids', to='core.company'),
),
migrations.AddField(
model_name='bid',
name='contractor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contractor_bids', to='core.company'),
),
migrations.AddField(
model_name='bid',
name='engineer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='engineer_bids', to='core.company'),
),
migrations.AddField(
model_name='bid',
name='estimator',
field=models.ForeignKey(limit_choices_to={'groups__name__in': ['staff estimator']}, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='bid',
name='sales_rep',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.salesrep'),
),
]

@ -36,6 +36,10 @@ class LegacyModel(models.Model):
abstract = True
def cash_field(**kwargs):
return models.DecimalField(decimal_places=2, max_digits=16, **kwargs)
from .client import * # noqa: E402
from .frontend import * # noqa: E402
from .location import * # noqa: E402

@ -0,0 +1,198 @@
from django.core.exceptions import ValidationError
from django.db import models
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
from wagtail.documents.models import Document
from wagtail.models import Orderable
from wagtail.search import index
from sf_auth.models import User
from .client import Company
from .product import LandingPostType
from .project import SlabConstruction
from .location import STATES
class SalesRep(models.Model):
name = models.CharField(max_length=255)
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL)
def __str__(self):
return self.name
class Bid(index.Indexed, ClusterableModel):
number = models.CharField(
max_length=16,
unique=True,
help_text='not including revision, e.g. "23-100"'
)
name = models.CharField(max_length=255)
estimator = models.ForeignKey(
User,
on_delete=models.PROTECT,
limit_choices_to={'groups__name__in': ['staff estimator']},
)
sales_rep = models.ForeignKey(SalesRep, on_delete=models.PROTECT)
city = models.CharField(max_length=255, blank=True, null=True)
province = models.CharField(
'state',
max_length=255,
blank=True,
null=True,
choices=STATES,
)
client = models.ForeignKey(
Company,
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='client_bids',
)
contractor = models.ForeignKey(
Company,
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='contractor_bids',
)
architect = models.ForeignKey(
Company,
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='architect_bids',
)
engineer = models.ForeignKey(
Company,
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='engineer_bids',
)
def __str__(self):
return f'{self.number} {self.name}'
class BidDocument(Orderable, ClusterableModel):
bid = ParentalKey(Bid, on_delete=models.CASCADE, related_name='documents')
doc = models.ForeignKey(Document, on_delete=models.CASCADE)
def __str__(self):
return self.doc.title
def name(self):
return self.doc.title
class BidRevision(Orderable, ClusterableModel):
bid = ParentalKey(Bid, on_delete=models.PROTECT, related_name='revisions')
revision = models.IntegerField()
date = models.DateField()
installed = models.BooleanField()
def __str__(self):
return f'{self.bid.number} R{self.revision}'
def name(self):
return self.revision
class BidScope(Orderable, ClusterableModel):
revision = ParentalKey(BidRevision, on_delete=models.PROTECT, related_name='scopes')
name = models.CharField(max_length=255)
dwg_ref = models.CharField(
'drawing reference',
max_length=255,
blank=True,
null=True,
help_text='e.g. A401-1',
)
grid = models.CharField(max_length=16, blank=True, null=True)
slab_construction = models.ForeignKey(SlabConstruction, on_delete=models.PROTECT)
class TakeoffFloor(Orderable, ClusterableModel):
scope = ParentalKey(BidScope, on_delete=models.CASCADE, related_name='floors')
name = models.CharField(max_length=255, help_text='name, number, or designation')
risers = models.IntegerField(help_text='count', blank=True, default=0)
flights = models.IntegerField(help_text='count', blank=True, default=0)
ex_treads = models.IntegerField(
'extended treads',
help_text='count',
blank=True,
default=0,
)
haunches = models.IntegerField(help_text='count', blank=True, default=0)
cane_rail = models.IntegerField(help_text='linear feet', blank=True, default=0)
guard_rail = models.IntegerField(help_text='linear feet', blank=True, default=0)
center_rail = models.IntegerField(help_text='linear feet', blank=True, default=0)
ex_sr = models.IntegerField(
'extra stair rail',
help_text='linear feet, replaces wall rail',
blank=True,
default=0,
)
ex_wr = models.IntegerField(
'extra wall rail',
help_text='linear feet, replaces stair rail',
blank=True,
default=0,
)
add_sr = models.IntegerField(
'additional stair rail',
help_text='linear feet, e.g. at CIP, ramps, etc',
blank=True,
default=0,
)
add_wr = models.IntegerField(
'additional wall rail',
help_text='linear feet, e.g. at CIP, ramps, etc',
blank=True,
default=0,
)
gates = models.IntegerField(help_text='count', blank=True, default=0)
standpipes = models.IntegerField(help_text='count', blank=True, default=0)
class TakeoffLanding(Orderable, ClusterableModel):
floor = ParentalKey(TakeoffFloor, on_delete=models.CASCADE, related_name='landings')
width = models.IntegerField(help_text='inches')
depth = models.IntegerField(help_text='inches')
post_count = models.IntegerField(blank=True, default=0)
post_type = models.ForeignKey(
LandingPostType,
on_delete=models.PROTECT,
blank=True,
null=True,
)
post_length = models.IntegerField(
help_text='each post, linear feet',
blank=True,
default=0,
)
def clean(self):
if self.post_count > 0 and self.post_type is None:
raise ValidationError('Post type required when post count > 0')
def scope(self):
return self.floor.scope
# derived properties
def area_sf(self):
return (self.width / 12.0) * (self.depth / 12.0)
def embed_count(self):
if self.scope.slab_construction.uses_embeds:
return 4
return 0
def hanger_plate_count(self):
if self.scope.slab_construction.uses_hangers:
return 4 - self.post_count
return 0

@ -1,8 +1,9 @@
from django.db import models
from wagtail.search import index
class Company(models.Model):
name = models.CharField(max_length=255)
class Company(index.Indexed, models.Model):
name = models.CharField(max_length=255, unique=True)
def __str__(self):
return self.name
@ -10,4 +11,8 @@ class Company(models.Model):
class Meta:
verbose_name_plural = 'companies'
ordering = ['name']
search_fields = [
index.SearchField('name'),
index.AutocompleteField('name'),
]

@ -3,61 +3,63 @@ from wagtail.search import index
from . import Named, LegacyModel
STATES = [
('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'),
]
class Location(index.Indexed, Named, LegacyModel):
street = models.CharField(max_length=255, blank=True, null=True)
city = models.CharField(max_length=255, blank=True, null=True)
STATES = [
('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'),
]
province = models.CharField(max_length=2, choices=STATES, verbose_name='state')
postal_code = models.CharField(max_length=255, blank=True, null=True)
notes = models.TextField(blank=True, null=True)

@ -1,5 +1,9 @@
from django.db import models
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
from wagtail.fields import RichTextField
from . import Named, Orderable, ChoiceList
from wagtail.models import Orderable
from . import ChoiceList, cash_field
class Finish(ChoiceList):
@ -7,6 +11,19 @@ class Finish(ChoiceList):
verbose_name_plural = 'finishes'
class StringerType(ChoiceList):
pass
class LandingPostType(ChoiceList):
description = models.CharField(max_length=255)
weight = models.DecimalField(
help_text='pounds per foot',
max_digits=6,
decimal_places=1,
)
class Product(ChoiceList):
description = RichTextField(blank=True, null=True)
@ -14,17 +31,42 @@ class Product(ChoiceList):
abstract = True
class StairProduct(Product):
class StairTreadStyle(Product):
pass
class LandingProduct(Product):
pass
class StairPricePoint(models.Model):
tread_style = models.ForeignKey(StairTreadStyle, on_delete=models.CASCADE)
stringer_type = models.ForeignKey(StringerType, on_delete=models.CASCADE)
width = models.IntegerField(help_text='inches')
price = cash_field(help_text='per riser')
class RailProduct(Product):
pass
class LandingTreadStyle(Product):
base_price = cash_field(help_text='per square foot', blank=True, null=True)
stair_style = models.ForeignKey(
StairTreadStyle,
verbose_name='corresponding stair tread style',
on_delete=models.SET_NULL,
blank=True,
null=True,
)
def clean(self):
self.description = self.stair_style.description
class RailStyle(Product):
base_price = cash_field(help_text='per linear foot', blank=True, null=True)
add_price_adj = cash_field(
verbose_name='add price adjust',
help_text='difference from base price for added/CIP, per linear foot',
blank=True,
null=True,
)
class MiscProduct(Product):
pass

@ -9,24 +9,38 @@ from wagtail.admin.panels import (
TabbedInterface,
ObjectList,
)
from wagtail.models import Page, Orderable
from wagtail.contrib.routable_page.models import RoutablePageMixin, path
from wagtail.fields import RichTextField
from wagtail.models import Page, Orderable
from wagtail.search import index
from . import (
Company,
LandingPage,
LandingProduct,
LandingTreadStyle,
Location,
Finish,
MiscProduct,
RailProduct,
StairProduct,
RailStyle,
StairTreadStyle,
ChoiceList,
LegacyModel
LegacyModel,
cash_field,
)
from sf_auth.models import User
class BuildingCode(ChoiceList):
name = models.CharField(max_length=255)
notes = RichTextField()
class SlabConstruction(ChoiceList):
name = models.CharField(max_length=255)
install_price = cash_field(blank=True, null=True)
uses_embeds = models.BooleanField()
uses_hangers = models.BooleanField()
class ProjectCategory(LegacyModel, ChoiceList):
class Meta(ChoiceList.Meta):
verbose_name_plural = 'project categories'
@ -51,19 +65,8 @@ class Project(LegacyModel, index.Indexed, ClusterableModel):
award_date = models.DateField(null=True, blank=True)
complete_date = models.DateField(null=True, blank=True)
bid_number = models.CharField(max_length=255, blank=True, null=True)
contract_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
blank=True,
null=True,
)
actual_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
blank=True,
null=True,
verbose_name='closeout amount',
)
contract_amount = cash_field(blank=True, null=True)
actual_amount = cash_field(verbose_name='closeout amount', blank=True, null=True)
client_job_number = models.CharField(max_length=255, blank=True, null=True)
client_contract_no = models.CharField(
@ -190,7 +193,10 @@ class ProjectIndexPage(RoutablePageMixin, Page):
)
class Scope(LegacyModel, Orderable, ClusterableModel):
class Scope(LegacyModel, index.Indexed, Orderable, ClusterableModel):
class Meta:
ordering = ['project__number', 'name']
project = ParentalKey(Project, on_delete=models.CASCADE, related_name='scopes')
name = models.CharField(
max_length=255,
@ -207,20 +213,20 @@ class Scope(LegacyModel, Orderable, ClusterableModel):
notes = models.TextField(blank=True, null=True)
finishes = ParentalManyToManyField(Finish, blank=True)
stair_prods = ParentalManyToManyField(
StairProduct,
stair_styles = ParentalManyToManyField(
StairTreadStyle,
blank=True,
verbose_name='stair products',
verbose_name='stair tread styles',
)
land_prods = ParentalManyToManyField(
LandingProduct,
land_styles = ParentalManyToManyField(
LandingTreadStyle,
blank=True,
verbose_name='landing products',
verbose_name='landing tread styles',
)
rail_prods = ParentalManyToManyField(
RailProduct,
rail_styles = ParentalManyToManyField(
RailStyle,
blank=True,
verbose_name='rail products',
verbose_name='rail styles',
)
misc_prods = ParentalManyToManyField(
MiscProduct,
@ -255,9 +261,9 @@ class Scope(LegacyModel, Orderable, ClusterableModel):
]),
MultiFieldPanel([
FieldRowPanel([
FieldPanel('stair_prods', widget=CheckboxSelectMultiple),
FieldPanel('land_prods', widget=CheckboxSelectMultiple),
FieldPanel('rail_prods', widget=CheckboxSelectMultiple),
FieldPanel('stair_styles', widget=CheckboxSelectMultiple),
FieldPanel('land_styles', widget=CheckboxSelectMultiple),
FieldPanel('rail_styles', widget=CheckboxSelectMultiple),
FieldPanel('misc_prods', widget=CheckboxSelectMultiple),
]),
FieldRowPanel([
@ -265,3 +271,23 @@ class Scope(LegacyModel, Orderable, ClusterableModel):
]),
]),
]
search_fields = [
index.SearchField('name'),
index.AutocompleteField('name'),
index.SearchField('notes'),
index.AutocompleteField('notes'),
index.FilterField('stair_styles'),
index.FilterField('land_styles'),
index.FilterField('rail_styles'),
index.FilterField('misc_prods'),
index.RelatedFields('project', [
index.SearchField('name'),
index.AutocompleteField('name'),
index.SearchField('number'),
index.AutocompleteField('number'),
]),
]

@ -1,5 +1,5 @@
from django.db.models import Count
from django.forms.widgets import CheckboxSelectMultiple
from django.forms.widgets import Select, CheckboxSelectMultiple
from wagtail.admin.panels import (
FieldPanel,
FieldRowPanel,
@ -10,31 +10,54 @@ from wagtail.admin.panels import (
)
from wagtail.admin.views.reports import ReportView
from wagtail.admin.viewsets.chooser import ChooserViewSet
from wagtail.snippets.views.snippets import SnippetViewSet, SnippetViewSetGroup
from wagtail.snippets.views.snippets import (
SnippetViewSet,
SnippetViewSetGroup,
CreateView,
EditView,
)
from .models import (
Project,
ProjectCategory,
Location,
Company,
StairProduct,
LandingProduct,
RailProduct,
StairTreadStyle,
LandingTreadStyle,
RailStyle,
MiscProduct,
Scope,
Finish,
)
from .models.bid import (
SalesRep,
Bid,
BidDocument,
BidRevision,
BidScope,
TakeoffFloor,
TakeoffLanding,
)
class SnippetCreateView(CreateView):
def get_success_url(self):
return self.get_edit_url()
class SnippetEditView(EditView):
def get_success_url(self):
return self.get_edit_url()
class PerPageMixin():
list_per_page = 50
SnippetViewSet.create_view_class = SnippetCreateView
SnippetViewSet.edit_view_class = SnippetEditView
SnippetViewSet.list_per_page = 50
class ProjectViewSet(PerPageMixin, SnippetViewSet):
class ProjectViewSet(SnippetViewSet):
model = Project
icon = 'helmet'
list_display = ['__str__', 'status', 'project_manager']
search_fields = ['number', 'name']
list_filter = ['status', 'categories', 'project_manager']
edit_handler = TabbedInterface([
ObjectList(
@ -108,7 +131,14 @@ class ProjectViewSet(PerPageMixin, SnippetViewSet):
])
class ProjectCategoryViewSet(PerPageMixin, SnippetViewSet):
class ScopeViewSet(SnippetViewSet):
model = Scope
icon = 'crosshairs'
list_display = ['name', 'project']
list_filter = ['stair_styles', 'land_styles', 'rail_styles', 'misc_prods']
class ProjectCategoryViewSet(SnippetViewSet):
model = ProjectCategory
icon = 'form'
list_display = ['name']
@ -131,7 +161,7 @@ class LocationChooserViewSet(ChooserViewSet):
]
class LocationViewSet(PerPageMixin, SnippetViewSet):
class LocationViewSet(SnippetViewSet):
model = Location
icon = 'location'
chooser_viewset_class = LocationChooserViewSet
@ -149,9 +179,15 @@ class LocationViewSet(PerPageMixin, SnippetViewSet):
]
class CompanyViewSet(PerPageMixin, SnippetViewSet):
class CompanyChooserViewSet(ChooserViewSet):
model = Company
form_fields = ['name']
class CompanyViewSet(SnippetViewSet):
model = Company
icon = 'building'
chooser_viewset_class = CompanyChooserViewSet
list_display = ['name']
panels = [
@ -164,51 +200,82 @@ class ProjectViewSetGroup(SnippetViewSetGroup):
add_to_admin_menu = True
items = [
ProjectViewSet,
ScopeViewSet,
ProjectCategoryViewSet,
LocationViewSet,
CompanyViewSet,
]
class ProductViewSet(PerPageMixin, SnippetViewSet):
class ProductViewSet(SnippetViewSet):
list_display = ['name']
panels = [
FieldPanel('name'),
FieldPanel('description'),
FieldPanel('sort_order'),
]
frontend_panels_tab = ObjectList(
[
FieldPanel('name'),
FieldPanel('description'),
FieldPanel('sort_order'),
],
heading='Frontend',
)
class StairProductViewSet(ProductViewSet):
model = StairProduct
class StairTreadStyleViewSet(ProductViewSet):
model = StairTreadStyle
edit_handler = TabbedInterface([ProductViewSet.frontend_panels_tab])
class LandingProductViewSet(ProductViewSet):
model = LandingProduct
class LandingTreadStyleViewSet(ProductViewSet):
model = LandingTreadStyle
edit_handler = TabbedInterface([
ObjectList(
[
FieldPanel('name'),
FieldPanel('stair_style'),
FieldPanel('sort_order'),
],
heading='Frontend',
),
ObjectList(
[
FieldPanel('base_price'),
],
heading='Pricing',
)
])
class RailProductViewSet(ProductViewSet):
model = RailProduct
class RailStyleViewSet(ProductViewSet):
model = RailStyle
edit_handler = TabbedInterface([
ProductViewSet.frontend_panels_tab,
ObjectList(
[
FieldPanel('base_price'),
FieldPanel('add_price_adj'),
],
heading='Pricing',
)
])
class MiscProductViewSet(ProductViewSet):
model = MiscProduct
class FinishViewSet(PerPageMixin, SnippetViewSet):
class FinishViewSet(SnippetViewSet):
model = Finish
panels = [
FieldPanel('name'),
FieldPanel('sort_order'),
]
class ProductViewSetGroup(SnippetViewSetGroup):
items = [
StairProductViewSet,
LandingProductViewSet,
RailProductViewSet,
StairTreadStyleViewSet,
LandingTreadStyleViewSet,
RailStyleViewSet,
MiscProductViewSet,
FinishViewSet,
]
@ -216,14 +283,87 @@ class ProductViewSetGroup(SnippetViewSetGroup):
add_to_admin_menu = True
class ScopeViewSet(SnippetViewSet):
model = Scope
class BidScopeViewSet(SnippetViewSet):
model = BidScope
icon = 'crosshairs'
class MultiFinishScopesReport(ReportView):
title = 'Scopes specifying multiple finishes'
header_icon = 'form'
class BidRevisionViewSet(SnippetViewSet):
model = BidRevision
icon = 'edit'
panels = [
FieldPanel('bid'),
FieldPanel('revision'),
FieldPanel('date'),
FieldPanel('installed'),
InlinePanel('scopes'),
]
def get_queryset(self):
scopes = Scope.objects.all().annotate(finish_count=Count('finishes'))
return scopes.filter(finish_count__gt=1)
class BidDocumentViewSet(SnippetViewSet):
model = BidDocument
icon = 'doc-full'
panels = [
FieldPanel('doc'),
]
class BidViewSet(SnippetViewSet):
model = Bid
icon = 'openquote'
edit_handler = TabbedInterface([
ObjectList(
[
MultiFieldPanel([
FieldPanel('number'),
FieldPanel('name'),
FieldRowPanel([
FieldPanel('city'),
FieldPanel('province'),
]),
]),
FieldRowPanel([
FieldPanel('estimator'),
FieldPanel('sales_rep', widget=Select),
]),
FieldRowPanel([
FieldPanel('client'),
FieldPanel('contractor'),
]),
FieldRowPanel([
FieldPanel('architect'),
FieldPanel('engineer'),
]),
],
heading='Basic info',
),
ObjectList(
[
InlinePanel('documents'),
],
heading='Supporting documents',
),
ObjectList(
[
InlinePanel('revisions'),
],
heading='Revisions',
),
])
class SalesRepViewSet(SnippetViewSet):
model = SalesRep
icon = 'group'
class BidViewSetGroup(SnippetViewSetGroup):
menu_label = 'Estimating'
add_to_admin_menu = True
items = [
BidViewSet,
BidRevisionViewSet,
BidScopeViewSet,
BidDocumentViewSet,
SalesRepViewSet,
]

@ -7,12 +7,13 @@ from wagtail.snippets.wagtail_hooks import SnippetsMenuItem
from .views import (
ProjectViewSetGroup,
ProductViewSetGroup,
MultiFinishScopesReport,
BidViewSetGroup,
)
register_snippet(ProjectViewSetGroup)
register_snippet(ProductViewSetGroup)
register_snippet(BidViewSetGroup)
@hooks.register('construct_wagtail_userbar')
@ -32,23 +33,3 @@ def register_icons(icons):
@hooks.register('construct_main_menu')
def remove_snippets(request, menu_items):
menu_items[:] = [item for item in menu_items if not isinstance(item, SnippetsMenuItem)]
@hooks.register('register_reports_menu_item')
def register_multi_finish_scopes_report_menu_item():
return AdminOnlyMenuItem(
'Multi-Finish Scopes',
reverse('multi_finish_scopes_report'),
icon_name=MultiFinishScopesReport.header_icon,
)
@hooks.register('register_admin_urls')
def register_multi_finish_scopes_report_url():
return [
path(
'reports/multi-finish-scopes/',
MultiFinishScopesReport.as_view(),
name='multi_finish_scopes_report',
)
]

@ -5,7 +5,7 @@ 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 > import.log
python manage.py import_legacy --first-project 1680 > import.log
export DJANGO_SUPERUSER_EMAIL=$1
export DJANGO_SUPERUSER_PASSWORD=$2

File diff suppressed because one or more lines are too long

@ -31,6 +31,9 @@ class UserManager(BaseUserManager):
class User(AbstractBaseUser, PermissionsMixin):
class Meta:
ordering = ['last_name', 'first_name']
email = models.EmailField(
verbose_name="email address",
max_length=255,

Loading…
Cancel
Save