Version 3 Dockerization

This commit is contained in:
Nathan Chapman 2023-01-21 14:15:36 -07:00
parent 510dcd1462
commit eae76c773d
333 changed files with 54718 additions and 9334 deletions

View File

@ -1 +0,0 @@
mchretien@forum.dk

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
data/

10
.gitignore vendored
View File

@ -131,4 +131,12 @@ dmypy.json
# Pyre type checker
.pyre/
src/media/*
/media/*
/data/
/redis-data/
/static/admin/
/static/debug_toolbar/
/static/CACHE/
/media/
/nginx/certs/

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
FROM python:3.11-alpine3.17
RUN apk add build-base libpq-dev
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
LANG='en_US.UTF-8' LC_ALL='en_US.UTF-8' \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_VERSION=1.2.2
RUN pip install "poetry==$POETRY_VERSION"
WORKDIR /app
COPY poetry.lock pyproject.toml /app/
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
COPY . /app/

37
Pipfile
View File

@ -1,37 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
celery = {extras = ["redis"], version = "*"}
Django = "==4.0.2"
django-allauth = "*"
django-anymail = {extras = ["mailgun"], version = "*"}
django-celery-beat = "*"
django-celery-results = "*"
django-compressor = "*"
django-filter = "*"
django-measurement = "*"
django-setup-cli = "*"
django-storages = "*"
django-templated-email = "*"
paypal-checkout-serversdk = "*"
Pillow = "*"
redis = "*"
usps-api = "*"
psycopg2-binary = "*"
gunicorn = "*"
sentry-sdk = "*"
django-localflavor = "*"
django-analytical = "*"
django-simple-captcha = "*"
stripe = "*"
[dev-packages]
django-debug-toolbar = "*"
selenium = "*"
pycodestyle = "*"
[requires]
python_version = "3.10"

1097
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

27
README.md Normal file
View File

@ -0,0 +1,27 @@
```
sudo docker run -it --rm -p 80:80 --name certbot \
-v "/etc/letsencrypt:/etc/letsencrypt" \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
certbot/certbot certonly --standalone -d ptcoffee-dev.windmillapps.org
```
env settings
```
NGINX_CONF=dev.conf
DJANGO_SETTINGS_MODULE=ptcoffee.settings
DB_ENGINE=django.db.backends.postgresql
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
DB_HOST=db
DB_PORT=5432
CACHE_BACKEND=django.core.cache.backends.redis.RedisCache
CACHE_LOCATION=redis://redis:6379/0
STRIPE_API_KEY=***
PAYPAL_CLIENT_ID=***
PAYPAL_SECRET_ID=***
DEFAULT_FROM_EMAIL=debug@nathanjchapman.com
SERVER_EMAIL=debug@nathanjchapman.com
MAILGUN_API_KEY=***
MAILGUN_SENDER_DOMAIN=***
```

View File

@ -1,4 +1,4 @@
# Generated by Django 4.0.2 on 2022-03-11 02:25
# Generated by Django 4.1.5 on 2023-01-21 20:20
import django.contrib.auth.models
import django.contrib.auth.validators
@ -25,7 +25,7 @@ class Migration(migrations.Migration):
('street_address_1', models.CharField(blank=True, max_length=256)),
('street_address_2', models.CharField(blank=True, max_length=256)),
('city', models.CharField(blank=True, max_length=256)),
('state', 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'), ('ME', 'Maine'), ('MD', 'Maryland'), ('MA', 'Massachusetts'), ('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=2)),
('state', models.CharField(blank=True, choices=[('AL', 'Alabama'), ('AK', 'Alaska'), ('AS', 'American Samoa'), ('AZ', 'Arizona'), ('AR', 'Arkansas'), ('AA', 'Armed Forces Americas'), ('AE', 'Armed Forces Europe'), ('AP', 'Armed Forces Pacific'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DE', 'Delaware'), ('DC', 'District of Columbia'), ('FM', 'Federated States of Micronesia'), ('FL', 'Florida'), ('GA', 'Georgia'), ('GU', 'Guam'), ('HI', 'Hawaii'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('IA', 'Iowa'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('ME', 'Maine'), ('MH', 'Marshall Islands'), ('MD', 'Maryland'), ('MA', 'Massachusetts'), ('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'), ('MP', 'Northern Mariana Islands'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PW', 'Palau'), ('PA', 'Pennsylvania'), ('PR', 'Puerto Rico'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VT', 'Vermont'), ('VI', 'Virgin Islands'), ('VA', 'Virginia'), ('WA', 'Washington'), ('WV', 'West Virginia'), ('WI', 'Wisconsin'), ('WY', 'Wyoming')], max_length=2)),
('postal_code', models.CharField(blank=True, max_length=20)),
],
),
@ -43,11 +43,12 @@ class Migration(migrations.Migration):
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('addresses', models.ManyToManyField(blank=True, related_name='user_addresses', to='accounts.Address')),
('stripe_id', models.CharField(blank=True, max_length=255)),
('addresses', models.ManyToManyField(blank=True, related_name='user_addresses', to='accounts.address')),
('default_billing_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
('default_shipping_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',

View File

@ -9,7 +9,7 @@
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save changes" class="action-button"> or
<input type="submit" value="Save changes" class="btn"> or
<a href="{% url 'account-detail' user.id %}">Cancel</a>
</form>
</section>

14
core/apps.py Normal file
View File

@ -0,0 +1,14 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'
# def ready(self):
# from .signals import (
# order_created,
# transaction_created,
# order_line_post_save,
# trackingnumber_postsave
# )

View File

@ -9,7 +9,7 @@
"valid_to": "2054-05-31T06:00:00Z",
"discount_value_type": "percentage",
"discount_value": "10.00",
"products": [],
"variants": [],
"users": [1]
}
}, {
@ -23,7 +23,7 @@
"valid_to": "2022-04-30T06:00:00Z",
"discount_value_type": "percentage",
"discount_value": "10.00",
"products": [],
"variants": [],
"users": []
}
}]

View File

@ -7,7 +7,7 @@
"subtitle": "Camper Vacuum Mug",
"description": "Camp mug is made of double-wall stainless steel, designed to keep drinks hot or cold for hours.",
"checkout_limit": 20,
"visible_in_listings": false,
"visible_in_listings": true,
"sorting": 3,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -19,10 +19,10 @@
"product": 10,
"name": "Blue - 12 oz.",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -46,7 +46,7 @@
"subtitle": "Classic BLTC Logo front left and Steam/Waves on back",
"description": "ComfortBlend Hoodie - Screen. EcoSmart® 7.8-oz poly-cotton blend hoodie features a front pouch pocket and two-ply hood with matching drawcord. Rib-knit cuffs and waistband ensure all-day comfort and warmth.",
"checkout_limit": 20,
"visible_in_listings": false,
"visible_in_listings": true,
"sorting": 5,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -58,10 +58,10 @@
"product": 11,
"name": "Blue (Small)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -73,10 +73,10 @@
"product": 11,
"name": "Blue (Medium)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -88,10 +88,10 @@
"product": 11,
"name": "Blue (Large)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -122,7 +122,7 @@
"subtitle": "Logo T-Shirt with Steam/Waves on back",
"description": "5.5 oz. DryBlend 50/50 T-Shirt - Screen. The 5.5-oz 50/50 cotton/polyester blend fabric is moisture-wicking.",
"checkout_limit": 20,
"visible_in_listings": false,
"visible_in_listings": true,
"sorting": 7,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -134,10 +134,10 @@
"product": 12,
"name": "Heather Red (Small)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -149,10 +149,10 @@
"product": 12,
"name": "Heather Red (Medium)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -164,10 +164,10 @@
"product": 12,
"name": "Heather Red (Large)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -179,10 +179,10 @@
"product": 12,
"name": "Heather Blue (Small)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -194,10 +194,10 @@
"product": 12,
"name": "Heather Blue (Medium)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -209,10 +209,10 @@
"product": 12,
"name": "Heather Blue (Large)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -257,7 +257,7 @@
"subtitle": "Washed Cotton Mesh Back Cap with Logo",
"description": "Soft washed cotton gives this trucker hat an appealing vintage look. Designed with ventilated mesh back panels and a front twill sweatband for comfort.",
"checkout_limit": 20,
"visible_in_listings": false,
"visible_in_listings": true,
"sorting": 4,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -269,10 +269,10 @@
"product": 13,
"name": "Black and White",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -296,7 +296,7 @@
"subtitle": "Classic BLTC Cafe Logo without the extra branding",
"description": "Made from a 7.1-oz, 52/48 cotton/polyester blend. Includes a spacious hood with drawcords and metal grommets. Interior phone pocket with headphone cord port.",
"checkout_limit": 20,
"visible_in_listings": false,
"visible_in_listings": true,
"sorting": 6,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -308,10 +308,10 @@
"product": 14,
"name": "Forest Green/Silver Embroidered (Small)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -323,10 +323,10 @@
"product": 14,
"name": "Forest Green/Silver Embroidered (Medium)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -338,10 +338,10 @@
"product": 14,
"name": "Forest Green/Silver Embroidered (Large)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -353,10 +353,10 @@
"product": 14,
"name": "Navy Blue/Silver Embroidered (Small)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -368,10 +368,10 @@
"product": 14,
"name": "Navy Blue/Silver Embroidered (Medium)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -383,10 +383,10 @@
"product": 14,
"name": "Navy Blue/Silver Embroidered (Large)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -398,10 +398,10 @@
"product": 14,
"name": "Black/Gold Embroidered (Small)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -413,10 +413,10 @@
"product": 14,
"name": "Black/Gold Embroidered (Medium)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -428,10 +428,10 @@
"product": 14,
"name": "Black/Gold Embroidered (Large)",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -469,7 +469,7 @@
"subtitle": "Embroidered Buttonless Cap",
"description": "These caps feature a buttonless design that won't interfere with headsets/headphones. The low profile fit and adjustable fabric strap with hook-and-loop closure helps create a more comfortable fit.",
"checkout_limit": 20,
"visible_in_listings": false,
"visible_in_listings": true,
"sorting": 2,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -481,10 +481,10 @@
"product": 15,
"name": "Navy Blue with Red Embroidered Print",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -508,7 +508,7 @@
"subtitle": "Bean/Leaf Logo Neo Vacuum Insulated Cup",
"description": "Designed to keep drinks hot or cold for hours. A great cup for enjoying wine, tea, coffee, hot chocolate, and more!",
"checkout_limit": 20,
"visible_in_listings": false,
"visible_in_listings": true,
"sorting": 1,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -520,10 +520,10 @@
"product": 16,
"name": "Black and Gold 10 oz.",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"
@ -535,10 +535,10 @@
"product": 16,
"name": "White and Black 10 oz.",
"sku": "",
"price": "0.01",
"price": "25.00",
"weight": "00:oz",
"track_inventory": true,
"stock": 0,
"stock": 10,
"sorting": null,
"created_at": "2022-02-23T18:06:57.624Z",
"updated_at": "2022-02-23T18:06:57.624Z"

View File

@ -8,11 +8,11 @@
"billing_address": null,
"shipping_address": 1,
"coupon": null,
"subtotal_amount": "13.40",
"subtotal_amount": "26.80",
"coupon_amount": "0.00",
"shipping_total": "9.55",
"total_amount": "22.95",
"weight": "0.0:oz",
"total_amount": "36.35",
"weight": "32.0:oz",
"created_at": "2022-03-15T17:18:59.584Z",
"updated_at": "2022-03-15T17:18:59.584Z"
}
@ -25,11 +25,11 @@
"billing_address": null,
"shipping_address": 1,
"coupon": null,
"subtotal_amount": "13.40",
"subtotal_amount": "26.80",
"coupon_amount": "0.00",
"shipping_total": "9.55",
"total_amount": "22.95",
"weight": "0.0:oz",
"total_amount": "36.35",
"weight": "32.0:oz",
"created_at": "2022-03-15T17:22:18.440Z",
"updated_at": "2022-03-15T17:22:18.440Z"
}
@ -42,11 +42,11 @@
"billing_address": null,
"shipping_address": 1,
"coupon": null,
"subtotal_amount": "13.40",
"subtotal_amount": "26.80",
"coupon_amount": "0.00",
"shipping_total": "9.55",
"total_amount": "22.95",
"weight": "0.0:oz",
"total_amount": "36.35",
"weight": "32.0:oz",
"created_at": "2022-03-15T17:26:27.869Z",
"updated_at": "2022-03-15T17:26:27.869Z"
}
@ -58,7 +58,7 @@
"variant": 1,
"quantity": 2,
"quantity_fulfilled": 0,
"customer_note": "Grind: Whole Beans; ",
"customer_note": "Grind: Whole Beans",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "0.00"
@ -71,7 +71,7 @@
"variant": 1,
"quantity": 1,
"quantity_fulfilled": 1,
"customer_note": "Grind: Espresso; ",
"customer_note": "Grind: Espresso",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "0.00"
@ -84,7 +84,7 @@
"variant": 8,
"quantity": 1,
"quantity_fulfilled": 1,
"customer_note": "Grind: Whole Beans; ",
"customer_note": "Grind: Whole Beans",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "0.00"
@ -97,7 +97,7 @@
"variant": 7,
"quantity": 1,
"quantity_fulfilled": 1,
"customer_note": "Grind: Cone Drip; ",
"customer_note": "Grind: Cone Drip",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "0.00"
@ -110,7 +110,7 @@
"variant": 4,
"quantity": 1,
"quantity_fulfilled": 0,
"customer_note": "Grind: Percolator; ",
"customer_note": "Grind: Percolator",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "0.00"
@ -123,7 +123,7 @@
"variant": 6,
"quantity": 1,
"quantity_fulfilled": 0,
"customer_note": "Grind: Whole Beans; ",
"customer_note": "Grind: Whole Beans",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "0.00"

View File

@ -17,6 +17,8 @@
"default_shipping_rate": 1,
"free_shipping_min": "100.00",
"max_cart_quantity": 20,
"max_cart_weight": "20:lb"
"max_cart_weight": "20:lb",
"default_contact_email": "support@ptcoffee.com",
"order_from_email": "orders@ptcoffee.com"
}
}]

View File

@ -1,7 +1,6 @@
# Generated by Django 4.0.2 on 2022-10-16 02:36
# Generated by Django 4.1.5 on 2023-01-21 20:20
import core.weight
from decimal import Decimal
from django.conf import settings
import django.contrib.postgres.fields
import django.core.validators
@ -17,8 +16,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('accounts', '0003_user_stripe_id'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0001_initial'),
]
operations = [
@ -33,9 +32,10 @@ class Migration(migrations.Migration):
('valid_to', models.DateTimeField(blank=True, null=True)),
('discount_value_type', models.CharField(choices=[('fixed', 'USD'), ('percentage', '%')], default='fixed', max_length=10)),
('discount_value', models.DecimalField(decimal_places=2, max_digits=12)),
('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ('code',),
'ordering': ['-valid_from', '-valid_to', 'code'],
},
),
migrations.CreateModel(
@ -44,10 +44,11 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('draft', 'Draft'), ('unfulfilled', 'Unfulfilled'), ('partially_fulfilled', 'Partially fulfilled'), ('partially_returned', 'Partially returned'), ('returned', 'Returned'), ('fulfilled', 'Fulfilled'), ('canceled', 'Canceled')], default='unfulfilled', max_length=32)),
('subtotal_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('coupon_amount', models.CharField(blank=True, max_length=255)),
('coupon_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('shipping_total', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('weight', django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, null=True)),
('subscription_description', models.CharField(blank=True, max_length=500)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('billing_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
@ -56,7 +57,8 @@ class Migration(migrations.Migration):
('shipping_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
],
options={
'ordering': ('-created_at',),
'ordering': ['-created_at'],
'permissions': [('cancel_order', 'Can cancel order')],
},
),
migrations.CreateModel(
@ -88,31 +90,33 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Product Categories',
},
),
migrations.CreateModel(
name='ProductPhoto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='products/images')),
('sorting', models.PositiveIntegerField(blank=True, null=True)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
],
options={
'ordering': ['sorting'],
},
),
migrations.CreateModel(
name='ShippingRate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('shipping_provider', models.CharField(choices=[('USPS', 'USPS')], default='USPS', max_length=255)),
('name', models.CharField(max_length=255)),
('container', models.CharField(choices=[('LG FLAT RATE BOX', 'Flate Rate Box - Large'), ('MD FLAT RATE BOX', 'Flate Rate Box - Medium'), ('REGIONALRATEBOXA', 'Regional Rate Box A'), ('REGIONALRATEBOXB', 'Regional Rate Box B'), ('VARIABLE', 'Variable')], default='VARIABLE', max_length=255)),
('min_order_weight', models.PositiveIntegerField(blank=True, null=True)),
('max_order_weight', models.PositiveIntegerField(blank=True, null=True)),
('container', models.CharField(choices=[('PRIORITY', (('FLAT RATE ENVELOPE', 'Flat Rate Envelope'), ('LEGAL FLAT RATE ENVELOPE', 'Legal Flat Rate Envelope'), ('PADDED FLAT RATE ENVELOPE', 'Padded Flat Rate Envelope'), ('SM FLAT RATE ENVELOPE', 'Sm Flat Rate Envelope'), ('WINDOW FLAT RATE ENVELOPE', 'Window Flat Rate Envelope'), ('GIFT CARD FLAT RATE ENVELOPE', 'Gift Card Flat Rate Envelope'), ('SM FLAT RATE BOX', 'Sm Flat Rate Box'), ('MD FLAT RATE BOX', 'Md Flat Rate Box'), ('LG FLAT RATE BOX', 'Lg Flat Rate Box'), ('VARIABLE', 'Variable'))), ('PRIORITY COMMERCIAL', (('REGIONALRATEBOXA', 'Regional Rate Box A'), ('REGIONALRATEBOXB', 'Regional Rate Box B')))], default='VARIABLE', max_length=255)),
('min_order_weight', django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, null=True)),
('max_order_weight', django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, null=True)),
('is_selectable', models.BooleanField(default=True)),
],
options={
'ordering': ['min_order_weight'],
},
),
migrations.CreateModel(
name='SiteSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('usps_user_id', models.CharField(max_length=255)),
],
options={
'verbose_name': 'Site Settings',
'verbose_name_plural': 'Site Settings',
},
),
migrations.CreateModel(
name='Transaction',
fields=[
@ -141,44 +145,63 @@ class Migration(migrations.Migration):
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stripe_id', models.CharField(blank=True, max_length=255)),
('customer', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscription', to=settings.AUTH_USER_MODEL)),
('stripe_id', models.CharField(blank=True, db_index=True, max_length=255)),
('items', django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(blank=True, null=True), default=list, size=None)),
('metadata', models.JSONField(blank=True, null=True)),
('is_active', models.BooleanField(default=False)),
('total_weight', django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscriptions', to=settings.AUTH_USER_MODEL)),
('shipping_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
],
),
migrations.CreateModel(
name='SiteSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('usps_user_id', models.CharField(max_length=255)),
('free_shipping_min', models.DecimalField(blank=True, decimal_places=2, help_text='Minimum dollar amount in the cart subtotal to qualify for free shipping.', max_digits=12, null=True)),
('max_cart_quantity', models.PositiveIntegerField(blank=True, default=20, help_text='Maximum amount of items allowed in cart.', null=True)),
('max_cart_weight', django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, help_text='Maximum weight allowed for cart.', measurement=measurement.measures.mass.Mass, null=True)),
('default_contact_email', models.CharField(blank=True, max_length=255)),
('order_from_email', models.CharField(blank=True, max_length=255)),
('default_zip_origination', models.CharField(blank=True, default='98368', max_length=5)),
('default_shipping_rate', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.shippingrate')),
],
options={
'verbose_name': 'Site Settings',
'verbose_name_plural': 'Site Settings',
},
),
migrations.CreateModel(
name='ProductVariant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('sku', models.CharField(max_length=255, unique=True)),
('stripe_id', models.CharField(blank=True, max_length=255)),
('sku', models.CharField(blank=True, max_length=255)),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
('weight', django_measurement.models.MeasurementField(blank=True, measurement=measurement.measures.mass.Mass, null=True)),
('weight', django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, null=True)),
('visible_in_listings', models.BooleanField(default=True)),
('track_inventory', models.BooleanField(default=False)),
('stock', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)])),
('sorting', models.PositiveIntegerField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.productphoto')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='core.product')),
],
options={
'ordering': ['weight'],
'ordering': ['sorting', 'weight'],
},
),
migrations.CreateModel(
name='ProductPhoto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='products/images')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
],
),
migrations.CreateModel(
name='ProductOption',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('options', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), size=None)),
('products', models.ManyToManyField(related_name='options', to='core.Product')),
('products', models.ManyToManyField(related_name='options', to='core.product')),
],
),
migrations.AddField(
@ -195,19 +218,23 @@ class Migration(migrations.Migration):
('customer_note', models.TextField(blank=True, default='')),
('currency', models.CharField(default='USD', max_length=3)),
('unit_price', models.DecimalField(decimal_places=2, max_digits=12)),
('tax_rate', models.DecimalField(decimal_places=2, default=Decimal('0.0'), max_digits=5)),
('order', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='core.order')),
('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_lines', to='core.product')),
('variant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_lines', to='core.productvariant')),
],
),
migrations.AddField(
model_name='coupon',
name='products',
field=models.ManyToManyField(blank=True, to='core.Product'),
model_name='order',
name='subscription',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.subscription'),
),
migrations.AddField(
model_name='coupon',
name='users',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
name='variants',
field=models.ManyToManyField(blank=True, to='core.productvariant'),
),
migrations.AddIndex(
model_name='subscription',
index=models.Index(fields=['stripe_id'], name='core_subscr_stripe__08018b_idx'),
),
]

View File

@ -27,7 +27,6 @@ from . import (
ShippingProvider,
ShippingContainer
)
from .usps import build_usps_rate_request
from .weight import WeightUnits, zero_weight
logger = logging.getLogger(__name__)
@ -125,6 +124,7 @@ class Product(models.Model):
class ProductPhoto(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
image = models.ImageField(upload_to='products/images')
sorting = models.PositiveIntegerField(blank=True, null=True)
def __str__(self):
return f'{self.product.name} {self.image}'
@ -135,6 +135,9 @@ class ProductPhoto(models.Model):
super(ProductPhoto, self).delete(*args, **kwargs)
storage.delete(path)
class Meta:
ordering = ['sorting']
# def save(self, *args, **kwargs):
# super().save(*args, **kwargs)
@ -167,7 +170,6 @@ class ProductVariant(models.Model):
)
name = models.CharField(max_length=255)
sku = models.CharField(max_length=255, blank=True)
stripe_id = models.CharField(max_length=255, blank=True, db_index=True)
price = models.DecimalField(
max_digits=settings.DEFAULT_MAX_DIGITS,
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
@ -177,9 +179,11 @@ class ProductVariant(models.Model):
weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
default=zero_weight,
blank=True,
null=True
)
visible_in_listings = models.BooleanField(default=True)
track_inventory = models.BooleanField(default=False)
stock = models.IntegerField(
blank=True,
@ -198,9 +202,6 @@ class ProductVariant(models.Model):
class Meta:
ordering = ['sorting', 'weight']
indexes = [
models.Index(fields=['stripe_id'])
]
class ProductOption(models.Model):
@ -244,7 +245,7 @@ class Coupon(models.Model):
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
)
products = models.ManyToManyField(Product, blank=True)
variants = models.ManyToManyField(ProductVariant, blank=True)
users = models.ManyToManyField(User, blank=True)
class Meta:
@ -422,7 +423,10 @@ class Order(models.Model):
return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
class Meta:
ordering = ('-created_at',)
ordering = ['-created_at']
permissions = [
('cancel_order', 'Can cancel order'),
]
class Transaction(models.Model):
@ -477,9 +481,6 @@ class OrderLine(models.Model):
max_digits=settings.DEFAULT_MAX_DIGITS,
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
)
tax_rate = models.DecimalField(
max_digits=5, decimal_places=2, default=Decimal('0.0')
)
def get_total(self):
return self.unit_price * self.quantity
@ -654,6 +655,11 @@ class SiteSettings(SingletonBase):
null=True,
help_text='Maximum weight allowed for cart.'
)
default_contact_email = models.CharField(max_length=255, blank=True)
order_from_email = models.CharField(max_length=255, blank=True)
default_zip_origination = models.CharField(
max_length=5, blank=True, default='98368'
)
def __str__(self):
return 'Site Settings'

View File

@ -16,7 +16,6 @@ from core import (
ShippingProvider,
ShippingContainer
)
from core.usps import build_usps_rate_request
logger = logging.getLogger(__name__)
@ -57,7 +56,7 @@ def get_shipping_cost(total_weight, postal_code):
str(total_weight.lb), container, str(postal_code)
)
usps = USPSApi(settings.USPS_USER_ID, test=settings.DEBUG)
usps = USPSApi(SiteSettings.load().usps_user_id, test=settings.DEBUG)
try:
validation = usps.get_rate(usps_rate_request)
@ -82,3 +81,21 @@ def get_shipping_cost(total_weight, postal_code):
shipping_cost = Decimal(postage['CommercialRate'])
return shipping_cost
def build_usps_rate_request(weight, container, zip_destination):
service = ShippingContainer.get_shipping_service_from_container(container)
return \
{
'service': service,
'zip_origination': SiteSettings.load().default_zip_origination,
'zip_destination': zip_destination,
'pounds': weight,
'ounces': '0',
'container': container,
'width': '',
'length': '',
'height': '',
'girth': '',
'machinable': 'TRUE'
}

View File

@ -20,6 +20,16 @@ logger = logging.getLogger(__name__)
stripe.api_key = settings.STRIPE_API_KEY
def format_order_lines(lines):
for line in lines:
yield {
'variant': str(line.variant),
'customer_note': line.customer_note,
'quantity': line.quantity,
'unit_price': str(line.unit_price)
}
@receiver(post_save, sender=Order, dispatch_uid="order_created")
def order_created(sender, instance, created, **kwargs):
if created:
@ -37,7 +47,23 @@ def transaction_created(sender, instance, created, **kwargs):
order = {
'order_id': instance.order.pk,
'email': instance.order.customer.email,
'full_name': instance.order.customer.get_full_name()
'full_name': instance.order.customer.get_full_name(),
'subtotal_amount': str(instance.order.subtotal_amount),
'coupon_amount': str(instance.order.coupon_amount),
'shipping_total': str(instance.order.shipping_total),
'total_amount': str(instance.order.total_amount),
'shipping_address': {
'first_name': instance.order.shipping_address.first_name,
'last_name': instance.order.shipping_address.last_name,
'street_address_1': instance.order.shipping_address.street_address_1,
'street_address_2': instance.order.shipping_address.street_address_2,
'city': instance.order.shipping_address.city,
'state': instance.order.shipping_address.state,
'postal_code': instance.order.shipping_address.postal_code
},
'line_items': list(
format_order_lines(instance.order.lines.all())
)
}
send_order_confirmation_email.delay(order)
instance.confirmation_email_sent = True

View File

@ -1,11 +1,10 @@
from celery import shared_task
from celery.utils.log import get_task_logger
from django.conf import settings
from django.core.mail import EmailMessage, send_mail
from templated_email import send_templated_mail
from .models import Order
from .models import Order, SiteSettings
logger = get_task_logger(__name__)
@ -20,7 +19,7 @@ ORDER_REFUND_TEMPLATE = 'storefront/order_refund'
def send_order_confirmation_email(order):
send_templated_mail(
template_name=CONFIRM_ORDER_TEMPLATE,
from_email=settings.ORDER_FROM_EMAIL,
from_email=SiteSettings.load().order_from_email,
recipient_list=[order['email']],
context=order
)
@ -32,7 +31,7 @@ def send_order_confirmation_email(order):
def send_order_shipped_email(data):
send_templated_mail(
template_name=SHIP_ORDER_TEMPLATE,
from_email=settings.ORDER_FROM_EMAIL,
from_email=SiteSettings.load().order_from_email,
recipient_list=[data['email']],
context=data
)

View File

@ -5,6 +5,7 @@ import xmltodict
from lxml import etree
from usps import USPSApi as USPSApiBase
from django.conf import settings
from . import ShippingContainer
@ -21,7 +22,6 @@ class USPSApi(USPSApiBase):
class Rate:
def __init__(self, usps, request, **kwargs):
xml = etree.Element('RateV4Request', {'USERID': usps.api_user_id})
etree.SubElement(xml, 'Revision').text = '2'
@ -39,21 +39,3 @@ class Rate:
etree.SubElement(package, 'Machinable').text = request['machinable']
self.result = usps.send_request('rate', xml)
def build_usps_rate_request(weight, container, zip_destination):
service = ShippingContainer.get_shipping_service_from_container(container)
return \
{
'service': service,
'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
'zip_destination': zip_destination,
'pounds': weight,
'ounces': '0',
'container': container,
'width': '',
'length': '',
'height': '',
'girth': '',
'machinable': 'TRUE'
}

View File

@ -23,6 +23,7 @@ class ProductVariantUpdateForm(forms.ModelForm):
'sku',
'price',
'weight',
'visible_in_listings',
'track_inventory',
'stock',
'sorting',
@ -48,7 +49,7 @@ class CouponForm(forms.ModelForm):
'valid_to',
'discount_value_type',
'discount_value',
'products',
'variants',
)
widgets = {
'valid_from': forms.DateInput(attrs={
@ -97,7 +98,10 @@ class OrderTrackingForm(forms.ModelForm):
class Meta:
model = TrackingNumber
fields = ('tracking_id',)
fields = ['tracking_id']
labels = {
'tracking_id': 'Tracking Number'
}
OrderTrackingFormset = forms.inlineformset_factory(

View File

@ -0,0 +1,62 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Catalog | {% endblock %}
{% block content %}
<article>
<header class="object-header">
<h1><img src="{% static 'images/cubes.png' %}"> Catalog</h1>
<div class="object-menu">
<a href="{% url 'dashboard:category-create' %}" class="btn">+ New category</a>
<a href="{% url 'dashboard:product-create' %}" class="btn">+ New product</a>
</div>
</header>
{% for category in category_list %}
<section class="panel">
<header class="panel-header">
<h3>{{ category }}</h3>
<a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category &rarr;</a>
</header>
{% include 'dashboard/product/_table.html' with product_list=category.product_set.all %}
</section>
{% endfor %}
{% if uncategorized_products|length > 0 %}
<section class="panel">
<header class="panel-header">
<h4>Uncategorized Products</h4>
</header>
{% include 'dashboard/product/_table.html' with product_list=uncategorized_products %}
</section>
{% endif %}
<section class="panel">
<header class="panel-header">
<h4>Product Options</h4>
<a href="{% url 'dashboard:option-create' %}" class="btn">+ New product option</a>
</header>
<table>
<thead>
<tr>
<th>Name</th>
<th>Values</th>
</tr>
</thead>
<tbody>
{% for option in option_list %}
<tr class="is-link" onclick="window.location='{% url 'dashboard:option-detail' option.pk %}'">
<th>{{ option.name }}</th>
<td>
{% for val in option.options %}
{{ val }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,9 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Delete {{ category }} | {% endblock %}
{% block content %}
{% url 'dashboard:category-detail' category.pk as back_url %}
{% include 'dashboard/partials/_delete_form.html' with back_url=back_url object=category form=form %}
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}New category | {% endblock %}
{% block content %}
{% url 'dashboard:catalog' as back_url %}
{% include 'dashboard/partials/_create_form.html' with back_url=back_url object='category' form=form %}
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}{{ category }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:catalog' %}">&larr; Back to catalog</a>
</p>
<header class="object-header">
<div>
<h1><img src="{% static 'images/cubes.png' %}"> {{ category.name }}</h1>
<p><strong>Is a main category</strong>: {{ category.main_category|yesno:"Yes,No" }}</p>
</div>
<div class="object-menu">
{% if perms.core.delete_productcategory %}
<a href="{% url 'dashboard:category-delete' category.pk %}" class="btn btn-warning">Delete</a>
{% endif %}
{% if perms.core.change_productcategory %}
<a href="{% url 'dashboard:category-update' category.pk %}" class="btn">Edit</a>
{% endif %}
</div>
</header>
<section class="panel">
<header class="panel-header">
<h4>Products</h4>
</header>
{% include 'dashboard/product/_table.html' with product_list=category.product_set.all %}
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}Update {{ category }} | {% endblock %}
{% block content %}
{% url 'dashboard:category-detail' category.pk as back_url %}
{% include 'dashboard/partials/_form.html' with back_url=back_url object=category form=form %}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Categories | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:catalog' %}">&larr; Back to catalog</a>
</p>
<header class="object-header">
<h1><img src="{% static 'images/cubes.png' %}"> Categories</h1>
<div class="object-menu">
<a href="{% url 'dashboard:category-create' %}" class="btn">+ New category</a>
</div>
</header>
{% for category in category_list %}
<section class="panel">
<header class="panel-header">
<h3>{{ category }}</h3>
<a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category &rarr;</a>
</header>
{% include 'dashboard/product/_table.html' with product_list=category.product_set.all %}
</section>
{% endfor %}
</article>
{% endblock content %}

View File

@ -0,0 +1,67 @@
{% extends 'dashboard.html' %}
{% load static %}
{% load tz %}
{% block head_title %}Site Configuration | {% endblock %}
{% block content %}
<article>
<header class="object-header">
<h1><img src="{% static 'images/gear.png' %}"> Site configuration</h1>
</header>
<section class="panel">
<header class="panel-header">
<h4>Site Settings</h4>
{% if perms.core.change_sitesettings %}
<a href="{% url 'dashboard:settings-update' site_settings.pk %}" class="btn">Edit</a>
{% endif %}
</header>
<dl class="panel-datalist">
<dt>USPS User ID</dt>
<dd>{{ site_settings.usps_user_id }}</dd>
<dt>Default shipping rate</dt>
<dd>{{ site_settings.default_shipping_rate }}</dd>
<dt>Free shipping min</dt>
<dd>${{ site_settings.free_shipping_min }}</dd>
<dt>Max cart quantity</dt>
<dd>{{ site_settings.max_cart_quantity }} items</dd>
<dt>Max cart weight</dt>
<dd>{{ site_settings.max_cart_weight }}</dd>
<dt>Default contact email</dt>
<dd>{{ site_settings.default_contact_email }}</dd>
<dt>Default order-from email</dt>
<dd>{{ site_settings.order_from_email }}</dd>
</dl>
</section>
<section class="panel">
<header class="panel-header">
<h4>Shipping rates</h4>
<a href="{% url 'dashboard:rate-create' %}" class="btn">+ New rate</a>
</header>
<table>
<thead>
<tr>
<th>Name</th>
<th>Shipping Provider</th>
<th>Container</th>
<th>Weight range</th>
</tr>
</thead>
<tbody>
{% for rate in shipping_rate_list %}
<tr class="is-link" onclick="window.location='{% url 'dashboard:rate-detail' rate.pk %}'">
<td>{{ rate }}</td>
<td>{{ rate.shipping_provider }}</td>
<td>{{ rate.get_container_display }}</td>
<td>{{ rate.min_order_weight }} &ndash; {{ rate.max_order_weight }}</td>
</tr>
{% empty %}
<p>No shipping rates yet.</p>
{% endfor %}
</tbody>
</table>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Delete {{ coupon }} | {% endblock %}
{% block content %}
{% url 'dashboard:coupon-detail' coupon.pk as back_url %}
{% include 'dashboard/partials/_delete_form.html' with back_url=back_url object=coupon form=form %}
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}New coupon | {% endblock %}
{% block content %}
{% url 'dashboard:coupon-list' as back_url %}
{% include 'dashboard/partials/_create_form.html' with back_url=back_url object='coupon' form=form %}
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}{{ coupon }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:coupon-list' %}">&larr; Back to coupons</a>
</p>
<header class="object-header">
<h1><img src="{% static 'images/coupon.png' %}"> {{ coupon.name }}</h1>
<div class="object-menu">
{% if perms.core.delete_coupon %}
<a href="{% url 'dashboard:coupon-delete' coupon.pk %}" class="btn btn-warning">Delete</a>
{% endif %}
{% if perms.core.change_coupon %}
<a href="{% url 'dashboard:coupon-update' coupon.pk %}" class="btn">Edit</a>
{% endif %}
</div>
</header>
<section class="panel">
<header class="panel-header">
<h4>Details</h4>
</header>
<dl class="panel-datalist">
<dt>Applies to</dt>
<dd>{{ coupon.get_type_display }}</dd>
<dt>Code</dt>
<dd>{{ coupon.code }}</dd>
<dt>Valid from</dt>
<dd>{{ coupon.valid_from }}</dd>
<dt>Valid to</dt>
<dd>{{ coupon.valid_to }}</dd>
<dt>Discount value</dt>
<dd>{{ coupon.discount_value }} {{ coupon.get_discount_value_type_display }}</dd>
</dl>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}Update {{ coupon }} | {% endblock %}
{% block content %}
{% url 'dashboard:coupon-detail' coupon.pk as back_url %}
{% include 'dashboard/partials/_form.html' with back_url=back_url object=coupon form=form %}
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Coupons | {% endblock %}
{% block content %}
<article>
<header class="object-header">
<h1><img src="{% static 'images/coupon.png' %}"> Coupons</h1>
<a href="{% url 'dashboard:coupon-create' %}" class="btn">+ New coupon</a>
</header>
<section class="panel">
<header class="panel-header">
<h4></h4>
</header>
<table>
<thead>
<th>Name</th>
<th>Code</th>
<th>Valid</th>
<th>Starts</th>
<th>Ends</th>
<th>Value</th>
</thead>
<tbody>
{% for coupon in coupon_list %}
<tr class="is-link" onclick="window.location='{% url 'dashboard:coupon-detail' coupon.pk %}'">
<td>{{ coupon.name }}</td>
<td>{{ coupon.code }}</td>
<td>
<div class="status-display">
{% if coupon.is_valid %}
<span class="status-dot status-success"></span> Valid
{% else %}
<span class="status-dot status-info"></span> Invalid
{% endif %}
</div>
</td>
<td>{{ coupon.valid_from|date:"SHORT_DATE_FORMAT" }}</td>
<td>{{ coupon.valid_to|date:"SHORT_DATE_FORMAT" }}</td>
<td>{{ coupon.discount_value }} {{ coupon.get_discount_value_type_display }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5">No coupons.</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,50 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}{{ customer.get_full_name }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:customer-list' %}">&larr; Back to customers</a>
</p>
<header class="object-header">
<h1><img src="{% static "images/customer.png" %}"> Customer: {{customer.get_full_name}}</h1>
{% if perms.core.change_customer %}
<a href="{% url 'dashboard:customer-update' customer.pk %}" class="btn">Edit</a>
{% endif %}
</header>
<section class="panel">
<header class="panel-header">
<h4>Details</h4>
</header>
<dl class="panel-datalist">
<dt>Primary email address</dt>
<dd>
<a href="mailto:{{ customer.email }}">{{ customer.email }} &nearr;</a>
</dd>
<dt>Default shipping address</dt>
<dd>
{% include 'dashboard/partials/_address.html' with address=customer.default_shipping_address %}
</dd>
<dt>All addresses</dt>
<dd>
{% for address in customer.addresses.all %}
{% include 'dashboard/partials/_address.html' with address=address %}
{% endfor %}
</dd>
</dl>
</section>
<section class="panel">
<header class="panel-header">
<h4>Orders</h4>
</header>
<table>
{% include 'dashboard/order/_table.html' with order_list=customer.orders.all %}
</table>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}Update {{ customer.get_full_name }} | {% endblock %}
{% block content %}
{% url 'dashboard:customer-detail' customer.pk as back_url %}
{% include 'dashboard/partials/_form.html' with back_url=back_url object=customer form=form %}
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Customers | {% endblock %}
{% block content %}
<article>
<header class="object-header">
<h1><img src="{% static 'images/customer.png' %}"> Customers</h1>
</header>
<section class="panel">
<header class="panel-header">
<h4></h4>
</header>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Orders</th>
</tr>
</thead>
<tbody>
{% for customer in user_list %}
<tr class="is-link" onclick="window.location='{% url 'dashboard:customer-detail' customer.pk %}'">
<td>{{customer.get_full_name}}</td>
<td>{{customer.email}}</td>
<td>{{customer.num_orders}}</td>
</tr>
{% empty %}
<tr>
<td colspan="3">No customers</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td>
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</td>
</tr>
</tfoot>
</table>
</section>
</article>
{% endblock content %}

View File

@ -1,11 +1,13 @@
{% extends "dashboard.html" %}
{% extends 'dashboard.html' %}
{% load static %}
{% load tz %}
{% block head_title %}Home | {% endblock %}
{% block content %}
<article>
<h1><img src="{% static "images/store.png" %}" alt=""> Port Townsend Coffee</h1>
<section class="store__info">
<h1><img src="{% static "images/store.png" %}"> Port Townsend Roasting Co.</h1>
<section class="store-info">
<div class="orders">
<h5>Orders</h5>
<small>Today {% now "" %}</small>
@ -19,7 +21,7 @@
</section>
<section>
<a class="store__action" href="{% url 'dashboard:order-list' %}?status=unfulfilled">{{orders_unfulfilled}} orders ready to fulfill &rarr;</a>
<a class="store-action" href="{% url 'dashboard:order-list' %}?status=unfulfilled">{{ orders_unfulfilled }} orders ready to fulfill &rarr;</a>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Delete {{ option }} | {% endblock %}
{% block content %}
{% url 'dashboard:option-detail' option.pk as back_url %}
{% include 'dashboard/partials/_delete_form.html' with back_url=back_url object=option form=form %}
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}New option | {% endblock %}
{% block content %}
{% url 'dashboard:catalog' as back_url %}
{% include 'dashboard/partials/_create_form.html' with back_url=back_url object='option' form=form %}
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}{{ option }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:catalog' %}">&larr; Back to catalog</a>
</p>
<header class="object-header">
<h1>Product Option: {{ option.name }}</h1>
<div class="object-menu">
{% if perms.core.delete_productoption %}
<a href="{% url 'dashboard:option-delete' option.pk %}" class="btn btn-warning">Delete</a>
{% endif %}
{% if perms.core.change_productoption %}
<a href="{% url 'dashboard:option-update' option.pk %}" class="btn">Edit</a>
{% endif %}
</div>
</header>
<section class="panel">
<header class="panel-header">
<h4>Products</h4>
</header>
<ul>
{% for product in option.products.all %}
<li><a href="{% url 'dashboard:product-detail' product.pk %}">{{ product.name }}</a></li>
{% endfor %}
</ul>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}Update {{ option }} | {% endblock %}
{% block content %}
{% url 'dashboard:option-detail' option.pk as back_url %}
{% include 'dashboard/partials/_form.html' with back_url=back_url object=option form=form %}
{% endblock %}

View File

@ -0,0 +1,24 @@
<thead>
<tr>
<th>Order No.</th>
<th>Date</th>
<th>Customer</th>
<th>Status</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for order in order_list %}
<tr class="is-link" onclick="window.location='{% url 'dashboard:order-detail' order.pk %}'">
<td>No. {{order.pk}} {% if order.subscription %}(subscription){% endif %}</td>
<td>{{order.created_at|date:"D, M j Y"}}</td>
<td>{{order.customer.get_full_name}}</td>
<td>
<div class="status-display">
<span class="status-dot status-{{order.status}}"></span> {{order.get_status_display}}
</div>
</td>
<td>${{order.total_amount}}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -0,0 +1,26 @@
{% extends 'dashboard.html' %}
{% block head_title %}Cancel Order No. {{ order.pk }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:order-detail' order.pk %}">&larr; Back</a>
</p>
<header>
<h1>Cancel Order No. {{order.pk}}</h1>
</header>
<section class="panel">
<header class="panel-header">
<h4>Are you sure you want to cancel Order No. {{ order.pk }}?</h4>
</header>
<form method="POST" class="panel-form">
{% csrf_token %}
{{ form.as_p }}
<p>
<input class="btn btn-warning" type="submit" value="Cancel order">
</p>
</form>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,163 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Order No. {{ order.pk }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:order-list' %}">&larr; Back to orders</a>
</p>
<header class="object-header">
<h1><img src="{% static 'images/box.png' %}"> Order No. {{order.pk}}</h1>
{% if perms.core.cancel_order %}
<a class="btn btn-warning" href="{% url 'dashboard:order-cancel' order.pk %}">Cancel order</a>
{% endif %}
</header>
<section class="panel">
<header class="panel-header">
<h4>Details</h4>
</header>
<dl class="panel-datalist">
<dt>Date</dt>
<dd>{{ order.created_at }}</dd>
<dt>Customer</dt>
<dd>
<a href="{% url 'dashboard:customer-detail' order.customer.pk %}">{{order.customer.get_full_name}}</a>&emsp;
<a href="mailto:{{order.customer.email}}">{{order.customer.email}} &nearr;</a>
</dd>
{% if order.subscription %}
<dt>Subscription</dt>
<dd>
{{ order.subscription_description }}&emsp;<a href="https://dashboard.stripe.com/subscriptions/{{ order.subscription.stripe_id }}" target="_blank">View on Stripe &nearr;</a>
</dd>
{% else %}
<dt>PayPal Transaction</dt>
<dd>
{{order.transaction.get_status_display}}&emsp;<a href="https://www.paypal.com/activity/payment/{{ order.transaction.paypal_id }}" target="_blank">View on PayPal &nearr;</a>
</dd>
{% endif %}
<dt>Status</dt>
<dd>
<span class="status status-{{order.status}}">{{order.get_status_display}} ({{order.total_quantity_fulfilled}} / {{order.total_quantity_ordered}})</span>
</dd>
</dl>
</section>
<section class="panel">
<header class="panel-header">
<h4>Items</h4>
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="btn">Fulfill order &rarr;</a>
</header>
<table>
<thead>
<tr>
<th>Product</th>
<th>SKU</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for line in order.lines.all %}
<tr>
{% if line.variant %}
{% with product=line.variant.product %}
<td>
<figure class="product-figure">
{% if line.variant.image %}
<img class="product-image" src="{{line.variant.image.image.url}}" alt="{{line.variant.image.image}}">
{% else %}
<img class="product-image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
{% endif %}
<figcaption><strong>{{line.variant}}</strong><br>{{line.customer_note}}</figcaption>
</figure>
</td>
<td>{{product.sku}}</td>
<td>{{line.quantity}}</td>
<td>${{line.unit_price}}</td>
<td>${{line.get_total}}</td>
{% endwith %}
{% elif line.product %}
{% with product=line.product %}
<td>
<figure class="product-figure">
{% if line.variant.image %}
<img class="product-image" src="{{line.variant.image.image.url}}" alt="{{line.variant.image.image}}">
{% else %}
<img class="product-image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
{% endif %}
<figcaption><strong>{{line.product}}</strong><br>{{line.customer_note}}</figcaption>
</figure>
</td>
<td>{{product.sku}}</td>
<td>{{line.quantity}}</td>
<td>${{line.unit_price}}</td>
<td>${{line.get_total}}</td>
{% endwith %}
{% endif %}
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<th colspan="4">Subtotal:</th>
<td>${{order.subtotal_amount}}</td>
</tr>
{% if order.coupon_amount > 0 %}
<tr>
<th colspan="4">Discount:</th>
<td>${{ order.coupon_amount }}</td>
</tr>
{% endif %}
<tr>
<th colspan="4">Shipping:</th>
<td>${{ order.shipping_total }}</td>
</tr>
<tr>
<th colspan="4">Total:</th>
<td>${{ order.total_amount }}</td>
</tr>
</tfoot>
</table>
</section>
<section class="panel">
<header class="panel-header">
<h4>Shipping</h4>
<a href="{% url 'dashboard:order-ship' order.pk %}" class="btn">Add tracking</a>
</header>
<div class="panel-section panel-shipping">
<div>
<strong>Shipping address</strong>
{% include 'dashboard/partials/_address.html' with address=order.shipping_address %}
</div>
<table>
<thead>
<tr>
<th>Date</th>
<th>Tracking Number</th>
</tr>
</thead>
<tbody>
{% for number in order.tracking_numbers.all %}
<tr>
<td>{{number.created_at|date:"SHORT_DATE_FORMAT" }}</td>
<td>
<a href="https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={{ number.tracking_id }}" target="_blank">{{number.tracking_id}} &nearr;</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2">No tracking information.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,90 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Fulfill Order No. {{ order.pk }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:order-detail' order.pk %}">&larr; Back to order</a>
</p>
<header class="object-header">
<h1><img src="{% static 'images/box.png' %}"> Order No. {{order.pk}}</h1>
</header>
<section class="panel">
<header class="panel-header">
<h3>Fulfillment</h3>
</header>
<form method="POST">
{% csrf_token %}
{{ form.management_form }}
<table>
<thead>
<tr>
<th>Product</th>
<th>SKU</th>
<th>Options</th>
<th>Quantity to fulfill</th>
</tr>
</thead>
<tbody>
{% for form in form %}
<tr>
{% if form.instance.variant %}
{% with product=form.instance.variant.product %}
{{form.id}}
<td>
<figure class="product-figure">
{% if item.variant.image %}
<img class="product-image" src="{{item.variant.image.image.url}}" alt="{{item.variant.image.image}}">
{% else %}
<img class="product-image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
{% endif %}
<figcaption><strong>{{form.instance.variant}}</strong></figcaption>
</figure>
</td>
<td>{{product.sku}}</td>
<td>{{form.instance.customer_note}}</td>
<td>{{form.quantity_fulfilled}} / {{form.instance.quantity}}</td>
{% endwith %}
{% elif form.instance.product %}
{% with product=form.instance.product %}
{{form.id}}
<td>
<figure class="product-figure">
{% if item.variant.image %}
<img class="product-image" src="{{item.variant.image.image.url}}" alt="{{item.variant.image.image}}">
{% else %}
<img class="product-image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
{% endif %}
<figcaption><strong>{{form.instance.product}}</strong></figcaption>
</figure>
</td>
<td>{{product.sku}}</td>
<td>{{form.instance.customer_note}}</td>
<td>{{form.quantity_fulfilled}} / {{form.instance.quantity}}</td>
{% endwith %}
{% endif %}
</tr>
{% endfor %}
</tbody>
<tfoot>
{% for dict in form.errors %}
<tr>
{% for error in dict.values %}
<td>{{ error }}</td>
{% endfor %}
</tr>
{% endfor %}
<tr>
<td colspan="3"></td>
<td class="text-right">
<input class="btn" type="submit" value="Fulfill order">
</td>
</tr>
</tfoot>
</table>
</form>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,39 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Orders | {% endblock %}
{% block content %}
<article>
<header>
<h1><img src="{% static "images/box.png" %}"> Orders</h1>
</header>
<section class="panel">
<header class="panel-header">
<h3></h3>
</header>
<table>
{% include 'dashboard/order/_table.html' with order_list=order_list %}
<tfoot>
<tr>
<td>
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</td>
</tr>
</tfoot>
</table>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,49 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Ship Order No. {{ order.pk }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:order-detail' order.pk %}">&larr; Back to order</a>
</p>
<header class="object-header">
<h1><img src="{% static 'images/box.png' %}"> Order No. {{order.pk}}</h1>
</header>
<section class="panel">
<header class="panel-header">
<h3>Add Tracking</h3>
</header>
<div class="panel-section">
<strong>Shipping address</strong>
{% include 'dashboard/partials/_address.html' with address=order.shipping_address %}
</div>
<form method="POST">
{% csrf_token %}
{{ form.management_form }}
<table>
<tbody>
{% for formitem in form %}
{{formitem}}
{% endfor %}
</tbody>
<tfoot>
{% for dict in form.errors %}
<tr>
{% for error in dict.values %}
<td>{{ error }}</td>
{% endfor %}
</tr>
{% endfor %}
<tr>
<td colspan="2" class="text-right">
<button type="submit" class="btn">Ship order and send tracking info to customer <img src="{% static 'images/paper_plane.png' %}" class="inline-image"></button>
</td>
</tr>
</tfoot>
</table>
</form>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,9 @@
<address>
{{address.first_name}}
{{address.last_name}}<br>
{{address.street_address_1}}<br>
{% if address.street_address_2 %}
{{address.street_address_2}}<br>
{% endif %}
{{address.city}}, {{address.state}}, {{address.postal_code}}
</address>

View File

@ -0,0 +1,17 @@
<article>
<p>
<a href="{{ back_url }}">&larr; Back</a>
</p>
<header>
<h1>New {{ object }}</h1>
</header>
<section class="panel">
<form method="POST" class="panel-form">
{% csrf_token %}
{{form.as_p}}
<p>
<button type="submit">Create {{ object }}</button>
</p>
</form>
</section>
</article>

View File

@ -0,0 +1,20 @@
<article>
<p>
<a href="{{ back_url }}">&larr; Back</a>
</p>
<header>
<h1>Delete: {{ object }}</h1>
</header>
<section class="panel">
<header class="panel-header">
<h4>Are you sure you want to delete "{{ object }}"?</h4>
</header>
<form method="POST" class="panel-form">
{% csrf_token %}
{{ form.as_p }}
<p>
<button class="btn-warning" type="submit">Confirm</button>
</p>
</form>
</section>
</article>

View File

@ -0,0 +1,17 @@
<article>
<p>
<a href="{{ back_url }}">&larr; Back</a>
</p>
<header>
<h1>Update {{ object }}</h1>
</header>
<section class="panel">
<form method="POST" class="panel-form">
{% csrf_token %}
{{form.as_p}}
<p>
<button type="submit">Save changes</button>
</p>
</form>
</section>
</article>

View File

@ -0,0 +1,35 @@
<table>
<thead>
<tr>
<th>Sorting</th>
<th colspan="2">Product</th>
<th>Visible in listings</th>
</tr>
</thead>
<tbody>
{% for product in product_list %}
<tr class="is-link" onclick="window.location='{% url 'dashboard:product-detail' product.pk %}'">
<td>{{ product.sorting }}</td>
<td>
<figure class="product-figure">
<img class="product-image" src="{{ product.get_first_img.image.url }}" alt="{{ product.get_first_img.image }}">
</figure>
</td>
<td>
<h4>{{ product.name }}</h4>
</td>
<td>
<div class="status-display">
{% if product.visible_in_listings %}
<span class="status-dot status-success"></span> Visible in listings
{% else %}
<span class="status-dot status-info"></span> Not visible in listings
{% endif %}
</div>
</td>
<td>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,9 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Delete {{ product }} | {% endblock %}
{% block content %}
{% url 'dashboard:product-detail' product.pk as back_url %}
{% include 'dashboard/partials/_delete_form.html' with back_url=back_url object=product form=form %}
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}New product | {% endblock %}
{% block content %}
{% url 'dashboard:catalog' as back_url %}
{% include 'dashboard/partials/_create_form.html' with back_url=back_url object='product' form=form %}
{% endblock %}

View File

@ -0,0 +1,142 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}{{ product }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:catalog' %}">&larr; Back to catalog</a>
</p>
<header class="object-header">
<h1><img src="{% static "images/cubes.png" %}"> {{product.name}}</h1>
<div class="object-menu">
{% if perms.core.delete_product %}
<a href="{% url 'dashboard:product-delete' product.pk %}" class="btn btn-warning">Delete</a>
{% endif %}
{% if perms.core.change_product %}
<a href="{% url 'dashboard:product-update' product.pk %}" class="btn">Edit</a>
{% endif %}
</div>
</header>
<section class="panel">
<header class="panel-header">
<h4>Details</h4>
</header>
<dl class="panel-datalist">
<dt>Category</dt>
<dd>{{ product.category }}</dd>
<dt>Ordering</dt>
<dd>{{ product.sorting }}</dd>
<dt>Subtitle</dt>
<dd><h5>{{ product.subtitle }}</h5></dd>
<dt>Description</dt>
<dd>{{ product.description }}</dd>
<dt>Checkout limit</dt>
<dd><strong>{{ product.checkout_limit }}</strong></dd>
<dt>Visibility</dt>
<dd>
{% if product.visible_in_listings %}
<span class="status status-success">Visible in listings</span>
{% else %}
<span class="status status-info">Not visible in listings</span>
{% endif %}
</dd>
<dt>Applied options</dt>
<dd>
<p><em>Edit product options on the <a href="{% url 'dashboard:catalog' %}">catalog page</a></em></p>
{% for option in product.options.all %}
<h3>{{ option.name }}</h3>
<p>
{% for val in option.options %}
{{ val }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endfor %}
</dd>
</dl>
</section>
<section class="panel">
<header class="panel-header">
<h4>Varaints</h4>
<a href="{% url 'dashboard:variant-create' product.pk %}" class="btn">+ New variant</a>
</header>
<table>
<thead>
<tr>
<th>Sorting</th>
<th>Name</th>
<th>Visibility</th>
<th>SKU</th>
<th>Price</th>
<th>Weight</th>
<th colspan="2">Stock</th>
</tr>
</thead>
<tbody>
{% for variant in product.variants.all %}
<tr>
<td>{{ variant.sorting }}</td>
<td>
<h3>{{ variant.name }}</h3>
</td>
<td>
<div class="status-display">
{% if product.visible_in_listings and variant.visible_in_listings %}
<span class="status-dot status-success"></span>Visible in listings
{% elif not product.visible_in_listings and variant.visible_in_listings %}
<span class="status-dot status-warning"></span>Not visible because product
{% else %}
<span class="status-dot status-info"></span>Not visible in listings
{% endif %}
</div>
</td>
<td>{{ variant.sku }}</td>
<td>${{ variant.price }}</td>
<td>{{ variant.weight }}</td>
<td>
{% if variant.track_inventory %}
{{ variant.stock }}
{% else %}
N/A
{% endif %}
</td>
<td>
{% if perms.core.change_productvariant %}
<a href="{% url 'dashboard:variant-update' product.pk variant.pk %}">Edit</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="panel">
<header class="panel-header">
<h4>Photos</h4>
<a href="{% url 'dashboard:prodphoto-create' product.pk %}" class="btn">+ Upload new photo</a>
</header>
<div class="gallery panel-section">
{% for photo in product.productphoto_set.all %}
<figure class="gallery-item">
<img src="{{ photo.image.url }}">
<figcaption>
<form action="{% url 'dashboard:prodphoto-delete' product.pk photo.pk %}" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-warning" value="Delete photo">
</form>
</figcaption>
</figure>
{% endfor %}
</div>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}Update {{ product }} | {% endblock %}
{% block content %}
{% url 'dashboard:product-detail' product.pk as back_url %}
{% include 'dashboard/partials/_form.html' with back_url=back_url object=product form=form %}
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Products | {% endblock %}
{% block content %}
<article>
<header class="object-header">
<h1><img src="{% static 'images/cubes.png' %}"> Catalog</h1>
<div class="header-menu">
<a href="{% url 'dashboard:category-create' %}" class="btn">+ New category</a>
<a href="{% url 'dashboard:product-create' %}" class="btn">+ New product</a>
</div>
</header>
<section class="panel">
<header class="panel-header">
<h4>Products</h4>
</header>
{% include 'dashboard/product/_table.html' with product_list=product_list %}
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,23 @@
{% extends 'dashboard.html' %}
{% block head_title %}New Product Photo | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:product-detail' product.pk %}">&larr; Back to product</a>
</p>
<header>
<h1>Add photo to {{product.name}}</h1>
</header>
<section class="panel">
<form enctype="multipart/form-data" method="POST" class="panel-form">
{% csrf_token %}
{{form.as_p}}
<p>
<input class="btn" type="submit" value="Add photo">
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}Delete {{ rate }} | {% endblock %}
{% block content %}
{% url 'dashboard:rate-detail' rate.pk as back_url %}
{% include 'dashboard/partials/_delete_form.html' with back_url=back_url object=rate form=form %}
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}New Shipping Rate | {% endblock %}
{% block content %}
{% url 'dashboard:config' as back_url %}
{% include 'dashboard/partials/_create_form.html' with back_url=back_url object='rate' form=form %}
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends 'dashboard.html' %}
{% load static %}
{% block head_title %}{{ rate }} | {% endblock %}
{% block content %}
<article>
<p>
<a href="{% url 'dashboard:config' %}">&larr; Back to site configuration</a>
</p>
<header class="object-header">
<h1><img src="{% static 'images/gear.png' %}"> Shipping Rate</h1>
<div class="object-menu">
{% if perms.core.delete_shippingrate %}
<a href="{% url 'dashboard:rate-delete' rate.pk %}" class="btn btn-warning">Delete</a>
{% endif %}
{% if perms.core.change_shippingrate %}
<a href="{% url 'dashboard:rate-update' rate.pk %}" class="btn">Edit</a>
{% endif %}
</div>
</header>
<section class="panel">
<header class="panel-header">
<h4>{{rate.name}}</h4>
</header>
<dl class="panel-datalist">
<dt>Shipping Provider</dt>
<dd>{{ rate.shipping_provider }}</dd>
<dt>Container</dt>
<dd>{{ rate.get_container_display }}</dd>
<dt>Weight range</dt>
<dd>{{ rate.min_order_weight }} &ndash; {{ rate.max_order_weight }}</dd>
</dl>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}Update {{ rate }} | {% endblock %}
{% block content %}
{% url 'dashboard:rate-detail' rate.pk as back_url %}
{% include 'dashboard/partials/_form.html' with back_url=back_url object=rate form=form %}
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends 'dashboard.html' %}
{% block head_title %}Update Site Settings | {% endblock %}
{% block content %}
{% url 'dashboard:config' as back_url %}
{% include 'dashboard/partials/_form.html' with back_url=back_url object='Site Settings' form=form %}
{% endblock %}

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