food-market/src/food-market.domain/Sales/RetailSale.cs
nurdotnet 61558179e3 phase2c: RetailSale document — посты в stock как минусовые движения
Domain (foodmarket.Domain.Sales):
- RetailSale: Number "ПР-{yyyy}-{NNNNNN}", Date, Status (Draft/Posted),
  Store/RetailPoint/Customer/Currency, Subtotal/DiscountTotal/Total,
  Payment (Cash/Card/BankTransfer/Bonus/Mixed) + PaidCash/PaidCard split,
  CashierUserId, Notes, Lines.
- RetailSaleLine: ProductId, Quantity, UnitPrice, Discount, LineTotal,
  VatPercent (snapshot), SortOrder.
- PaymentMethod enum.

EF: retail_sales + retail_sale_lines, unique index (tenant,Number),
indexes by date/status/cashier. Migration Phase2c_RetailSale.

API /api/sales/retail (Authorize):
- GET list with filters status/store/from/to/search.
- GET {id} with lines joined to products + units, customer/retail-point
  names resolved.
- POST create draft (lines optional, totals computed server-side).
- PUT update — replaces lines wholesale; rejected if Posted.
- DELETE — drafts only.
- POST {id}/post — creates -qty StockMovements via IStockService for each
  line (decreasing stock), Type=RetailSale; flips to Posted, stamps PostedAt.
- POST {id}/unpost — reverses with +qty movements tagged "retail-sale-reversal".
- Auto-numbering scoped per tenant + year.

Web:
- types: RetailSaleStatus, PaymentMethod, RetailSaleListRow, RetailSaleLineDto,
  RetailSaleDto.
- /sales/retail list (number, date+time, status badge, store, cashier point,
  customer (or "аноним"), payment method, line count, total).
- /sales/retail/new + /:id edit page mirrors Supply edit page UX:
  sticky top bar (Back / Save / Post / Unpost / Delete), reqs grid with
  date/store/customer/currency/payment/paid-cash/paid-card, lines table
  with inline qty/price/discount + Subtotal/Discount/К оплате footer.
- ProductPicker reused. On line add, picks retail price from product's
  prices list (matches "розн" in priceTypeName) or first.
- Sidebar new group "Продажи" → "Розничные чеки" (ShoppingCart).

Posting cycle ready: Supply (+stock) → ... → RetailSale (-stock).
В Stock и Движения видно текущее состояние и историю.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:07:37 +05:00

71 lines
2 KiB
C#

using foodmarket.Domain.Catalog;
using foodmarket.Domain.Common;
namespace foodmarket.Domain.Sales;
public enum RetailSaleStatus
{
Draft = 0,
Posted = 1,
}
public enum PaymentMethod
{
Cash = 0,
Card = 1,
BankTransfer = 2,
Bonus = 3,
Mixed = 99,
}
public class RetailSale : TenantEntity
{
public string Number { get; set; } = "";
public DateTime Date { get; set; } = DateTime.UtcNow;
public RetailSaleStatus Status { get; set; } = RetailSaleStatus.Draft;
public Guid StoreId { get; set; }
public Store Store { get; set; } = null!;
public Guid? RetailPointId { get; set; }
public RetailPoint? RetailPoint { get; set; }
public Guid? CustomerId { get; set; }
public Counterparty? Customer { get; set; }
public Guid? CashierUserId { get; set; }
public Guid CurrencyId { get; set; }
public Currency Currency { get; set; } = null!;
public decimal Subtotal { get; set; } // sum of LineTotal before discount
public decimal DiscountTotal { get; set; }
public decimal Total { get; set; } // = Subtotal - DiscountTotal
public PaymentMethod Payment { get; set; } = PaymentMethod.Cash;
public decimal PaidCash { get; set; }
public decimal PaidCard { get; set; }
public string? Notes { get; set; }
public DateTime? PostedAt { get; set; }
public Guid? PostedByUserId { get; set; }
public ICollection<RetailSaleLine> Lines { get; set; } = new List<RetailSaleLine>();
}
public class RetailSaleLine : TenantEntity
{
public Guid RetailSaleId { get; set; }
public RetailSale RetailSale { get; set; } = null!;
public Guid ProductId { get; set; }
public Product Product { get; set; } = null!;
public decimal Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Discount { get; set; }
public decimal LineTotal { get; set; } // = Quantity * UnitPrice - Discount
public decimal VatPercent { get; set; } // snapshot
public int SortOrder { get; set; }
}