Coverage for .tox / coverage / lib / python3.11 / site-packages / sideshow / db / model / products.py: 100%
43 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 17:10 -0600
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 17:10 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# Sideshow -- Case/Special Order Tracker
5# Copyright © 2024-2025 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 Products
25"""
27import sqlalchemy as sa
28from sqlalchemy import orm
30from wuttjamaican.db import model
31from wuttjamaican.util import make_utc
33from sideshow.enum import PendingProductStatus
36class ProductMixin: # pylint: disable=duplicate-code
37 """
38 Base class for product tables. This has shared columns, used by e.g.:
40 * :class:`LocalProduct`
41 * :class:`PendingProduct`
42 """
44 scancode = sa.Column(
45 sa.String(length=14),
46 nullable=True,
47 doc="""
48 Scancode for the product, as string.
50 .. note::
52 This column allows 14 chars, so can store a full GPC with check
53 digit. However as of writing the actual format used here does
54 not matter to Sideshow logic; "anything" should work.
56 That may change eventually, depending on POS integration
57 scenarios that come up. Maybe a config option to declare
58 whether check digit should be included or not, etc.
59 """,
60 )
62 brand_name = sa.Column(
63 sa.String(length=100),
64 nullable=True,
65 doc="""
66 Brand name for the product - up to 100 chars.
67 """,
68 )
70 description = sa.Column(
71 sa.String(length=255),
72 nullable=True,
73 doc="""
74 Description for the product - up to 255 chars.
75 """,
76 )
78 size = sa.Column(
79 sa.String(length=30),
80 nullable=True,
81 doc="""
82 Size of the product, as string - up to 30 chars.
83 """,
84 )
86 weighed = sa.Column(
87 sa.Boolean(),
88 nullable=True,
89 doc="""
90 Flag indicating the product is sold by weight; default is null.
91 """,
92 )
94 department_id = sa.Column(
95 sa.String(length=10),
96 nullable=True,
97 doc="""
98 ID of the department to which the product belongs, if known.
99 """,
100 )
102 department_name = sa.Column(
103 sa.String(length=30),
104 nullable=True,
105 doc="""
106 Name of the department to which the product belongs, if known.
107 """,
108 )
110 special_order = sa.Column(
111 sa.Boolean(),
112 nullable=True,
113 doc="""
114 Flag indicating the item is a "special order" - e.g. something not
115 normally carried by the store. Default is null.
116 """,
117 )
119 vendor_name = sa.Column(
120 sa.String(length=50),
121 nullable=True,
122 doc="""
123 Name of vendor from which product may be purchased, if known. See
124 also :attr:`vendor_item_code`.
125 """,
126 )
128 vendor_item_code = sa.Column(
129 sa.String(length=20),
130 nullable=True,
131 doc="""
132 Item code (SKU) to use when ordering this product from the vendor
133 identified by :attr:`vendor_name`, if known.
134 """,
135 )
137 case_size = sa.Column(
138 sa.Numeric(precision=9, scale=4),
139 nullable=True,
140 doc="""
141 Case pack count for the product, if known.
142 """,
143 )
145 unit_cost = sa.Column(
146 sa.Numeric(precision=9, scale=5),
147 nullable=True,
148 doc="""
149 Cost of goods amount for one "unit" (not "case") of the product,
150 as decimal to 4 places.
151 """,
152 )
154 unit_price_reg = sa.Column(
155 sa.Numeric(precision=8, scale=3),
156 nullable=True,
157 doc="""
158 Regular price for a "unit" of the product.
159 """,
160 )
162 notes = sa.Column(
163 sa.Text(),
164 nullable=True,
165 doc="""
166 Arbitrary notes regarding the product, if applicable.
167 """,
168 )
170 @property
171 def full_description(self): # pylint: disable=empty-docstring
172 """ """
173 fields = [self.brand_name or "", self.description or "", self.size or ""]
174 fields = [f.strip() for f in fields if f.strip()]
175 return " ".join(fields)
177 def __str__(self):
178 return self.full_description
181class LocalProduct(ProductMixin, model.Base): # pylint: disable=too-few-public-methods
182 """
183 This table contains the :term:`local product` records.
185 Sideshow will do customer lookups against this table by default,
186 unless it's configured to use :term:`external products <external
187 product>` instead.
189 Also by default, when a :term:`new order batch` with
190 :term:`pending product(s) <pending product>` is executed, new
191 record(s) will be added to this local products table, for lookup
192 next time.
193 """
195 __tablename__ = "sideshow_product_local"
197 uuid = model.uuid_column()
199 external_id = sa.Column(
200 sa.String(length=20),
201 nullable=True,
202 doc="""
203 ID of the true external product associated with this record, if
204 applicable.
205 """,
206 )
208 order_items = orm.relationship(
209 "OrderItem",
210 back_populates="local_product",
211 cascade_backrefs=False,
212 doc="""
213 List of :class:`~sideshow.db.model.orders.OrderItem` records
214 associated with this product.
215 """,
216 )
218 new_order_batch_rows = orm.relationship(
219 "NewOrderBatchRow",
220 back_populates="local_product",
221 cascade_backrefs=False,
222 doc="""
223 List of
224 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
225 records associated with this product.
226 """,
227 )
230class PendingProduct( # pylint: disable=too-few-public-methods
231 ProductMixin, model.Base
232):
233 """
234 This table contains the :term:`pending product` records, used when
235 creating an :term:`order` for new/unknown product(s).
237 Sideshow will automatically create and (hopefully) delete these
238 records as needed.
240 By default, when a :term:`new order batch` with pending product(s)
241 is executed, new record(s) will be added to the :term:`local
242 products <local product>` table, for lookup next time.
243 """
245 __tablename__ = "sideshow_product_pending"
247 uuid = model.uuid_column()
249 product_id = sa.Column(
250 sa.String(length=20),
251 nullable=True,
252 doc="""
253 ID of the :term:`external product` associated with this record, if
254 applicable/known.
255 """,
256 )
258 status = sa.Column(
259 sa.Enum(PendingProductStatus),
260 nullable=False,
261 doc="""
262 Status code for the product record.
263 """,
264 )
266 created = sa.Column(
267 sa.DateTime(),
268 nullable=False,
269 default=make_utc,
270 doc="""
271 Timestamp when the product record was created.
272 """,
273 )
275 created_by_uuid = model.uuid_fk_column("user.uuid", nullable=False)
276 created_by = orm.relationship(
277 model.User,
278 cascade_backrefs=False,
279 doc="""
280 Reference to the
281 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
282 created the product record.
283 """,
284 )
286 order_items = orm.relationship(
287 "OrderItem",
288 back_populates="pending_product",
289 cascade_backrefs=False,
290 doc="""
291 List of :class:`~sideshow.db.model.orders.OrderItem` records
292 associated with this product.
293 """,
294 )
296 new_order_batch_rows = orm.relationship(
297 "NewOrderBatchRow",
298 back_populates="pending_product",
299 cascade_backrefs=False,
300 doc="""
301 List of
302 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
303 records associated with this product.
304 """,
305 )