Coverage for src/sideshow/db/model/orders.py: 100%
57 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-06 15:04 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-06 15:04 -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 ID of the proper customer account to which the order pertains, if
67 applicable.
69 This will be set only when an "existing" customer account can be
70 assigned for the order. See also :attr:`pending_customer`.
71 """)
73 pending_customer_uuid = model.uuid_fk_column('sideshow_pending_customer.uuid', nullable=True)
74 pending_customer = orm.relationship(
75 'PendingCustomer',
76 cascade_backrefs=False,
77 back_populates='orders',
78 doc="""
79 Reference to the
80 :class:`~sideshow.db.model.customers.PendingCustomer` record
81 for the order, if applicable.
83 This is set only when the order is for a "new / unknown"
84 customer. See also :attr:`customer_id`.
85 """)
87 customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
88 Name for the customer account.
89 """)
91 phone_number = sa.Column(sa.String(length=20), nullable=True, doc="""
92 Phone number for the customer.
93 """)
95 email_address = sa.Column(sa.String(length=255), nullable=True, doc="""
96 Email address for the customer.
97 """)
99 total_price = sa.Column(sa.Numeric(precision=10, scale=3), nullable=True, doc="""
100 Full price (not including tax etc.) for all items on the order.
101 """)
103 created = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc="""
104 Timestamp when the order was created.
106 If the order is created via New Order Batch, this will match the
107 batch execution timestamp.
108 """)
110 created_by_uuid = model.uuid_fk_column('user.uuid', nullable=False)
111 created_by = orm.relationship(
112 model.User,
113 cascade_backrefs=False,
114 doc="""
115 Reference to the
116 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
117 created the order.
118 """)
120 items = orm.relationship(
121 'OrderItem',
122 collection_class=ordering_list('sequence', count_from=1),
123 cascade='all, delete-orphan',
124 cascade_backrefs=False,
125 back_populates='order',
126 doc="""
127 List of :class:`OrderItem` records belonging to the order.
128 """)
130 def __str__(self):
131 return str(self.order_id)
134class OrderItem(model.Base):
135 """
136 Represents an :term:`order item` within an :class:`Order`.
138 Usually these are created from
139 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
140 records.
141 """
142 __tablename__ = 'sideshow_order_item'
144 uuid = model.uuid_column()
146 order_uuid = model.uuid_fk_column('sideshow_order.uuid', nullable=False)
147 order = orm.relationship(
148 Order,
149 cascade_backrefs=False,
150 back_populates='items',
151 doc="""
152 Reference to the :class:`Order` to which the item belongs.
153 """)
155 sequence = sa.Column(sa.Integer(), nullable=False, doc="""
156 1-based numeric sequence for the item, i.e. its line number within
157 the order.
158 """)
160 product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
161 ID of the true product which the order item represents, if
162 applicable.
164 This will be set only when an "existing" product can be selected
165 for the order. See also :attr:`pending_product`.
166 """)
168 pending_product_uuid = model.uuid_fk_column('sideshow_pending_product.uuid', nullable=True)
169 pending_product = orm.relationship(
170 'PendingProduct',
171 cascade_backrefs=False,
172 back_populates='order_items',
173 doc="""
174 Reference to the
175 :class:`~sideshow.db.model.products.PendingProduct` record for
176 the order item, if applicable.
178 This is set only when the order item is for a "new / unknown"
179 product. See also :attr:`product_id`.
180 """)
182 product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
183 Scancode for the product, as string.
185 .. note::
187 This column allows 14 chars, so can store a full GPC with check
188 digit. However as of writing the actual format used here does
189 not matter to Sideshow logic; "anything" should work.
191 That may change eventually, depending on POS integration
192 scenarios that come up. Maybe a config option to declare
193 whether check digit should be included or not, etc.
194 """)
196 product_brand = sa.Column(sa.String(length=100), nullable=True, doc="""
197 Brand name for the product - up to 100 chars.
198 """)
200 product_description = sa.Column(sa.String(length=255), nullable=True, doc="""
201 Description for the product - up to 255 chars.
202 """)
204 product_size = sa.Column(sa.String(length=30), nullable=True, doc="""
205 Size of the product, as string - up to 30 chars.
206 """)
208 product_weighed = sa.Column(sa.Boolean(), nullable=True, doc="""
209 Flag indicating the product is sold by weight; default is null.
210 """)
212 department_id = sa.Column(sa.String(length=10), nullable=True, doc="""
213 ID of the department to which the product belongs, if known.
214 """)
216 department_name = sa.Column(sa.String(length=30), nullable=True, doc="""
217 Name of the department to which the product belongs, if known.
218 """)
220 special_order = sa.Column(sa.Boolean(), nullable=True, doc="""
221 Flag indicating the item is a "special order" - e.g. something not
222 normally carried by the store. Default is null.
223 """)
225 case_size = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
226 Case pack count for the product, if known.
227 """)
229 order_qty = sa.Column(sa.Numeric(precision=10, scale=4), nullable=False, doc="""
230 Quantity (as decimal) of product being ordered.
232 This must be interpreted along with :attr:`order_uom` to determine
233 the *complete* order quantity, e.g. "2 cases".
234 """)
236 order_uom = sa.Column(sa.String(length=10), nullable=False, doc="""
237 Code indicating the unit of measure for product being ordered.
239 This should be one of the codes from
240 :data:`~sideshow.enum.ORDER_UOM`.
241 """)
243 unit_cost = sa.Column(sa.Numeric(precision=9, scale=5), nullable=True, doc="""
244 Cost of goods amount for one "unit" (not "case") of the product,
245 as decimal to 4 places.
246 """)
248 unit_price_reg = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
249 Regular price for the item unit. Unless a sale is in effect,
250 :attr:`unit_price_quoted` will typically match this value.
251 """)
253 unit_price_sale = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
254 Sale price for the item unit, if applicable. If set, then
255 :attr:`unit_price_quoted` will typically match this value. See
256 also :attr:`sale_ends`.
257 """)
259 sale_ends = sa.Column(sa.DateTime(timezone=True), nullable=True, doc="""
260 End date/time for the sale in effect, if any.
262 This is only relevant if :attr:`unit_price_sale` is set.
263 """)
265 unit_price_quoted = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
266 Quoted price for the item unit. This is the "effective" unit
267 price, which is used to calculate :attr:`total_price`.
269 This price does *not* reflect the :attr:`discount_percent`. It
270 normally should match either :attr:`unit_price_reg` or
271 :attr:`unit_price_sale`.
272 """)
274 case_price_quoted = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
275 Quoted price for a "case" of the item, if applicable.
277 This is mostly for display purposes; :attr:`unit_price_quoted` is
278 used for calculations.
279 """)
281 discount_percent = sa.Column(sa.Numeric(precision=5, scale=3), nullable=True, doc="""
282 Discount percent to apply when calculating :attr:`total_price`, if
283 applicable.
284 """)
286 total_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
287 Full price (not including tax etc.) which the customer is quoted
288 for the order item.
290 This is calculated using values from:
292 * :attr:`unit_price_quoted`
293 * :attr:`order_qty`
294 * :attr:`order_uom`
295 * :attr:`case_size`
296 * :attr:`discount_percent`
297 """)
299 status_code = sa.Column(sa.Integer(), nullable=False, doc="""
300 Code indicating current status for the order item.
301 """)
303 paid_amount = sa.Column(sa.Numeric(precision=8, scale=3), nullable=False, default=0, doc="""
304 Amount which the customer has paid toward the :attr:`total_price`
305 of the item.
306 """)
308 payment_transaction_number = sa.Column(sa.String(length=20), nullable=True, doc="""
309 Transaction number in which payment for the order was taken, if
310 applicable/known.
311 """)
313 def __str__(self):
314 return str(self.pending_product or self.product_description or "")