Retail
The Retail module holds a store's daily operations: the cash drawer (BOOK-28 to BOOK-30, BOOK-32), the daily close (CARD-01, CARD-02, CARD-05, RPT-15) check cashing (BOOK-33) the lottery (BOOK-39 to BOOK-43) and fuel (gallons, tanks, deliveries and their prepaid sales tax).
| Projects | CrestApps.Almohaseb.Retail (module), CrestApps.Almohaseb.Retail.Core (models, index, rules, service) |
| Features | CrestApps.Almohaseb.Retail.CashDrawer (Cash drawer; depends on the foundation and OrchardCore.Contents) and CrestApps.Almohaseb.Retail.DailyClose (Daily close; depends on the cash drawer and Sales & receivables), and CrestApps.Almohaseb.Retail.CheckCashing (Check cashing; depends on the cash drawer and Accounting) CrestApps.Almohaseb.Retail.Lottery (Lottery; depends on the cash drawer and Sales & receivables) and CrestApps.Almohaseb.Retail.Fuel (Fuel; depends on the cash drawer, Sales & receivables and Purchases), all enabled by the c-store recipe |
| Collection | Retail |
Data
| Type | Kind | Holds |
|---|---|---|
AlmohasebDrawerCashReason | Content type (AlmohasebDrawerCashReasonPart) | Name, Effect (Opening, Add, Subtract, Closing), AllowsMany (forced off for opening and closing), SortOrder, IsActive |
AlmohasebCurrencyDenomination | Content type (AlmohasebCurrencyDenominationPart) | Name, Value, Currency, SortOrder, IsActive |
DrawerCashEntry | Document | Date (business date), ReasonId, Effect (copied from the reason), Amount, Counts (DenominationId, Value as counted, Quantity), Memo, CreatedBy, CreatedUtc |
The reasons and denominations are data: the legacy configuration ids (CashDrawerReasonIds, start/end pairs) became the Effect of each reason. The c-store recipe seeds Opening count, Change from the safe, Paid out, Safe drop, Closing count and the US bills and coins. CashDrawerMigrations (version 1) creates the types and DrawerCashEntryIndex (ItemId, Date, ReasonId, Effect, Amount).
Rules
CashDrawerRules is pure:
Summarize(date, entries)gives aDrawerDay: the lastOpeningandClosingamounts, the sum ofAddand ofSubtract;BeforeTakings= opening + added − taken;CashTakings= closing − before takings (nulluntil counted). The legacy total summed the amounts with minus reasons subtracted and end reasons left out. The over or short needs the sales, so it is the daily close's.Validate(entry, reason, sameDay)returnsDrawerEntryErrors:DateRequired,ReasonRequired(missing or inactive),AmountRequired(at least 0.01, two decimals),OncePerDay(a reason withoutAllowsManyalready on that day; the legacy looked at the filtered range, not the day),CountMismatch(negative quantity, or the counts not adding up to the amount).
ICashDrawerService / CashDrawerService: ListAsync(date), SummarizeAsync(from, to), GetAsync, SaveAsync (drops zero quantities; with counts, the amount is their total; copies the reason's effect), DeleteAsync, and CarryToNextDayAsync(date), which creates the next day's opening entry from the last closing count, count by count, refused with NoClosing, NextDayOpened (the legacy compared dates with their time, so it rarely caught this) or NoOpeningReason. ICashDrawerLookup (ContentCashDrawerLookup) reads the published reasons and the active denominations.
The drawer is not posted: paid-outs are entered as bills or expenses, and the over or short is posted by the daily close.
Daily close
AlmohasebCardTender (content type, AlmohasebCardTenderPart: Name, AccountId (optional asset account), TracksCount, SortOrder, IsActive) is the business's list of card machines and EBT/WIC tenders; the c-store recipe seeds Card terminal 1, EBT food (with count), EBT cash and WIC, without accounts. The legacy fixed fields (four machines, FS, cash part, WIC) and their settings became this list. DailyCloseSettings.OverShortAccountId (Settings → Daily close, group almohaseb-daily-close) is the over and short account.
DailyClose (document close-yyyyMMdd, one per date; the legacy reconciliation date had no uniqueness): Tenders (TenderId, Amount, Count), OverShort, OverShortAccountId, Revision. DailyCloseIndex (ItemId, Date, OverShort; DailyCloseMigrations).
DailyCloseFigures(date, salesTotal, creditSales, tenders, drawer): CashSales = sales − credit sales counted in the daily sales − tenders; ExpectedClosing = drawer before takings + cash sales; OverShort = closing − expected. This restates the legacy cash reconciliation (RPT-15 Difference) on ledger data: purchases and expenses paid from the register are drawer taken out entries.
IDailyCloseService / DailyCloseService: GetFiguresAsync(date, tenders) reads IDailySalesService.GetAsync(date).Total, the CreditSales of the day with InDailySales, and the drawer; CloseAsync(date, tenders, user) validates (BadAmount, UnknownTender, NoClosingCount), posts and saves only on success. DailyClosePostingRule (Retail.DailyClose): Dr each tender account for its total, Cr Undeposited Funds; an over Dr Undeposited Funds, Cr the over and short account; a short the reverse. Nothing to post returns no entry. Closing again re-posts in place.
DailyCloseController (Almohaseb/Store/DailyClose, GET date (default yesterday), POST Amounts[tender], Counts[tender], refresh), shown with Close the business day (CloseAlmohasebDays), also has the checklist of the day: daily sales, opening count, closing count, tenders (the legacy nine-step wizard, CARD-02, as links to each screen).
IDailyCashMovementProvider.GetCashOutAsync(date) lets another feature tell the close about cash it paid out of the register; DailyCloseFigures.OtherCashOut sums them and ExpectedClosing subtracts it.
Check cashing
CashedCheck (document): Date, Amount, Fee, CheckNumber, Maker, ExternalId (the check cashing software's id; a second check with the same id is refused, AlreadyLoaded; the legacy SCCS load had no dedupe, CONN-08), FromRegister (the legacy "Affects Total"), Status (Cashed, Deposited, Returned), DepositId, Memo, Revision; CashPaid = amount − fee. CheckDeposit: Number (sequence Retail.CheckDeposit, D- from 1001; the legacy suggested "Deposit n" with an operator-precedence bug), Date, CheckIds, Total. CashedCheckIndex (Date, Status; ExternalId) and CheckDepositIndex come from CheckCashingMigrations.
ICheckCashingService / CheckCashingService: SaveAsync validates (DateRequired, AmountRequired, BadFee (0 to the amount), Locked for deposited or returned checks) and posts the fee; DeleteAsync unposts the fee first and keeps deposited or returned checks; MarkReturnedAsync; DepositAsync(ids, date) (only checks to deposit; NothingToDeposit); DeleteDepositAsync releases the checks; ListDepositsAsync, GetDepositAsync. The same class is the IDailyCashMovementProvider of the daily close: the cash paid for the day's checks from the register, returned checks excepted.
CashedCheckFeePostingRule (Retail.CheckCashingFee, source CashedCheckFee): the cash paid and the check held both stay in Undeposited Funds, so only the fee is posted, Dr Undeposited Funds, Cr the fee account of Settings > Check cashing (CheckCashingSettings.FeeAccountId, an income or other income account); none when it is not set.
CheckCashingController: Almohaseb/Store/Checks (GET from, to, status, edit), /Save, /Selected (POST ids, command = deposit, returned, delete, depositDate), /Deposits, /Deposits/{id} (the slip, a page without the admin layout), /Deposits/{id}/Delete.
Lottery
AlmohasebLotteryGame (content type, AlmohasebLotteryGamePart: Name, GameNumber (three digits, the start of the barcode), TicketPrice, TicketsPerBook (1 to 1,000), IsActive) and AlmohasebLotteryBox (AlmohasebLotteryBoxPart: Name, Position, IsActive) are the business's lists; the c-store recipe seeds Box 1 to Box 12 (lbx-01..lbx-12) and no games, since games differ by state and change often. LotterySettings (Settings → Lottery, group almohaseb-lottery): SalesCategoryIds and PayoutsCategoryIds, the sales categories compared with the counts; the c-store recipe sets cat-lottery and cat-payouts. ILotteryLookup (ContentLotteryLookup) reads the published games and boxes and the settings.
| Document | Holds |
|---|---|
LotteryBook | GameId, BookNumber (unique within the game), TicketPrice and Tickets (copied from the game when received), Status (Received, Active, SoldOut, Returned), BoxId, ReceivedDate, ActivatedDate, ClosedDate, StartTicket |
LotteryCount | lottery-yyyyMMdd, one per date: Lines (BookId, OpeningTicket, ClosingTicket, TicketPrice; Sold = closing − opening, Amount), Total |
LotteryTerminalDay | terminal-yyyyMMdd, one per date: DrawSales, DrawCancels, DrawCashes, Promotions, Coupons, InstantCashes; NetDrawSales = sales − cancels − promotions − coupons, Cashes = draw + instant winnings |
LotteryMigrations (version 1) creates the types, LotteryBookIndex (GameId, BookNumber; Status, BoxId) and LotteryDayIndex (Kind 0 count / 1 terminal, Date).
LotteryBarcode.Parse reads 9 digits (game 3, book 6) or 12 (plus the ticket 3), spaces and dashes ignored; anything else is not a barcode (the legacy took substrings and threw on a short scan).
ILotteryService / LotteryService:
ReceiveAsync(barcode, gameId, bookNumber, date, user): from the barcode (its game must be active,BadBarcode) or a game and a number (GameRequired,BookNumberRequired);DuplicateBookfor the same game and number.ActivateAsync(bookId, boxId, date, startTicket): only a received book (BookNotAvailable) into an active box (BoxRequired) without an active book (BoxTaken), the start ticket within the book (BadTicket).ReturnAsync(bookId, date): a received or active book.GetCountAsync(date): the books on display that day (activated by then, not closed before it; one sold out that day is counted that day, one returned that day is not), in box order, each opening at its closing in the latest earlier count, else at its start ticket. A saved count keeps its closings and gains books activated since.SaveCountAsync(date, closingTickets, user): recomputes the openings (so a corrected earlier day flows into this one), refuses a closing below the opening or above the book (BadTicket), marks a book closing at its last ticketSoldOutand puts one corrected below it the same day back toActive.GetTerminalAsync,SaveTerminalAsync(amounts zero or more, two decimals,BadAmount).GetFiguresAsync(date):LotteryDayFigureswith the instant sales counted, the terminal day, and the daily sales of the settings' categories:Sales= instant + net draw sales,SalesDifference= sales − rung,PayoutsDifference= terminal winnings − payouts rung.
Nothing is posted: the lottery sales and payouts reach the books through the daily sales.
LotteryController: Almohaseb/Store/Lottery (GET/POST date, closing[bookId]), /Books (GET status = empty for received and active, received, active, soldout, returned, all), /Books/Receive (Barcode, GameId, BookNumber, Date), /Books/Activate (bookId, boxId, startTicket), /Books/Return, /Terminal (GET/POST).
Fuel
AlmohasebFuelGrade (content type, AlmohasebFuelGradePart: Name, Product (Gasoline, Diesel, Other; the prepaid sales tax differs), SalesCategoryId (the register category compared with, optional), SortOrder, IsActive) is the business's list; the c-store recipe seeds Regular 87, Mid-grade 89, Premium 91 and Diesel (fgr-*) without categories, since the c-store categories have no fuel line. IFuelLookup (ContentFuelLookup) reads the published grades and, for suppliers, the vendors of IVendorLookup.ListForBillsAsync (so the feature depends on Purchases).
| Document | Holds |
|---|---|
FuelDay | fuel-yyyyMMdd, one per date: Lines (GradeId, Gallons (three decimals), Amount, TankReading (gallons at the close, optional); Price = amount ÷ gallons to three decimals) |
FuelDelivery | Date, VendorId, InvoiceNumber, Memo, Lines (GradeId, Gallons, Cost, PrepaidTaxRate in dollars per gallon, four decimals; PrepaidTax = gallons × rate to the cent, away from zero); Cost, PrepaidTax, Total |
FuelMigrations (version 1) creates the type, FuelDayIndex (Date) and FuelDeliveryIndex (Date; VendorId, InvoiceNumber).
IFuelService / FuelService:
GetDayAsync(date): the saved day, plus an empty line for each active grade without one, in the grades' order.SaveDayAsync(date, lines, user): drops lines with nothing on them;DateRequired,UnknownGrade(unknown or twice),BadGallons,BadAmount,BadReading.SaveDeliveryAsync(delivery, user): drops lines without gallons or cost;NothingDelivered,UnknownSupplier,UnknownGrade,BadGallons,BadAmount,BadRate,DuplicateInvoice(same supplier and invoice number),NotFound.DeleteDeliveryAsync,ListDeliveriesAsync(from, to),GetLatestRatesAsync()(the rate of the latest delivery of each grade, suggested on the next).GetSummaryAsync(from, to):FuelSummarywith aFuelGradeSummaryper grade with sales, deliveries or a reconciliation (GallonsSold,Sales,AveragePrice,GallonsDelivered,Cost,AverageCost,PrepaidTax),PrepaidTaxByProduct,GallonsDeliveredByProduct, andRegisterSales(the daily sales of the grades' categories;IsComparedis false when no grade has one) withSalesDifference.
FuelTankReconciliation: from the latest reading before the period to the latest one in it, Expected = opening + delivered − sold after the opening date up to the closing date, Variance = closing − expected. Without both readings there is none.
Nothing is posted: the fuel sales reach the books through the daily sales and the supplier invoice through a bill. The prepaid tax by product is what the Michigan schedules (5083/5085, STX-09) read in Phase 5.
FuelController: Almohaseb/Store/Fuel (GET/POST date, default yesterday; Lines[i].GradeId, .Gallons, .Amount, .TankReading), /Deliveries (GET from, to, edit), /Deliveries/Save (Form.*, Form.Lines[i].*), /Deliveries/{id}/Delete, /Summary (from, to, the current month by default).
Reports
Each store feature adds a CrestApps Reports report in category Store through a startup with [Feature(...)] and [RequireFeatures(ReportsConstants.Feature)]:
| Report | Name | Feature | Reads |
|---|---|---|---|
CardTendersReport (RPT-16) | almohaseb-card-tenders | Daily close | IDailyCloseService.ListAsync(from, to): each close's tender totals, the counts of tenders with TracksCount, the over or short |
CheckDepositsReport (RPT-19) | almohaseb-check-deposits | Check cashing | The deposits dated in the range with their checks; the checks still to deposit |
LotterySummaryReport (RPT-22) | almohaseb-lottery-summary | Lottery | ILotteryService.ListFiguresAsync(from, to): a LotteryDayFigures for each day with a count, terminal figures or lottery sales |
FuelReport | almohaseb-fuel | Fuel | IFuelService.GetSummaryAsync |
The legacy slip-printer outputs (RPT-17, RPT-18, RPT-20) are replaced by the printable deposit slip and the export of these reports.
Dashboard
DailyCloseWidgetStartup ([Feature(DailyClose)], [RequireFeatures("OrchardCore.AdminDashboard")]) adds the AlmohasebDailyCloseWidget type (RPT-23: the legacy reconciliation panel): TitlePart, DashboardPart and AlmohasebDailyCloseWidgetPart (Days, 1–31, default 7). It lists the days ending yesterday, latest first, each with its close's over or short or Not closed, linked to the daily close of that date (IDailyCloseService.ListAsync); it needs ViewAlmohasebCashDrawer. The c-store recipe seeds it as dsh-close (2 × 2).
Admin
The Store menu (almohasebStore, icon fa-store): Daily close (almohasebDailyClose), Cash drawer (almohasebCashDrawer), Checks cashed (almohasebChecksCashed), Check deposits (almohasebCheckDeposits), Lottery count (almohasebLotteryCount), Lottery books (almohasebLotteryBooks), Lottery terminal (almohasebLotteryTerminal), Fuel sales (almohasebFuelSales), Fuel deliveries (almohasebFuelDeliveries), Fuel summary (almohasebFuelSummary), Drawer reasons, Denominations, Card tenders, Lottery games, Lottery boxes and Fuel grades (the content lists); and Settings > Daily close, Settings > Check cashing and Settings > Lottery.
| Route (under the admin prefix) | Route name | Permission |
|---|---|---|
Almohaseb/Store/CashDrawer (date, edit) | AlmohasebCashDrawer | View the cash drawer |
Almohaseb/Store/CashDrawer/Save, /Delete/{id}, /Carry | AlmohasebCashDrawerSave, AlmohasebCashDrawerDelete, AlmohasebCashDrawerCarry | Enter the cash drawer |
Permissions
| Permission | Key | Implied by |
|---|---|---|
| Enter the cash drawer | ManageAlmohasebCashDrawer | |
| View the cash drawer | ViewAlmohasebCashDrawer | ManageAlmohasebCashDrawer |
| Close the business day | CloseAlmohasebDays | |
| Enter cashed checks and deposit them | ManageAlmohasebCheckCashing | |
| View cashed checks and deposits | ViewAlmohasebCheckCashing | ManageAlmohasebCheckCashing |
| Receive, activate and count lottery books | ManageAlmohasebLottery | |
| View the lottery | ViewAlmohasebLottery | ManageAlmohasebLottery |
| Enter fuel sales, tank readings and deliveries | ManageAlmohasebFuel | |
| View fuel sales and deliveries | ViewAlmohasebFuel | ManageAlmohasebFuel |