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>
192 lines
9.3 KiB
C#
192 lines
9.3 KiB
C#
using System;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
|
|
#nullable disable
|
|
|
|
namespace foodmarket.Infrastructure.Persistence.Migrations
|
|
{
|
|
/// <inheritdoc />
|
|
public partial class Phase2c_RetailSale : Migration
|
|
{
|
|
/// <inheritdoc />
|
|
protected override void Up(MigrationBuilder migrationBuilder)
|
|
{
|
|
migrationBuilder.CreateTable(
|
|
name: "retail_sales",
|
|
schema: "public",
|
|
columns: table => new
|
|
{
|
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
Number = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
|
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
Status = table.Column<int>(type: "integer", nullable: false),
|
|
StoreId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
RetailPointId = table.Column<Guid>(type: "uuid", nullable: true),
|
|
CustomerId = table.Column<Guid>(type: "uuid", nullable: true),
|
|
CashierUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
|
CurrencyId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
Subtotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
DiscountTotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
Total = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
Payment = table.Column<int>(type: "integer", nullable: false),
|
|
PaidCash = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
PaidCard = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
Notes = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
|
PostedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
|
PostedByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
|
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false)
|
|
},
|
|
constraints: table =>
|
|
{
|
|
table.PrimaryKey("PK_retail_sales", x => x.Id);
|
|
table.ForeignKey(
|
|
name: "FK_retail_sales_counterparties_CustomerId",
|
|
column: x => x.CustomerId,
|
|
principalSchema: "public",
|
|
principalTable: "counterparties",
|
|
principalColumn: "Id",
|
|
onDelete: ReferentialAction.Restrict);
|
|
table.ForeignKey(
|
|
name: "FK_retail_sales_currencies_CurrencyId",
|
|
column: x => x.CurrencyId,
|
|
principalSchema: "public",
|
|
principalTable: "currencies",
|
|
principalColumn: "Id",
|
|
onDelete: ReferentialAction.Restrict);
|
|
table.ForeignKey(
|
|
name: "FK_retail_sales_retail_points_RetailPointId",
|
|
column: x => x.RetailPointId,
|
|
principalSchema: "public",
|
|
principalTable: "retail_points",
|
|
principalColumn: "Id",
|
|
onDelete: ReferentialAction.Restrict);
|
|
table.ForeignKey(
|
|
name: "FK_retail_sales_stores_StoreId",
|
|
column: x => x.StoreId,
|
|
principalSchema: "public",
|
|
principalTable: "stores",
|
|
principalColumn: "Id",
|
|
onDelete: ReferentialAction.Restrict);
|
|
});
|
|
|
|
migrationBuilder.CreateTable(
|
|
name: "retail_sale_lines",
|
|
schema: "public",
|
|
columns: table => new
|
|
{
|
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
RetailSaleId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
ProductId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
Quantity = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
UnitPrice = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
Discount = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
LineTotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
|
VatPercent = table.Column<decimal>(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false),
|
|
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
|
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false)
|
|
},
|
|
constraints: table =>
|
|
{
|
|
table.PrimaryKey("PK_retail_sale_lines", x => x.Id);
|
|
table.ForeignKey(
|
|
name: "FK_retail_sale_lines_products_ProductId",
|
|
column: x => x.ProductId,
|
|
principalSchema: "public",
|
|
principalTable: "products",
|
|
principalColumn: "Id",
|
|
onDelete: ReferentialAction.Restrict);
|
|
table.ForeignKey(
|
|
name: "FK_retail_sale_lines_retail_sales_RetailSaleId",
|
|
column: x => x.RetailSaleId,
|
|
principalSchema: "public",
|
|
principalTable: "retail_sales",
|
|
principalColumn: "Id",
|
|
onDelete: ReferentialAction.Cascade);
|
|
});
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sale_lines_OrganizationId_ProductId",
|
|
schema: "public",
|
|
table: "retail_sale_lines",
|
|
columns: new[] { "OrganizationId", "ProductId" });
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sale_lines_ProductId",
|
|
schema: "public",
|
|
table: "retail_sale_lines",
|
|
column: "ProductId");
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sale_lines_RetailSaleId",
|
|
schema: "public",
|
|
table: "retail_sale_lines",
|
|
column: "RetailSaleId");
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sales_CurrencyId",
|
|
schema: "public",
|
|
table: "retail_sales",
|
|
column: "CurrencyId");
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sales_CustomerId",
|
|
schema: "public",
|
|
table: "retail_sales",
|
|
column: "CustomerId");
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sales_OrganizationId_CashierUserId",
|
|
schema: "public",
|
|
table: "retail_sales",
|
|
columns: new[] { "OrganizationId", "CashierUserId" });
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sales_OrganizationId_Date",
|
|
schema: "public",
|
|
table: "retail_sales",
|
|
columns: new[] { "OrganizationId", "Date" });
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sales_OrganizationId_Number",
|
|
schema: "public",
|
|
table: "retail_sales",
|
|
columns: new[] { "OrganizationId", "Number" },
|
|
unique: true);
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sales_OrganizationId_Status",
|
|
schema: "public",
|
|
table: "retail_sales",
|
|
columns: new[] { "OrganizationId", "Status" });
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sales_RetailPointId",
|
|
schema: "public",
|
|
table: "retail_sales",
|
|
column: "RetailPointId");
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "IX_retail_sales_StoreId",
|
|
schema: "public",
|
|
table: "retail_sales",
|
|
column: "StoreId");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Down(MigrationBuilder migrationBuilder)
|
|
{
|
|
migrationBuilder.DropTable(
|
|
name: "retail_sale_lines",
|
|
schema: "public");
|
|
|
|
migrationBuilder.DropTable(
|
|
name: "retail_sales",
|
|
schema: "public");
|
|
}
|
|
}
|
|
}
|