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

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""" 

26 

27import sqlalchemy as sa 

28from sqlalchemy import orm 

29 

30from wuttjamaican.db import model 

31from wuttjamaican.util import make_utc 

32 

33from sideshow.enum import PendingProductStatus 

34 

35 

36class ProductMixin: # pylint: disable=duplicate-code 

37 """ 

38 Base class for product tables. This has shared columns, used by e.g.: 

39 

40 * :class:`LocalProduct` 

41 * :class:`PendingProduct` 

42 """ 

43 

44 scancode = sa.Column( 

45 sa.String(length=14), 

46 nullable=True, 

47 doc=""" 

48 Scancode for the product, as string. 

49 

50 .. note:: 

51 

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. 

55 

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 ) 

61 

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 ) 

69 

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 ) 

77 

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 ) 

85 

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 ) 

93 

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 ) 

101 

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 ) 

109 

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 ) 

118 

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 ) 

127 

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 ) 

136 

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 ) 

144 

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 ) 

153 

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 ) 

161 

162 notes = sa.Column( 

163 sa.Text(), 

164 nullable=True, 

165 doc=""" 

166 Arbitrary notes regarding the product, if applicable. 

167 """, 

168 ) 

169 

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) 

176 

177 def __str__(self): 

178 return self.full_description 

179 

180 

181class LocalProduct(ProductMixin, model.Base): # pylint: disable=too-few-public-methods 

182 """ 

183 This table contains the :term:`local product` records. 

184 

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. 

188 

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 """ 

194 

195 __tablename__ = "sideshow_product_local" 

196 

197 uuid = model.uuid_column() 

198 

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 ) 

207 

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 ) 

217 

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 ) 

228 

229 

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). 

236 

237 Sideshow will automatically create and (hopefully) delete these 

238 records as needed. 

239 

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 """ 

244 

245 __tablename__ = "sideshow_product_pending" 

246 

247 uuid = model.uuid_column() 

248 

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 ) 

257 

258 status = sa.Column( 

259 sa.Enum(PendingProductStatus), 

260 nullable=False, 

261 doc=""" 

262 Status code for the product record. 

263 """, 

264 ) 

265 

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 ) 

274 

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 ) 

285 

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 ) 

295 

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 )