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

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 ID of the proper customer account to which the order pertains, if 

67 applicable. 

68 

69 This will be set only when an "existing" customer account can be 

70 assigned for the order. See also :attr:`pending_customer`. 

71 """) 

72 

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. 

82 

83 This is set only when the order is for a "new / unknown" 

84 customer. See also :attr:`customer_id`. 

85 """) 

86 

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

88 Name for the customer account. 

89 """) 

90 

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

92 Phone number for the customer. 

93 """) 

94 

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

96 Email address for the customer. 

97 """) 

98 

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

102 

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

104 Timestamp when the order was created. 

105 

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

107 batch execution timestamp. 

108 """) 

109 

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

119 

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

129 

130 def __str__(self): 

131 return str(self.order_id) 

132 

133 

134class OrderItem(model.Base): 

135 """ 

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

137 

138 Usually these are created from 

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

140 records. 

141 """ 

142 __tablename__ = 'sideshow_order_item' 

143 

144 uuid = model.uuid_column() 

145 

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

154 

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

159 

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. 

163 

164 This will be set only when an "existing" product can be selected 

165 for the order. See also :attr:`pending_product`. 

166 """) 

167 

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. 

177 

178 This is set only when the order item is for a "new / unknown" 

179 product. See also :attr:`product_id`. 

180 """) 

181 

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

183 Scancode for the product, as string. 

184 

185 .. note:: 

186 

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. 

190 

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

195 

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

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

198 """) 

199 

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

201 Description for the product - up to 255 chars. 

202 """) 

203 

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

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

206 """) 

207 

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

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

210 """) 

211 

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

215 

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

219 

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

224 

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

226 Case pack count for the product, if known. 

227 """) 

228 

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

230 Quantity (as decimal) of product being ordered. 

231 

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

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

234 """) 

235 

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

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

238 

239 This should be one of the codes from 

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

241 """) 

242 

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

247 

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

252 

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

258 

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

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

261 

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

263 """) 

264 

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

268 

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

273 

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. 

276 

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

278 used for calculations. 

279 """) 

280 

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

285 

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. 

289 

290 This is calculated using values from: 

291 

292 * :attr:`unit_price_quoted` 

293 * :attr:`order_qty` 

294 * :attr:`order_uom` 

295 * :attr:`case_size` 

296 * :attr:`discount_percent` 

297 """) 

298 

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

300 Code indicating current status for the order item. 

301 """) 

302 

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

307 

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

312 

313 def __str__(self): 

314 return str(self.pending_product or self.product_description or "")