Coverage for .tox/coverage/lib/python3.11/site-packages/sideshow/db/model/orders.py: 100%
66 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-09 12:58 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-09 12:58 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# Sideshow -- Case/Special Order Tracker
5# Copyright © 2024 Lance Edgar
6#
7# This file is part of Sideshow.
8#
9# Sideshow is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# Sideshow is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17# General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with Sideshow. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Data models for Orders
25"""
27import datetime
29import sqlalchemy as sa
30from sqlalchemy import orm
31from sqlalchemy.ext.orderinglist import ordering_list
33from wuttjamaican.db import model
36class Order(model.Base):
37 """
38 Represents an :term:`order` for a customer. Each order has one or
39 more :attr:`items`.
41 Usually, orders are created by way of a
42 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`.
43 """
44 __tablename__ = 'sideshow_order'
46 # TODO: this feels a bit hacky yet but it does avoid problems
47 # showing the Orders grid for a PendingCustomer
48 __colanderalchemy_config__ = {
49 'excludes': ['items'],
50 }
52 uuid = model.uuid_column()
54 order_id = sa.Column(sa.Integer(), nullable=False, doc="""
55 Unique ID for the order.
57 When the order is created from New Order Batch, this order ID will
58 match the batch ID.
59 """)
61 store_id = sa.Column(sa.String(length=10), nullable=True, doc="""
62 ID of the store to which the order pertains, if applicable.
63 """)
65 customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
66 Proper account ID for the :term:`external customer` to which the
67 order pertains, if applicable.
69 See also :attr:`local_customer` and :attr:`pending_customer`.
70 """)
72 local_customer_uuid = model.uuid_fk_column('sideshow_customer_local.uuid', nullable=True)
73 local_customer = orm.relationship(
74 'LocalCustomer',
75 cascade_backrefs=False,
76 back_populates='orders',
77 doc="""
78 Reference to the
79 :class:`~sideshow.db.model.customers.LocalCustomer` record
80 for the order, if applicable.
82 See also :attr:`customer_id` and :attr:`pending_customer`.
83 """)
85 pending_customer_uuid = model.uuid_fk_column('sideshow_customer_pending.uuid', nullable=True)
86 pending_customer = orm.relationship(
87 'PendingCustomer',
88 cascade_backrefs=False,
89 back_populates='orders',
90 doc="""
91 Reference to the
92 :class:`~sideshow.db.model.customers.PendingCustomer` record
93 for the order, if applicable.
95 See also :attr:`customer_id` and :attr:`local_customer`.
96 """)
98 customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
99 Name for the customer account.
100 """)
102 phone_number = sa.Column(sa.String(length=20), nullable=True, doc="""
103 Phone number for the customer.
104 """)
106 email_address = sa.Column(sa.String(length=255), nullable=True, doc="""
107 Email address for the customer.
108 """)
110 total_price = sa.Column(sa.Numeric(precision=10, scale=3), nullable=True, doc="""
111 Full price (not including tax etc.) for all items on the order.
112 """)
114 created = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc="""
115 Timestamp when the order was created.
117 If the order is created via New Order Batch, this will match the
118 batch execution timestamp.
119 """)
121 created_by_uuid = model.uuid_fk_column('user.uuid', nullable=False)
122 created_by = orm.relationship(
123 model.User,
124 cascade_backrefs=False,
125 doc="""
126 Reference to the
127 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
128 created the order.
129 """)
131 items = orm.relationship(
132 'OrderItem',
133 collection_class=ordering_list('sequence', count_from=1),
134 cascade='all, delete-orphan',
135 cascade_backrefs=False,
136 back_populates='order',
137 doc="""
138 List of :class:`OrderItem` records belonging to the order.
139 """)
141 def __str__(self):
142 return str(self.order_id)
145class OrderItem(model.Base):
146 """
147 Represents an :term:`order item` within an :class:`Order`.
149 Usually these are created from
150 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
151 records.
152 """
153 __tablename__ = 'sideshow_order_item'
155 uuid = model.uuid_column()
157 order_uuid = model.uuid_fk_column('sideshow_order.uuid', nullable=False)
158 order = orm.relationship(
159 Order,
160 cascade_backrefs=False,
161 back_populates='items',
162 doc="""
163 Reference to the :class:`Order` to which the item belongs.
164 """)
166 sequence = sa.Column(sa.Integer(), nullable=False, doc="""
167 1-based numeric sequence for the item, i.e. its line number within
168 the order.
169 """)
171 product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
172 Proper ID for the :term:`external product` which the order item
173 represents, if applicable.
175 See also :attr:`local_product` and :attr:`pending_product`.
176 """)
178 local_product_uuid = model.uuid_fk_column('sideshow_product_local.uuid', nullable=True)
179 local_product = orm.relationship(
180 'LocalProduct',
181 cascade_backrefs=False,
182 back_populates='order_items',
183 doc="""
184 Reference to the
185 :class:`~sideshow.db.model.products.LocalProduct` record for
186 the order item, if applicable.
188 See also :attr:`product_id` and :attr:`pending_product`.
189 """)
191 pending_product_uuid = model.uuid_fk_column('sideshow_product_pending.uuid', nullable=True)
192 pending_product = orm.relationship(
193 'PendingProduct',
194 cascade_backrefs=False,
195 back_populates='order_items',
196 doc="""
197 Reference to the
198 :class:`~sideshow.db.model.products.PendingProduct` record for
199 the order item, if applicable.
201 See also :attr:`product_id` and :attr:`local_product`.
202 """)
204 product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
205 Scancode for the product, as string.
207 .. note::
209 This column allows 14 chars, so can store a full GPC with check
210 digit. However as of writing the actual format used here does
211 not matter to Sideshow logic; "anything" should work.
213 That may change eventually, depending on POS integration
214 scenarios that come up. Maybe a config option to declare
215 whether check digit should be included or not, etc.
216 """)
218 product_brand = sa.Column(sa.String(length=100), nullable=True, doc="""
219 Brand name for the product - up to 100 chars.
220 """)
222 product_description = sa.Column(sa.String(length=255), nullable=True, doc="""
223 Description for the product - up to 255 chars.
224 """)
226 product_size = sa.Column(sa.String(length=30), nullable=True, doc="""
227 Size of the product, as string - up to 30 chars.
228 """)
230 product_weighed = sa.Column(sa.Boolean(), nullable=True, doc="""
231 Flag indicating the product is sold by weight; default is null.
232 """)
234 department_id = sa.Column(sa.String(length=10), nullable=True, doc="""
235 ID of the department to which the product belongs, if known.
236 """)
238 department_name = sa.Column(sa.String(length=30), nullable=True, doc="""
239 Name of the department to which the product belongs, if known.
240 """)
242 special_order = sa.Column(sa.Boolean(), nullable=True, doc="""
243 Flag indicating the item is a "special order" - e.g. something not
244 normally carried by the store. Default is null.
245 """)
247 case_size = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
248 Case pack count for the product, if known.
249 """)
251 order_qty = sa.Column(sa.Numeric(precision=10, scale=4), nullable=False, doc="""
252 Quantity (as decimal) of product being ordered.
254 This must be interpreted along with :attr:`order_uom` to determine
255 the *complete* order quantity, e.g. "2 cases".
256 """)
258 order_uom = sa.Column(sa.String(length=10), nullable=False, doc="""
259 Code indicating the unit of measure for product being ordered.
261 This should be one of the codes from
262 :data:`~sideshow.enum.ORDER_UOM`.
263 """)
265 unit_cost = sa.Column(sa.Numeric(precision=9, scale=5), nullable=True, doc="""
266 Cost of goods amount for one "unit" (not "case") of the product,
267 as decimal to 4 places.
268 """)
270 unit_price_reg = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
271 Regular price for the item unit. Unless a sale is in effect,
272 :attr:`unit_price_quoted` will typically match this value.
273 """)
275 unit_price_sale = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
276 Sale price for the item unit, if applicable. If set, then
277 :attr:`unit_price_quoted` will typically match this value. See
278 also :attr:`sale_ends`.
279 """)
281 sale_ends = sa.Column(sa.DateTime(timezone=True), nullable=True, doc="""
282 End date/time for the sale in effect, if any.
284 This is only relevant if :attr:`unit_price_sale` is set.
285 """)
287 unit_price_quoted = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
288 Quoted price for the item unit. This is the "effective" unit
289 price, which is used to calculate :attr:`total_price`.
291 This price does *not* reflect the :attr:`discount_percent`. It
292 normally should match either :attr:`unit_price_reg` or
293 :attr:`unit_price_sale`.
294 """)
296 case_price_quoted = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
297 Quoted price for a "case" of the item, if applicable.
299 This is mostly for display purposes; :attr:`unit_price_quoted` is
300 used for calculations.
301 """)
303 discount_percent = sa.Column(sa.Numeric(precision=5, scale=3), nullable=True, doc="""
304 Discount percent to apply when calculating :attr:`total_price`, if
305 applicable.
306 """)
308 total_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
309 Full price (not including tax etc.) which the customer is quoted
310 for the order item.
312 This is calculated using values from:
314 * :attr:`unit_price_quoted`
315 * :attr:`order_qty`
316 * :attr:`order_uom`
317 * :attr:`case_size`
318 * :attr:`discount_percent`
319 """)
321 status_code = sa.Column(sa.Integer(), nullable=False, doc="""
322 Code indicating current status for the order item.
323 """)
325 paid_amount = sa.Column(sa.Numeric(precision=8, scale=3), nullable=False, default=0, doc="""
326 Amount which the customer has paid toward the :attr:`total_price`
327 of the item.
328 """)
330 payment_transaction_number = sa.Column(sa.String(length=20), nullable=True, doc="""
331 Transaction number in which payment for the order was taken, if
332 applicable/known.
333 """)
335 @property
336 def full_description(self):
337 """ """
338 fields = [
339 self.product_brand or '',
340 self.product_description or '',
341 self.product_size or '']
342 fields = [f.strip() for f in fields if f.strip()]
343 return ' '.join(fields)
345 def __str__(self):
346 return self.full_description