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

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

26 

27import datetime 

28 

29import sqlalchemy as sa 

30from sqlalchemy import orm 

31from sqlalchemy.ext.orderinglist import ordering_list 

32 

33from wuttjamaican.db import model 

34 

35 

36class Order(model.Base): 

37 """ 

38 Represents an :term:`order` for a customer. Each order has one or 

39 more :attr:`items`. 

40 

41 Usually, orders are created by way of a 

42 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`. 

43 """ 

44 __tablename__ = 'sideshow_order' 

45 

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 } 

51 

52 uuid = model.uuid_column() 

53 

54 order_id = sa.Column(sa.Integer(), nullable=False, doc=""" 

55 Unique ID for the order. 

56 

57 When the order is created from New Order Batch, this order ID will 

58 match the batch ID. 

59 """) 

60 

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

64 

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. 

68 

69 See also :attr:`local_customer` and :attr:`pending_customer`. 

70 """) 

71 

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. 

81 

82 See also :attr:`customer_id` and :attr:`pending_customer`. 

83 """) 

84 

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. 

94 

95 See also :attr:`customer_id` and :attr:`local_customer`. 

96 """) 

97 

98 customer_name = sa.Column(sa.String(length=100), nullable=True, doc=""" 

99 Name for the customer account. 

100 """) 

101 

102 phone_number = sa.Column(sa.String(length=20), nullable=True, doc=""" 

103 Phone number for the customer. 

104 """) 

105 

106 email_address = sa.Column(sa.String(length=255), nullable=True, doc=""" 

107 Email address for the customer. 

108 """) 

109 

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

113 

114 created = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc=""" 

115 Timestamp when the order was created. 

116 

117 If the order is created via New Order Batch, this will match the 

118 batch execution timestamp. 

119 """) 

120 

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

130 

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

140 

141 def __str__(self): 

142 return str(self.order_id) 

143 

144 

145class OrderItem(model.Base): 

146 """ 

147 Represents an :term:`order item` within an :class:`Order`. 

148 

149 Usually these are created from 

150 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow` 

151 records. 

152 """ 

153 __tablename__ = 'sideshow_order_item' 

154 

155 uuid = model.uuid_column() 

156 

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

165 

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

170 

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. 

174 

175 See also :attr:`local_product` and :attr:`pending_product`. 

176 """) 

177 

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. 

187 

188 See also :attr:`product_id` and :attr:`pending_product`. 

189 """) 

190 

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. 

200 

201 See also :attr:`product_id` and :attr:`local_product`. 

202 """) 

203 

204 product_scancode = sa.Column(sa.String(length=14), nullable=True, doc=""" 

205 Scancode for the product, as string. 

206 

207 .. note:: 

208 

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. 

212 

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

217 

218 product_brand = sa.Column(sa.String(length=100), nullable=True, doc=""" 

219 Brand name for the product - up to 100 chars. 

220 """) 

221 

222 product_description = sa.Column(sa.String(length=255), nullable=True, doc=""" 

223 Description for the product - up to 255 chars. 

224 """) 

225 

226 product_size = sa.Column(sa.String(length=30), nullable=True, doc=""" 

227 Size of the product, as string - up to 30 chars. 

228 """) 

229 

230 product_weighed = sa.Column(sa.Boolean(), nullable=True, doc=""" 

231 Flag indicating the product is sold by weight; default is null. 

232 """) 

233 

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

237 

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

241 

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

246 

247 case_size = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc=""" 

248 Case pack count for the product, if known. 

249 """) 

250 

251 order_qty = sa.Column(sa.Numeric(precision=10, scale=4), nullable=False, doc=""" 

252 Quantity (as decimal) of product being ordered. 

253 

254 This must be interpreted along with :attr:`order_uom` to determine 

255 the *complete* order quantity, e.g. "2 cases". 

256 """) 

257 

258 order_uom = sa.Column(sa.String(length=10), nullable=False, doc=""" 

259 Code indicating the unit of measure for product being ordered. 

260 

261 This should be one of the codes from 

262 :data:`~sideshow.enum.ORDER_UOM`. 

263 """) 

264 

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

269 

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

274 

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

280 

281 sale_ends = sa.Column(sa.DateTime(timezone=True), nullable=True, doc=""" 

282 End date/time for the sale in effect, if any. 

283 

284 This is only relevant if :attr:`unit_price_sale` is set. 

285 """) 

286 

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

290 

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

295 

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. 

298 

299 This is mostly for display purposes; :attr:`unit_price_quoted` is 

300 used for calculations. 

301 """) 

302 

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

307 

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. 

311 

312 This is calculated using values from: 

313 

314 * :attr:`unit_price_quoted` 

315 * :attr:`order_qty` 

316 * :attr:`order_uom` 

317 * :attr:`case_size` 

318 * :attr:`discount_percent` 

319 """) 

320 

321 status_code = sa.Column(sa.Integer(), nullable=False, doc=""" 

322 Code indicating current status for the order item. 

323 """) 

324 

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

329 

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

334 

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) 

344 

345 def __str__(self): 

346 return self.full_description