Merge pull request 'bug fixing' (#55) from bug-fixing into Dev

Reviewed-on: #55
This commit was merged in pull request #55.
This commit is contained in:
2026-09-13 07:52:06 +00:00
27 changed files with 8403 additions and 50 deletions
+5
View File
@@ -22,6 +22,11 @@ public class Grn
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; } public Warehouse? Warehouse { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public GrnStatus Status { get; set; } = GrnStatus.Draft; public GrnStatus Status { get; set; } = GrnStatus.Draft;
public int CreatedBy { get; set; } public int CreatedBy { get; set; }
@@ -17,6 +17,11 @@ public class SalesInvoice
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; } public Warehouse? Warehouse { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C; public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
public SalesInvoiceStatus Status { get; set; } = SalesInvoiceStatus.Draft; public SalesInvoiceStatus Status { get; set; } = SalesInvoiceStatus.Draft;
@@ -19,6 +19,11 @@ public class StockAdjustment
public int ReasonCodeId { get; set; } public int ReasonCodeId { get; set; }
public ReasonCode? ReasonCode { get; set; } public ReasonCode? ReasonCode { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public AdjustmentStatus Status { get; set; } = AdjustmentStatus.Posted; public AdjustmentStatus Status { get; set; } = AdjustmentStatus.Posted;
public int CreatedBy { get; set; } public int CreatedBy { get; set; }
@@ -16,6 +16,11 @@ public class StockCount
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; } public Warehouse? Warehouse { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public CountType CountType { get; set; } public CountType CountType { get; set; }
public CountStatus Status { get; set; } = CountStatus.Draft; public CountStatus Status { get; set; } = CountStatus.Draft;
@@ -19,6 +19,11 @@ public class StockTransfer
public int DestWarehouseId { get; set; } public int DestWarehouseId { get; set; }
public Warehouse? DestWarehouse { get; set; } public Warehouse? DestWarehouse { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from src/dest warehouse.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public TransferStatus Status { get; set; } = TransferStatus.Draft; public TransferStatus Status { get; set; } = TransferStatus.Draft;
public int CreatedBy { get; set; } public int CreatedBy { get; set; }
+5 -2
View File
@@ -15,13 +15,13 @@ public sealed record GrnLineDto(
HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers); HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers);
public sealed record GrnDto( public sealed record GrnDto(
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status, int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, string? GlJournalNo, int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, string? GlJournalNo,
int? PurchaseInvoiceId, string? PurchaseInvoiceNo, IReadOnlyList<GrnLineDto> Lines); int? PurchaseInvoiceId, string? PurchaseInvoiceNo, IReadOnlyList<GrnLineDto> Lines);
/// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary> /// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary>
public sealed record GrnSummaryDto( public sealed record GrnSummaryDto(
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status, int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount, int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount,
int? PurchaseInvoiceId, string? PurchaseInvoiceNo); int? PurchaseInvoiceId, string? PurchaseInvoiceNo);
@@ -81,6 +81,9 @@ public sealed class CreateGrnRequest
/// <summary>Required for a direct receipt (no PO); ignored otherwise (vendor derives from the PO).</summary> /// <summary>Required for a direct receipt (no PO); ignored otherwise (vendor derives from the PO).</summary>
public int? VendorId { get; set; } public int? VendorId { get; set; }
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
/// <summary>Owning branch for this GRN — warehouses are shared, so this is what actually
/// scopes the document (resolved via IBranchConfigService; required when branching is enabled).</summary>
[Required] public int BranchId { get; set; }
[Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new();
} }
@@ -15,14 +15,14 @@ public sealed record SalesInvoiceTotalsDto(
public sealed record SalesInvoiceDto( public sealed record SalesInvoiceDto(
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId, int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
string CustomerSnapshotName, string? CustomerSnapshotTaxNo, int WarehouseId, string CustomerSnapshotName, string? CustomerSnapshotTaxNo, int WarehouseId, int BranchId,
SalesInvoiceType InvoiceType, SalesInvoiceStatus Status, int CreatedBy, DateTime CreatedAt, SalesInvoiceType InvoiceType, SalesInvoiceStatus Status, int CreatedBy, DateTime CreatedAt,
DateTime? UpdatedAt, string? GlJournalNo, DateTime? GlPostedAt, DateTime? UpdatedAt, string? GlJournalNo, DateTime? GlPostedAt,
SalesInvoiceTotalsDto Totals, IReadOnlyList<SalesInvoiceLineDto> Lines); SalesInvoiceTotalsDto Totals, IReadOnlyList<SalesInvoiceLineDto> Lines);
public sealed record SalesInvoiceSummaryDto( public sealed record SalesInvoiceSummaryDto(
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId, int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
string CustomerSnapshotName, int WarehouseId, SalesInvoiceType InvoiceType, string CustomerSnapshotName, int WarehouseId, int BranchId, SalesInvoiceType InvoiceType,
SalesInvoiceStatus Status, SalesInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo); SalesInvoiceStatus Status, SalesInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
public sealed record SalesInvoicePaymentDto( public sealed record SalesInvoicePaymentDto(
@@ -65,6 +65,9 @@ public sealed class CreateSalesInvoiceRequest
{ {
[Required] public int CustomerId { get; set; } [Required] public int CustomerId { get; set; }
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
/// <summary>Owning branch — warehouses are shared, so this is what actually scopes the
/// document (resolved via IBranchConfigService).</summary>
[Required] public int BranchId { get; set; }
[Required, EnumDataType(typeof(SalesInvoiceType))] public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C; [Required, EnumDataType(typeof(SalesInvoiceType))] public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
[Required, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new();
} }
@@ -73,6 +76,9 @@ public sealed class UpdateSalesInvoiceRequest
{ {
[Required] public int CustomerId { get; set; } [Required] public int CustomerId { get; set; }
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
/// <summary>Owning branch — warehouses are shared, so this is what actually scopes the
/// document (resolved via IBranchConfigService).</summary>
[Required] public int BranchId { get; set; }
[Required, EnumDataType(typeof(SalesInvoiceType))] public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C; [Required, EnumDataType(typeof(SalesInvoiceType))] public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
[Required, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new();
} }
+5 -2
View File
@@ -8,13 +8,13 @@ namespace ERPCore.Dtos.Stock;
public sealed record AdjustmentLineDto(int AdjLineId, int ItemId, int? BinId, int? BatchId, decimal QtyDelta); public sealed record AdjustmentLineDto(int AdjLineId, int ItemId, int? BinId, int? BatchId, decimal QtyDelta);
public sealed record AdjustmentDto( public sealed record AdjustmentDto(
int AdjustmentId, string DocNo, int WarehouseId, int ReasonCodeId, AdjustmentStatus Status, int AdjustmentId, string DocNo, int WarehouseId, int BranchId, int ReasonCodeId, AdjustmentStatus Status,
int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt, int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
IReadOnlyList<AdjustmentLineDto> Lines, IReadOnlyList<int> LedgerRefs); IReadOnlyList<AdjustmentLineDto> Lines, IReadOnlyList<int> LedgerRefs);
/// <summary>Row shape for <c>GET /stock-adjustments</c> — no lines/ledgerRefs (those need a per-row query).</summary> /// <summary>Row shape for <c>GET /stock-adjustments</c> — no lines/ledgerRefs (those need a per-row query).</summary>
public sealed record AdjustmentSummaryDto( public sealed record AdjustmentSummaryDto(
int AdjustmentId, string DocNo, int WarehouseId, int ReasonCodeId, AdjustmentStatus Status, int AdjustmentId, string DocNo, int WarehouseId, int BranchId, int ReasonCodeId, AdjustmentStatus Status,
int CreatedBy, DateTime CreatedAt, int LineCount, string? GlJournalNo); int CreatedBy, DateTime CreatedAt, int LineCount, string? GlJournalNo);
// Requests ---------------------------------------------------------------------- // Requests ----------------------------------------------------------------------
@@ -31,6 +31,9 @@ public sealed class CreateAdjustmentLineInput
public sealed class CreateAdjustmentRequest public sealed class CreateAdjustmentRequest
{ {
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
/// <summary>Owning branch — warehouses are shared, so this is what actually scopes the
/// document (resolved via IBranchConfigService).</summary>
[Required] public int BranchId { get; set; }
/// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error, not 0.</summary> /// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error, not 0.</summary>
public int? ReasonCodeId { get; set; } public int? ReasonCodeId { get; set; }
[Required, MinLength(1)] public List<CreateAdjustmentLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateAdjustmentLineInput> Lines { get; set; } = new();
+5 -2
View File
@@ -8,12 +8,12 @@ namespace ERPCore.Dtos.Stock;
public sealed record CountLineDto(int CountLineId, int ItemId, int? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance); public sealed record CountLineDto(int CountLineId, int ItemId, int? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance);
public sealed record CountDto( public sealed record CountDto(
int CountId, string DocNo, int WarehouseId, CountType CountType, CountStatus Status, int CountId, string DocNo, int WarehouseId, int BranchId, CountType CountType, CountStatus Status,
int CreatedBy, DateTime CreatedAt, IReadOnlyList<CountLineDto> Lines); int CreatedBy, DateTime CreatedAt, IReadOnlyList<CountLineDto> Lines);
/// <summary>Row shape for <c>GET /stock-counts</c> — line count instead of the lines themselves.</summary> /// <summary>Row shape for <c>GET /stock-counts</c> — line count instead of the lines themselves.</summary>
public sealed record CountSummaryDto( public sealed record CountSummaryDto(
int CountId, string DocNo, int WarehouseId, CountType CountType, CountStatus Status, int CountId, string DocNo, int WarehouseId, int BranchId, CountType CountType, CountStatus Status,
int CreatedBy, DateTime CreatedAt, int LineCount); int CreatedBy, DateTime CreatedAt, int LineCount);
public sealed record CountPostResultDto(int CountId, CountStatus Status, int? AdjustmentId, string? GlJournalNo, IReadOnlyList<int> LedgerRefs); public sealed record CountPostResultDto(int CountId, CountStatus Status, int? AdjustmentId, string? GlJournalNo, IReadOnlyList<int> LedgerRefs);
@@ -23,6 +23,9 @@ public sealed record CountPostResultDto(int CountId, CountStatus Status, int? Ad
public sealed class CreateCountRequest public sealed class CreateCountRequest
{ {
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
/// <summary>Owning branch — warehouses are shared, so this is what actually scopes the
/// document (resolved via IBranchConfigService).</summary>
[Required] public int BranchId { get; set; }
[Required, EnumDataType(typeof(CountType))] public CountType CountType { get; set; } [Required, EnumDataType(typeof(CountType))] public CountType CountType { get; set; }
[Required, MinLength(1)] public List<int> ItemIds { get; set; } = new(); [Required, MinLength(1)] public List<int> ItemIds { get; set; } = new();
} }
+5 -2
View File
@@ -9,12 +9,12 @@ public sealed record TransferLineDto(
int TransferLineId, int ItemId, int? SrcBinId, int? DestBinId, int? BatchId, decimal Qty, decimal QtyReceived); int TransferLineId, int ItemId, int? SrcBinId, int? DestBinId, int? BatchId, decimal Qty, decimal QtyReceived);
public sealed record TransferDto( public sealed record TransferDto(
int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId, int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId, int BranchId,
TransferStatus Status, int CreatedBy, DateTime CreatedAt, IReadOnlyList<TransferLineDto> Lines); TransferStatus Status, int CreatedBy, DateTime CreatedAt, IReadOnlyList<TransferLineDto> Lines);
/// <summary>Row shape for <c>GET /stock-transfers</c> — line count instead of the lines themselves.</summary> /// <summary>Row shape for <c>GET /stock-transfers</c> — line count instead of the lines themselves.</summary>
public sealed record TransferSummaryDto( public sealed record TransferSummaryDto(
int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId, int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId, int BranchId,
TransferStatus Status, int CreatedBy, DateTime CreatedAt, int LineCount); TransferStatus Status, int CreatedBy, DateTime CreatedAt, int LineCount);
public sealed record ConsumedLayerDto(int LayerId, decimal QtyConsumed, decimal UnitCost); public sealed record ConsumedLayerDto(int LayerId, decimal QtyConsumed, decimal UnitCost);
@@ -42,6 +42,9 @@ public sealed class CreateTransferRequest
{ {
[Required] public int SrcWarehouseId { get; set; } [Required] public int SrcWarehouseId { get; set; }
[Required] public int DestWarehouseId { get; set; } [Required] public int DestWarehouseId { get; set; }
/// <summary>Owning branch for this transfer — warehouses are shared, so this is what
/// actually scopes the document (resolved via IBranchConfigService).</summary>
[Required] public int BranchId { get; set; }
[Required, MinLength(1)] public List<CreateTransferLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateTransferLineInput> Lines { get; set; } = new();
} }
@@ -22,11 +22,13 @@ public sealed class GrnConfiguration : IEntityTypeConfiguration<Grn>
builder.HasOne(g => g.PurchaseOrder).WithMany().HasForeignKey(g => g.PoId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(g => g.PurchaseOrder).WithMany().HasForeignKey(g => g.PoId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(g => g.Vendor).WithMany().HasForeignKey(g => g.VendorId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(g => g.Vendor).WithMany().HasForeignKey(g => g.VendorId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(g => g.Warehouse).WithMany().HasForeignKey(g => g.WarehouseId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(g => g.Warehouse).WithMany().HasForeignKey(g => g.WarehouseId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(g => g.Branch).WithMany().HasForeignKey(g => g.BranchId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(g => g.Creator).WithMany().HasForeignKey(g => g.CreatedBy).OnDelete(DeleteBehavior.Restrict); builder.HasOne(g => g.Creator).WithMany().HasForeignKey(g => g.CreatedBy).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(g => g.PurchaseInvoice).WithMany().HasForeignKey(g => g.PurchaseInvoiceId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(g => g.PurchaseInvoice).WithMany().HasForeignKey(g => g.PurchaseInvoiceId).OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(g => g.Status); builder.HasIndex(g => g.Status);
builder.HasIndex(g => g.PoId); builder.HasIndex(g => g.PoId);
builder.HasIndex(g => g.BranchId);
} }
} }
@@ -30,6 +30,11 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration<SalesIn
.HasForeignKey(x => x.WarehouseId) .HasForeignKey(x => x.WarehouseId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
builder.HasOne(x => x.Branch)
.WithMany()
.HasForeignKey(x => x.BranchId)
.OnDelete(DeleteBehavior.Restrict);
builder.Property(x => x.InvoiceType) builder.Property(x => x.InvoiceType)
.HasConversion<string>().HasMaxLength(20).IsRequired() .HasConversion<string>().HasMaxLength(20).IsRequired()
.HasDefaultValue(SalesInvoiceType.B2C); .HasDefaultValue(SalesInvoiceType.B2C);
@@ -47,6 +52,7 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration<SalesIn
builder.HasIndex(x => x.Status); builder.HasIndex(x => x.Status);
builder.HasIndex(x => x.InvoiceDate); builder.HasIndex(x => x.InvoiceDate);
builder.HasIndex(x => x.BranchId);
builder.HasMany(x => x.Lines) builder.HasMany(x => x.Lines)
.WithOne(x => x.SalesInvoice) .WithOne(x => x.SalesInvoice)
@@ -21,7 +21,10 @@ public sealed class StockAdjustmentConfiguration : IEntityTypeConfiguration<Stoc
builder.HasOne(a => a.Warehouse).WithMany().HasForeignKey(a => a.WarehouseId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(a => a.Warehouse).WithMany().HasForeignKey(a => a.WarehouseId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(a => a.ReasonCode).WithMany().HasForeignKey(a => a.ReasonCodeId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(a => a.ReasonCode).WithMany().HasForeignKey(a => a.ReasonCodeId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(a => a.Branch).WithMany().HasForeignKey(a => a.BranchId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(a => a.Creator).WithMany().HasForeignKey(a => a.CreatedBy).OnDelete(DeleteBehavior.Restrict); builder.HasOne(a => a.Creator).WithMany().HasForeignKey(a => a.CreatedBy).OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(a => a.BranchId);
} }
} }
@@ -20,9 +20,11 @@ public sealed class StockCountConfiguration : IEntityTypeConfiguration<StockCoun
builder.Property(c => c.RowVersion).IsRowVersion(); builder.Property(c => c.RowVersion).IsRowVersion();
builder.HasOne(c => c.Warehouse).WithMany().HasForeignKey(c => c.WarehouseId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(c => c.Warehouse).WithMany().HasForeignKey(c => c.WarehouseId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(c => c.Branch).WithMany().HasForeignKey(c => c.BranchId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(c => c.Creator).WithMany().HasForeignKey(c => c.CreatedBy).OnDelete(DeleteBehavior.Restrict); builder.HasOne(c => c.Creator).WithMany().HasForeignKey(c => c.CreatedBy).OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(c => c.Status); builder.HasIndex(c => c.Status);
builder.HasIndex(c => c.BranchId);
} }
} }
@@ -20,9 +20,11 @@ public sealed class StockTransferConfiguration : IEntityTypeConfiguration<StockT
builder.HasOne(t => t.SrcWarehouse).WithMany().HasForeignKey(t => t.SrcWarehouseId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(t => t.SrcWarehouse).WithMany().HasForeignKey(t => t.SrcWarehouseId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(t => t.DestWarehouse).WithMany().HasForeignKey(t => t.DestWarehouseId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(t => t.DestWarehouse).WithMany().HasForeignKey(t => t.DestWarehouseId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(t => t.Branch).WithMany().HasForeignKey(t => t.BranchId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(t => t.Creator).WithMany().HasForeignKey(t => t.CreatedBy).OnDelete(DeleteBehavior.Restrict); builder.HasOne(t => t.Creator).WithMany().HasForeignKey(t => t.CreatedBy).OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(t => t.Status); builder.HasIndex(t => t.Status);
builder.HasIndex(t => t.BranchId);
} }
} }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,199 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ERPCore.Migrations
{
/// <inheritdoc />
public partial class AddBranchIdToDocuments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "BranchId",
table: "stock_transfers",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "BranchId",
table: "stock_counts",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "BranchId",
table: "stock_adjustments",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "BranchId",
table: "sales_invoices",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "BranchId",
table: "grns",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_stock_transfers_BranchId",
table: "stock_transfers",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_stock_counts_BranchId",
table: "stock_counts",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_stock_adjustments_BranchId",
table: "stock_adjustments",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_sales_invoices_BranchId",
table: "sales_invoices",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_grns_BranchId",
table: "grns",
column: "BranchId");
// Backfill from each document's existing warehouse — warehouses were the sole
// branch anchor until now, so this preserves current branch assignment exactly.
// (Warehouse.BranchId is NOT NULL, so every row is resolvable; the WHERE BranchId = 0
// guard just keeps this idempotent if the migration is ever re-run against partial state.)
migrationBuilder.Sql(@"
UPDATE grns g SET ""BranchId"" = w.""BranchId""
FROM warehouses w WHERE w.""WarehouseId"" = g.""WarehouseId"" AND g.""BranchId"" = 0;
UPDATE sales_invoices x SET ""BranchId"" = w.""BranchId""
FROM warehouses w WHERE w.""WarehouseId"" = x.""WarehouseId"" AND x.""BranchId"" = 0;
UPDATE stock_counts c SET ""BranchId"" = w.""BranchId""
FROM warehouses w WHERE w.""WarehouseId"" = c.""WarehouseId"" AND c.""BranchId"" = 0;
UPDATE stock_adjustments a SET ""BranchId"" = w.""BranchId""
FROM warehouses w WHERE w.""WarehouseId"" = a.""WarehouseId"" AND a.""BranchId"" = 0;
UPDATE stock_transfers t SET ""BranchId"" = w.""BranchId""
FROM warehouses w WHERE w.""WarehouseId"" = t.""SrcWarehouseId"" AND t.""BranchId"" = 0;
");
migrationBuilder.AddForeignKey(
name: "FK_grns_hr_branches_BranchId",
table: "grns",
column: "BranchId",
principalTable: "hr_branches",
principalColumn: "BranchId",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_sales_invoices_hr_branches_BranchId",
table: "sales_invoices",
column: "BranchId",
principalTable: "hr_branches",
principalColumn: "BranchId",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_stock_adjustments_hr_branches_BranchId",
table: "stock_adjustments",
column: "BranchId",
principalTable: "hr_branches",
principalColumn: "BranchId",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_stock_counts_hr_branches_BranchId",
table: "stock_counts",
column: "BranchId",
principalTable: "hr_branches",
principalColumn: "BranchId",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_stock_transfers_hr_branches_BranchId",
table: "stock_transfers",
column: "BranchId",
principalTable: "hr_branches",
principalColumn: "BranchId",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_grns_hr_branches_BranchId",
table: "grns");
migrationBuilder.DropForeignKey(
name: "FK_sales_invoices_hr_branches_BranchId",
table: "sales_invoices");
migrationBuilder.DropForeignKey(
name: "FK_stock_adjustments_hr_branches_BranchId",
table: "stock_adjustments");
migrationBuilder.DropForeignKey(
name: "FK_stock_counts_hr_branches_BranchId",
table: "stock_counts");
migrationBuilder.DropForeignKey(
name: "FK_stock_transfers_hr_branches_BranchId",
table: "stock_transfers");
migrationBuilder.DropIndex(
name: "IX_stock_transfers_BranchId",
table: "stock_transfers");
migrationBuilder.DropIndex(
name: "IX_stock_counts_BranchId",
table: "stock_counts");
migrationBuilder.DropIndex(
name: "IX_stock_adjustments_BranchId",
table: "stock_adjustments");
migrationBuilder.DropIndex(
name: "IX_sales_invoices_BranchId",
table: "sales_invoices");
migrationBuilder.DropIndex(
name: "IX_grns_BranchId",
table: "grns");
migrationBuilder.DropColumn(
name: "BranchId",
table: "stock_transfers");
migrationBuilder.DropColumn(
name: "BranchId",
table: "stock_counts");
migrationBuilder.DropColumn(
name: "BranchId",
table: "stock_adjustments");
migrationBuilder.DropColumn(
name: "BranchId",
table: "sales_invoices");
migrationBuilder.DropColumn(
name: "BranchId",
table: "grns");
}
}
}
@@ -1447,6 +1447,9 @@ namespace ERPCore.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("GrnId")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("GrnId"));
b.Property<int>("BranchId")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -1493,6 +1496,8 @@ namespace ERPCore.Migrations
b.HasKey("GrnId"); b.HasKey("GrnId");
b.HasIndex("BranchId");
b.HasIndex("CreatedBy"); b.HasIndex("CreatedBy");
b.HasIndex("DocNo") b.HasIndex("DocNo")
@@ -4187,6 +4192,9 @@ namespace ERPCore.Migrations
.HasPrecision(18, 4) .HasPrecision(18, 4)
.HasColumnType("numeric(18,4)"); .HasColumnType("numeric(18,4)");
b.Property<int>("BranchId")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -4279,6 +4287,8 @@ namespace ERPCore.Migrations
b.HasKey("SalesInvoiceId"); b.HasKey("SalesInvoiceId");
b.HasIndex("BranchId");
b.HasIndex("CreatorUserId"); b.HasIndex("CreatorUserId");
b.HasIndex("CustomerId"); b.HasIndex("CustomerId");
@@ -4859,6 +4869,9 @@ namespace ERPCore.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("AdjustmentId")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("AdjustmentId"));
b.Property<int>("BranchId")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -4896,6 +4909,8 @@ namespace ERPCore.Migrations
b.HasKey("AdjustmentId"); b.HasKey("AdjustmentId");
b.HasIndex("BranchId");
b.HasIndex("CreatedBy"); b.HasIndex("CreatedBy");
b.HasIndex("DocNo") b.HasIndex("DocNo")
@@ -4958,6 +4973,9 @@ namespace ERPCore.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("CountId")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("CountId"));
b.Property<int>("BranchId")
.HasColumnType("integer");
b.Property<string>("CountType") b.Property<string>("CountType")
.IsRequired() .IsRequired()
.HasMaxLength(20) .HasMaxLength(20)
@@ -4990,6 +5008,8 @@ namespace ERPCore.Migrations
b.HasKey("CountId"); b.HasKey("CountId");
b.HasIndex("BranchId");
b.HasIndex("CreatedBy"); b.HasIndex("CreatedBy");
b.HasIndex("DocNo") b.HasIndex("DocNo")
@@ -5182,6 +5202,9 @@ namespace ERPCore.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("TransferId")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("TransferId"));
b.Property<int>("BranchId")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -5212,6 +5235,8 @@ namespace ERPCore.Migrations
b.HasKey("TransferId"); b.HasKey("TransferId");
b.HasIndex("BranchId");
b.HasIndex("CreatedBy"); b.HasIndex("CreatedBy");
b.HasIndex("DestWarehouseId"); b.HasIndex("DestWarehouseId");
@@ -6380,6 +6405,12 @@ namespace ERPCore.Migrations
modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b =>
{ {
b.HasOne("ERPCore.Domain.Entities.Branch", "Branch")
.WithMany()
.HasForeignKey("BranchId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.User", "Creator") b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany() .WithMany()
.HasForeignKey("CreatedBy") .HasForeignKey("CreatedBy")
@@ -6408,6 +6439,8 @@ namespace ERPCore.Migrations
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Restrict)
.IsRequired(); .IsRequired();
b.Navigation("Branch");
b.Navigation("Creator"); b.Navigation("Creator");
b.Navigation("PurchaseInvoice"); b.Navigation("PurchaseInvoice");
@@ -7142,6 +7175,12 @@ namespace ERPCore.Migrations
modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b =>
{ {
b.HasOne("ERPCore.Domain.Entities.Branch", "Branch")
.WithMany()
.HasForeignKey("BranchId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.User", "Creator") b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany() .WithMany()
.HasForeignKey("CreatorUserId"); .HasForeignKey("CreatorUserId");
@@ -7158,6 +7197,8 @@ namespace ERPCore.Migrations
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Restrict)
.IsRequired(); .IsRequired();
b.Navigation("Branch");
b.Navigation("Creator"); b.Navigation("Creator");
b.Navigation("Customer"); b.Navigation("Customer");
@@ -7423,6 +7464,12 @@ namespace ERPCore.Migrations
modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b =>
{ {
b.HasOne("ERPCore.Domain.Entities.Branch", "Branch")
.WithMany()
.HasForeignKey("BranchId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.User", "Creator") b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany() .WithMany()
.HasForeignKey("CreatedBy") .HasForeignKey("CreatedBy")
@@ -7441,6 +7488,8 @@ namespace ERPCore.Migrations
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Restrict)
.IsRequired(); .IsRequired();
b.Navigation("Branch");
b.Navigation("Creator"); b.Navigation("Creator");
b.Navigation("ReasonCode"); b.Navigation("ReasonCode");
@@ -7484,6 +7533,12 @@ namespace ERPCore.Migrations
modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b =>
{ {
b.HasOne("ERPCore.Domain.Entities.Branch", "Branch")
.WithMany()
.HasForeignKey("BranchId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.User", "Creator") b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany() .WithMany()
.HasForeignKey("CreatedBy") .HasForeignKey("CreatedBy")
@@ -7496,6 +7551,8 @@ namespace ERPCore.Migrations
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Restrict)
.IsRequired(); .IsRequired();
b.Navigation("Branch");
b.Navigation("Creator"); b.Navigation("Creator");
b.Navigation("Warehouse"); b.Navigation("Warehouse");
@@ -7603,6 +7660,12 @@ namespace ERPCore.Migrations
modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b =>
{ {
b.HasOne("ERPCore.Domain.Entities.Branch", "Branch")
.WithMany()
.HasForeignKey("BranchId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.User", "Creator") b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany() .WithMany()
.HasForeignKey("CreatedBy") .HasForeignKey("CreatedBy")
@@ -7621,6 +7684,8 @@ namespace ERPCore.Migrations
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Restrict)
.IsRequired(); .IsRequired();
b.Navigation("Branch");
b.Navigation("Creator"); b.Navigation("Creator");
b.Navigation("DestWarehouse"); b.Navigation("DestWarehouse");
+10 -4
View File
@@ -32,6 +32,7 @@ public sealed class AdjustmentService : IAdjustmentService
private readonly IStockMutator _mutator; private readonly IStockMutator _mutator;
private readonly INumberSequenceService _numbers; private readonly INumberSequenceService _numbers;
private readonly ICurrentUser _currentUser; private readonly ICurrentUser _currentUser;
private readonly IBranchConfigService _branchConfig;
private readonly IUnitOfWork _uow; private readonly IUnitOfWork _uow;
private readonly IGeneralLedgerService _gl; private readonly IGeneralLedgerService _gl;
private readonly string _glInventoryAccountCode; private readonly string _glInventoryAccountCode;
@@ -41,7 +42,7 @@ public sealed class AdjustmentService : IAdjustmentService
public AdjustmentService( public AdjustmentService(
IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses, IRepository<Item> items, IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses, IRepository<Item> items,
IRepository<ReasonCode> reasonCodes, IRepository<StockLedger> ledger, IStockMutator mutator, IRepository<ReasonCode> reasonCodes, IRepository<StockLedger> ledger, IStockMutator mutator,
INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow, INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, IUnitOfWork uow,
IGeneralLedgerService gl, IConfiguration configuration) IGeneralLedgerService gl, IConfiguration configuration)
{ {
_adjustments = adjustments; _adjustments = adjustments;
@@ -52,6 +53,7 @@ public sealed class AdjustmentService : IAdjustmentService
_mutator = mutator; _mutator = mutator;
_numbers = numbers; _numbers = numbers;
_currentUser = currentUser; _currentUser = currentUser;
_branchConfig = branchConfig;
_uow = uow; _uow = uow;
_gl = gl; _gl = gl;
_glInventoryAccountCode = configuration["Adjustment:GlInventoryAccountCode"] ?? string.Empty; _glInventoryAccountCode = configuration["Adjustment:GlInventoryAccountCode"] ?? string.Empty;
@@ -105,14 +107,15 @@ public sealed class AdjustmentService : IAdjustmentService
q = q.Where(a => EF.Functions.ILike(a.DocNo, $"%{term}%")); q = q.Where(a => EF.Functions.ILike(a.DocNo, $"%{term}%"));
} }
if (warehouseId is not null) q = q.Where(a => a.WarehouseId == warehouseId); if (warehouseId is not null) q = q.Where(a => a.WarehouseId == warehouseId);
if (branchId is not null) q = q.Where(a => a.Warehouse!.BranchId == branchId); // Warehouses are shared across branches — the adjustment carries its own owning BranchId.
if (branchId is not null) q = q.Where(a => a.BranchId == branchId);
if (reasonCodeId is not null) q = q.Where(a => a.ReasonCodeId == reasonCodeId); if (reasonCodeId is not null) q = q.Where(a => a.ReasonCodeId == reasonCodeId);
var total = await q.CountAsync(ct); var total = await q.CountAsync(ct);
var rows = await q.OrderByDescending(a => a.AdjustmentId) var rows = await q.OrderByDescending(a => a.AdjustmentId)
.Skip(query.Skip).Take(query.PageSize) .Skip(query.Skip).Take(query.PageSize)
.Select(a => new AdjustmentSummaryDto( .Select(a => new AdjustmentSummaryDto(
a.AdjustmentId, a.DocNo, a.WarehouseId, a.ReasonCodeId, a.Status, a.AdjustmentId, a.DocNo, a.WarehouseId, a.BranchId, a.ReasonCodeId, a.Status,
a.CreatedBy, a.CreatedAt, a.Lines.Count, a.GlJournalNo)) a.CreatedBy, a.CreatedAt, a.Lines.Count, a.GlJournalNo))
.ToListAsync(ct); .ToListAsync(ct);
@@ -145,6 +148,8 @@ public sealed class AdjustmentService : IAdjustmentService
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct)) if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422); throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422);
var branchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
var reason = await _reasonCodes.Query().AsNoTracking().FirstOrDefaultAsync(r => r.ReasonCodeId == request.ReasonCodeId, ct) var reason = await _reasonCodes.Query().AsNoTracking().FirstOrDefaultAsync(r => r.ReasonCodeId == request.ReasonCodeId, ct)
?? throw new DomainException(ErrorCodes.Validation, $"Reason code {request.ReasonCodeId} does not exist.", 422); ?? throw new DomainException(ErrorCodes.Validation, $"Reason code {request.ReasonCodeId} does not exist.", 422);
if (reason.Context != ReasonContext.Adjustment) if (reason.Context != ReasonContext.Adjustment)
@@ -169,6 +174,7 @@ public sealed class AdjustmentService : IAdjustmentService
{ {
DocNo = docNo, DocNo = docNo,
WarehouseId = request.WarehouseId, WarehouseId = request.WarehouseId,
BranchId = branchId,
ReasonCodeId = request.ReasonCodeId.Value, ReasonCodeId = request.ReasonCodeId.Value,
Status = AdjustmentStatus.Posted, Status = AdjustmentStatus.Posted,
CreatedBy = _currentUser.AuditUserId, CreatedBy = _currentUser.AuditUserId,
@@ -196,7 +202,7 @@ public sealed class AdjustmentService : IAdjustmentService
} }
private static AdjustmentDto ToDto(StockAdjustment a, IReadOnlyList<int> ledgerRefs) => new( private static AdjustmentDto ToDto(StockAdjustment a, IReadOnlyList<int> ledgerRefs) => new(
a.AdjustmentId, a.DocNo, a.WarehouseId, a.ReasonCodeId, a.Status, a.CreatedBy, a.CreatedAt, a.AdjustmentId, a.DocNo, a.WarehouseId, a.BranchId, a.ReasonCodeId, a.Status, a.CreatedBy, a.CreatedAt,
a.GlJournalNo, a.GlPostedAt, a.GlJournalNo, a.GlPostedAt,
a.Lines.OrderBy(l => l.AdjLineId) a.Lines.OrderBy(l => l.AdjLineId)
.Select(l => new AdjustmentLineDto(l.AdjLineId, l.ItemId, l.BinId, l.BatchId, l.QtyDelta)).ToList(), .Select(l => new AdjustmentLineDto(l.AdjLineId, l.ItemId, l.BinId, l.BatchId, l.QtyDelta)).ToList(),
+11 -4
View File
@@ -34,6 +34,7 @@ public sealed class CountService : ICountService
private readonly IStockMutator _mutator; private readonly IStockMutator _mutator;
private readonly INumberSequenceService _numbers; private readonly INumberSequenceService _numbers;
private readonly ICurrentUser _currentUser; private readonly ICurrentUser _currentUser;
private readonly IBranchConfigService _branchConfig;
private readonly IUnitOfWork _uow; private readonly IUnitOfWork _uow;
private readonly IGeneralLedgerService _gl; private readonly IGeneralLedgerService _gl;
private readonly string _glInventoryAccountCode; private readonly string _glInventoryAccountCode;
@@ -43,7 +44,7 @@ public sealed class CountService : ICountService
public CountService( public CountService(
IRepository<StockCount> counts, IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses, IRepository<StockCount> counts, IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses,
IRepository<Item> items, IRepository<ReasonCode> reasonCodes, IFifoCostingService fifo, IStockMutator mutator, IRepository<Item> items, IRepository<ReasonCode> reasonCodes, IFifoCostingService fifo, IStockMutator mutator,
INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow, INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, IUnitOfWork uow,
IGeneralLedgerService gl, IConfiguration configuration) IGeneralLedgerService gl, IConfiguration configuration)
{ {
_counts = counts; _counts = counts;
@@ -55,6 +56,7 @@ public sealed class CountService : ICountService
_mutator = mutator; _mutator = mutator;
_numbers = numbers; _numbers = numbers;
_currentUser = currentUser; _currentUser = currentUser;
_branchConfig = branchConfig;
_uow = uow; _uow = uow;
_gl = gl; _gl = gl;
_glInventoryAccountCode = configuration["Adjustment:GlInventoryAccountCode"] ?? string.Empty; _glInventoryAccountCode = configuration["Adjustment:GlInventoryAccountCode"] ?? string.Empty;
@@ -108,13 +110,14 @@ public sealed class CountService : ICountService
} }
if (status is not null) q = q.Where(c => c.Status == status); if (status is not null) q = q.Where(c => c.Status == status);
if (warehouseId is not null) q = q.Where(c => c.WarehouseId == warehouseId); if (warehouseId is not null) q = q.Where(c => c.WarehouseId == warehouseId);
if (branchId is not null) q = q.Where(c => c.Warehouse!.BranchId == branchId); // Warehouses are shared across branches — the count carries its own owning BranchId.
if (branchId is not null) q = q.Where(c => c.BranchId == branchId);
var total = await q.CountAsync(ct); var total = await q.CountAsync(ct);
var rows = await q.OrderByDescending(c => c.CountId) var rows = await q.OrderByDescending(c => c.CountId)
.Skip(query.Skip).Take(query.PageSize) .Skip(query.Skip).Take(query.PageSize)
.Select(c => new CountSummaryDto( .Select(c => new CountSummaryDto(
c.CountId, c.DocNo, c.WarehouseId, c.CountType, c.Status, c.CountId, c.DocNo, c.WarehouseId, c.BranchId, c.CountType, c.Status,
c.CreatedBy, c.CreatedAt, c.Lines.Count)) c.CreatedBy, c.CreatedAt, c.Lines.Count))
.ToListAsync(ct); .ToListAsync(ct);
@@ -133,6 +136,8 @@ public sealed class CountService : ICountService
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct)) if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422); throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422);
var branchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
var itemIds = request.ItemIds.Distinct().ToList(); var itemIds = request.ItemIds.Distinct().ToList();
foreach (var id in itemIds) foreach (var id in itemIds)
if (!await _items.Query().AnyAsync(i => i.ItemId == id, ct)) if (!await _items.Query().AnyAsync(i => i.ItemId == id, ct))
@@ -153,6 +158,7 @@ public sealed class CountService : ICountService
{ {
DocNo = docNo, DocNo = docNo,
WarehouseId = request.WarehouseId, WarehouseId = request.WarehouseId,
BranchId = branchId,
CountType = request.CountType, CountType = request.CountType,
Status = CountStatus.Draft, Status = CountStatus.Draft,
CreatedBy = _currentUser.AuditUserId, CreatedBy = _currentUser.AuditUserId,
@@ -222,6 +228,7 @@ public sealed class CountService : ICountService
{ {
DocNo = docNo, DocNo = docNo,
WarehouseId = count.WarehouseId, WarehouseId = count.WarehouseId,
BranchId = count.BranchId,
ReasonCodeId = reason.ReasonCodeId, ReasonCodeId = reason.ReasonCodeId,
Status = AdjustmentStatus.Posted, Status = AdjustmentStatus.Posted,
CreatedBy = _currentUser.AuditUserId, CreatedBy = _currentUser.AuditUserId,
@@ -250,7 +257,7 @@ public sealed class CountService : ICountService
} }
private static CountDto Map(StockCount c) => new( private static CountDto Map(StockCount c) => new(
c.CountId, c.DocNo, c.WarehouseId, c.CountType, c.Status, c.CreatedBy, c.CreatedAt, c.CountId, c.DocNo, c.WarehouseId, c.BranchId, c.CountType, c.Status, c.CreatedBy, c.CreatedAt,
c.Lines.OrderBy(l => l.CountLineId) c.Lines.OrderBy(l => l.CountLineId)
.Select(l => new CountLineDto(l.CountLineId, l.ItemId, l.BinId, l.SystemQty, l.CountedQty, l.Variance)).ToList()); .Select(l => new CountLineDto(l.CountLineId, l.ItemId, l.BinId, l.SystemQty, l.CountedQty, l.Variance)).ToList());
} }
+2 -2
View File
@@ -62,11 +62,11 @@ public sealed class DashboardService : IDashboardService
.ToList(); .ToList();
var grns = _grns.Query().AsNoTracking(); var grns = _grns.Query().AsNoTracking();
if (branchId is not null) grns = grns.Where(g => g.Warehouse!.BranchId == branchId); if (branchId is not null) grns = grns.Where(g => g.BranchId == branchId);
var pendingGrns = await grns.CountAsync(g => g.Status == GrnStatus.Draft, ct); var pendingGrns = await grns.CountAsync(g => g.Status == GrnStatus.Draft, ct);
var counts = _counts.Query().AsNoTracking(); var counts = _counts.Query().AsNoTracking();
if (branchId is not null) counts = counts.Where(c => c.Warehouse!.BranchId == branchId); if (branchId is not null) counts = counts.Where(c => c.BranchId == branchId);
var pendingCounts = await counts.CountAsync(c => c.Status == CountStatus.Counted, ct); var pendingCounts = await counts.CountAsync(c => c.Status == CountStatus.Counted, ct);
// PurchaseOrder/Requisition/Rfq carry no warehouse/branch at header level yet // PurchaseOrder/Requisition/Rfq carry no warehouse/branch at header level yet
+11 -7
View File
@@ -38,6 +38,7 @@ public sealed class GrnService : IGrnService
private readonly IFifoCostingService _fifo; private readonly IFifoCostingService _fifo;
private readonly INumberSequenceService _numbers; private readonly INumberSequenceService _numbers;
private readonly ICurrentUser _currentUser; private readonly ICurrentUser _currentUser;
private readonly IBranchConfigService _branchConfig;
private readonly IUnitOfWork _uow; private readonly IUnitOfWork _uow;
private readonly IGeneralLedgerService _gl; private readonly IGeneralLedgerService _gl;
private readonly string _glInventoryAccountCode; private readonly string _glInventoryAccountCode;
@@ -49,8 +50,8 @@ public sealed class GrnService : IGrnService
IRepository<Item> items, IRepository<Warehouse> warehouses, IRepository<Item> items, IRepository<Warehouse> warehouses,
IRepository<Bin> bins, IRepository<Vendor> vendors, IRepository<Batch> batches, IRepository<Bin> bins, IRepository<Vendor> vendors, IRepository<Batch> batches,
IRepository<StockLayer> layers, IRepository<StockLedger> ledger, IRepository<StockLayer> layers, IRepository<StockLedger> ledger,
IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow, IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig,
IGeneralLedgerService gl, IConfiguration configuration) IUnitOfWork uow, IGeneralLedgerService gl, IConfiguration configuration)
{ {
_grns = grns; _grns = grns;
_pos = pos; _pos = pos;
@@ -65,6 +66,7 @@ public sealed class GrnService : IGrnService
_fifo = fifo; _fifo = fifo;
_numbers = numbers; _numbers = numbers;
_currentUser = currentUser; _currentUser = currentUser;
_branchConfig = branchConfig;
_uow = uow; _uow = uow;
_gl = gl; _gl = gl;
_glInventoryAccountCode = configuration["Grn:GlInventoryAccountCode"] ?? string.Empty; _glInventoryAccountCode = configuration["Grn:GlInventoryAccountCode"] ?? string.Empty;
@@ -86,15 +88,14 @@ public sealed class GrnService : IGrnService
if (poId is not null) q = q.Where(g => g.PoId == poId); if (poId is not null) q = q.Where(g => g.PoId == poId);
if (vendorId is not null) q = q.Where(g => g.VendorId == vendorId); if (vendorId is not null) q = q.Where(g => g.VendorId == vendorId);
if (warehouseId is not null) q = q.Where(g => g.WarehouseId == warehouseId); if (warehouseId is not null) q = q.Where(g => g.WarehouseId == warehouseId);
// GRN carries no BranchId of its own — it inherits branch scope transitively via its // Warehouses are shared across branches — GRN carries its own BranchId, set at creation.
// Warehouse, same as Sales (docs/BRANCH_ISOLATION_PLAN.md "Anchor mechanism"). if (branchId is not null) q = q.Where(g => g.BranchId == branchId);
if (branchId is not null) q = q.Where(g => g.Warehouse!.BranchId == branchId);
var total = await q.CountAsync(ct); var total = await q.CountAsync(ct);
var rows = await q.OrderByDescending(g => g.GrnId) var rows = await q.OrderByDescending(g => g.GrnId)
.Skip(query.Skip).Take(query.PageSize) .Skip(query.Skip).Take(query.PageSize)
.Select(g => new GrnSummaryDto( .Select(g => new GrnSummaryDto(
g.GrnId, g.DocNo, g.PoId, g.VendorId, g.WarehouseId, g.Status, g.GrnId, g.DocNo, g.PoId, g.VendorId, g.WarehouseId, g.BranchId, g.Status,
g.CreatedBy, g.CreatedAt, g.PostedAt, g.Lines.Count, g.CreatedBy, g.CreatedAt, g.PostedAt, g.Lines.Count,
g.PurchaseInvoiceId, g.PurchaseInvoice != null ? g.PurchaseInvoice.DocNo : null)) g.PurchaseInvoiceId, g.PurchaseInvoice != null ? g.PurchaseInvoice.DocNo : null))
.ToListAsync(ct); .ToListAsync(ct);
@@ -116,6 +117,8 @@ public sealed class GrnService : IGrnService
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct)) if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422); throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422);
var branchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
// Resolve vendor + PO context. // Resolve vendor + PO context.
PurchaseOrder? po = null; PurchaseOrder? po = null;
int vendorId; int vendorId;
@@ -211,6 +214,7 @@ public sealed class GrnService : IGrnService
PoId = request.PoId, PoId = request.PoId,
VendorId = vendorId, VendorId = vendorId,
WarehouseId = request.WarehouseId, WarehouseId = request.WarehouseId,
BranchId = branchId,
Status = GrnStatus.Draft, Status = GrnStatus.Draft,
CreatedBy = _currentUser.AuditUserId, CreatedBy = _currentUser.AuditUserId,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
@@ -448,7 +452,7 @@ public sealed class GrnService : IGrnService
l.LayerId, l.ItemId, l.WarehouseId, l.BatchId, l.QtyReceived, l.QtyRemaining, l.UnitCost, l.ReceiptDate); l.LayerId, l.ItemId, l.WarehouseId, l.BatchId, l.QtyReceived, l.QtyRemaining, l.UnitCost, l.ReceiptDate);
private static GrnDto Map(Grn g) => new( private static GrnDto Map(Grn g) => new(
g.GrnId, g.DocNo, g.PoId, g.VendorId, g.WarehouseId, g.Status, g.CreatedBy, g.CreatedAt, g.PostedAt, g.GrnId, g.DocNo, g.PoId, g.VendorId, g.WarehouseId, g.BranchId, g.Status, g.CreatedBy, g.CreatedAt, g.PostedAt,
g.GlJournalNo, g.PurchaseInvoiceId, g.PurchaseInvoice?.DocNo, g.GlJournalNo, g.PurchaseInvoiceId, g.PurchaseInvoice?.DocNo,
g.Lines.OrderBy(l => l.GrnLineId).Select(l => new GrnLineDto( g.Lines.OrderBy(l => l.GrnLineId).Select(l => new GrnLineDto(
l.GrnLineId, l.PoLineId, l.ItemId, l.BinId, l.Qty, l.UnitCost, l.PoUnitPrice, l.GrnLineId, l.PoLineId, l.ItemId, l.BinId, l.Qty, l.UnitCost, l.PoUnitPrice,
@@ -26,6 +26,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
private readonly ISalesMappingService _mapping; private readonly ISalesMappingService _mapping;
private readonly ISalesDocumentWorkflowService _workflow; private readonly ISalesDocumentWorkflowService _workflow;
private readonly ICurrentUser _currentUser; private readonly ICurrentUser _currentUser;
private readonly IBranchConfigService _branchConfig;
private readonly INumberSequenceService _numbers; private readonly INumberSequenceService _numbers;
private readonly IUnitOfWork _uow; private readonly IUnitOfWork _uow;
@@ -33,7 +34,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
IRepository<SalesInvoice> invoices, IRepository<Customer> customers, IRepository<Item> items, IRepository<SalesInvoice> invoices, IRepository<Customer> customers, IRepository<Item> items,
IRepository<Uom> uoms, IRepository<Warehouse> warehouses, ISalesDomainService sales, ISalesPostingService posting, ISalesMappingService mapping, IRepository<Uom> uoms, IRepository<Warehouse> warehouses, ISalesDomainService sales, ISalesPostingService posting, ISalesMappingService mapping,
ISalesDocumentWorkflowService workflow, ISalesDocumentWorkflowService workflow,
ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow) ICurrentUser currentUser, IBranchConfigService branchConfig, INumberSequenceService numbers, IUnitOfWork uow)
{ {
_invoices = invoices; _invoices = invoices;
_customers = customers; _customers = customers;
@@ -45,6 +46,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
_mapping = mapping; _mapping = mapping;
_workflow = workflow; _workflow = workflow;
_currentUser = currentUser; _currentUser = currentUser;
_branchConfig = branchConfig;
_numbers = numbers; _numbers = numbers;
_uow = uow; _uow = uow;
} }
@@ -60,9 +62,8 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
if (status is not null) q = q.Where(x => x.Status == status); if (status is not null) q = q.Where(x => x.Status == status);
if (customerId is not null) q = q.Where(x => x.CustomerId == customerId); if (customerId is not null) q = q.Where(x => x.CustomerId == customerId);
if (warehouseId is not null) q = q.Where(x => x.WarehouseId == warehouseId); if (warehouseId is not null) q = q.Where(x => x.WarehouseId == warehouseId);
// Sales carries no BranchId of its own — it inherits branch scope transitively via // Warehouses are shared across branches — the invoice carries its own owning BranchId.
// its Warehouse (docs/BRANCH_ISOLATION_PLAN.md "Anchor mechanism: Warehouse carries the branch"). if (branchId is not null) q = q.Where(x => x.BranchId == branchId);
if (branchId is not null) q = q.Where(x => x.Warehouse!.BranchId == branchId);
var total = await q.CountAsync(ct); var total = await q.CountAsync(ct);
var rows = await q.OrderByDescending(x => x.SalesInvoiceId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct); var rows = await q.OrderByDescending(x => x.SalesInvoiceId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
@@ -83,6 +84,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
public async Task<ETagged<SalesInvoiceDto>> CreateAsync(CreateSalesInvoiceRequest request, CancellationToken ct = default) public async Task<ETagged<SalesInvoiceDto>> CreateAsync(CreateSalesInvoiceRequest request, CancellationToken ct = default)
{ {
await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, null, false, ct); await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, null, false, ct);
var branchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
var customerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct); var customerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct);
var customerSnapshotTaxNo = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.TaxRegistrationNo).FirstAsync(ct); var customerSnapshotTaxNo = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.TaxRegistrationNo).FirstAsync(ct);
@@ -96,6 +98,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
InvoiceDate = DateTime.UtcNow, InvoiceDate = DateTime.UtcNow,
CustomerId = request.CustomerId, CustomerId = request.CustomerId,
WarehouseId = request.WarehouseId, WarehouseId = request.WarehouseId,
BranchId = branchId,
InvoiceType = request.InvoiceType, InvoiceType = request.InvoiceType,
Status = SalesInvoiceStatus.Draft, Status = SalesInvoiceStatus.Draft,
CreatedBy = _currentUser.AuditUserId, CreatedBy = _currentUser.AuditUserId,
@@ -119,6 +122,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, null, false, ct); await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, null, false, ct);
invoice.CustomerId = request.CustomerId; invoice.CustomerId = request.CustomerId;
invoice.WarehouseId = request.WarehouseId; invoice.WarehouseId = request.WarehouseId;
invoice.BranchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
invoice.InvoiceType = request.InvoiceType; invoice.InvoiceType = request.InvoiceType;
invoice.CustomerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct); invoice.CustomerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct);
invoice.CustomerSnapshotTaxNo = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.TaxRegistrationNo).FirstAsync(ct); invoice.CustomerSnapshotTaxNo = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.TaxRegistrationNo).FirstAsync(ct);
@@ -201,5 +205,5 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
private SalesInvoiceSummaryDto MapSummary(SalesInvoice x) => new( private SalesInvoiceSummaryDto MapSummary(SalesInvoice x) => new(
x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName, x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName,
x.WarehouseId, x.InvoiceType, x.Status, _mapping.MapInvoiceTotals(x), x.CreatedAt, x.GlJournalNo); x.WarehouseId, x.BranchId, x.InvoiceType, x.Status, _mapping.MapInvoiceTotals(x), x.CreatedAt, x.GlJournalNo);
} }
@@ -31,7 +31,7 @@ public sealed class SalesMappingService : ISalesMappingService
public SalesInvoiceDto MapInvoice(SalesInvoice invoice) public SalesInvoiceDto MapInvoice(SalesInvoice invoice)
=> new( => new(
invoice.SalesInvoiceId, invoice.InvoiceNo, invoice.InvoiceDate, invoice.CustomerId, invoice.SalesInvoiceId, invoice.InvoiceNo, invoice.InvoiceDate, invoice.CustomerId,
invoice.CustomerSnapshotName, invoice.CustomerSnapshotTaxNo, invoice.WarehouseId, invoice.CustomerSnapshotName, invoice.CustomerSnapshotTaxNo, invoice.WarehouseId, invoice.BranchId,
invoice.InvoiceType, invoice.Status, invoice.CreatedBy, invoice.CreatedAt, invoice.UpdatedAt, invoice.InvoiceType, invoice.Status, invoice.CreatedBy, invoice.CreatedAt, invoice.UpdatedAt,
invoice.GlJournalNo, invoice.GlPostedAt, invoice.GlJournalNo, invoice.GlPostedAt,
MapInvoiceTotals(invoice), MapInvoiceTotals(invoice),
+10 -6
View File
@@ -34,11 +34,13 @@ public sealed class TransferService : ITransferService
private readonly IFifoCostingService _fifo; private readonly IFifoCostingService _fifo;
private readonly INumberSequenceService _numbers; private readonly INumberSequenceService _numbers;
private readonly ICurrentUser _currentUser; private readonly ICurrentUser _currentUser;
private readonly IBranchConfigService _branchConfig;
private readonly IUnitOfWork _uow; private readonly IUnitOfWork _uow;
public TransferService( public TransferService(
IRepository<StockTransfer> transfers, IRepository<Warehouse> warehouses, IRepository<Item> items, IRepository<StockTransfer> transfers, IRepository<Warehouse> warehouses, IRepository<Item> items,
IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow) IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser,
IBranchConfigService branchConfig, IUnitOfWork uow)
{ {
_transfers = transfers; _transfers = transfers;
_warehouses = warehouses; _warehouses = warehouses;
@@ -46,6 +48,7 @@ public sealed class TransferService : ITransferService
_fifo = fifo; _fifo = fifo;
_numbers = numbers; _numbers = numbers;
_currentUser = currentUser; _currentUser = currentUser;
_branchConfig = branchConfig;
_uow = uow; _uow = uow;
} }
@@ -62,15 +65,14 @@ public sealed class TransferService : ITransferService
if (status is not null) q = q.Where(t => t.Status == status); if (status is not null) q = q.Where(t => t.Status == status);
if (srcWarehouseId is not null) q = q.Where(t => t.SrcWarehouseId == srcWarehouseId); if (srcWarehouseId is not null) q = q.Where(t => t.SrcWarehouseId == srcWarehouseId);
if (destWarehouseId is not null) q = q.Where(t => t.DestWarehouseId == destWarehouseId); if (destWarehouseId is not null) q = q.Where(t => t.DestWarehouseId == destWarehouseId);
// A transfer touches two branches when it crosses one — show it on either side // Warehouses are shared across branches — the transfer carries its own owning BranchId.
// rather than only the branch that happens to hold the source warehouse. if (branchId is not null) q = q.Where(t => t.BranchId == branchId);
if (branchId is not null) q = q.Where(t => t.SrcWarehouse!.BranchId == branchId || t.DestWarehouse!.BranchId == branchId);
var total = await q.CountAsync(ct); var total = await q.CountAsync(ct);
var rows = await q.OrderByDescending(t => t.TransferId) var rows = await q.OrderByDescending(t => t.TransferId)
.Skip(query.Skip).Take(query.PageSize) .Skip(query.Skip).Take(query.PageSize)
.Select(t => new TransferSummaryDto( .Select(t => new TransferSummaryDto(
t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.Status, t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.BranchId, t.Status,
t.CreatedBy, t.CreatedAt, t.Lines.Count)) t.CreatedBy, t.CreatedAt, t.Lines.Count))
.ToListAsync(ct); .ToListAsync(ct);
@@ -90,6 +92,7 @@ public sealed class TransferService : ITransferService
throw new DomainException(ErrorCodes.Validation, "destWarehouseId must differ from srcWarehouseId.", 422); throw new DomainException(ErrorCodes.Validation, "destWarehouseId must differ from srcWarehouseId.", 422);
await EnsureWarehouseAsync(request.SrcWarehouseId, ct); await EnsureWarehouseAsync(request.SrcWarehouseId, ct);
await EnsureWarehouseAsync(request.DestWarehouseId, ct); await EnsureWarehouseAsync(request.DestWarehouseId, ct);
var branchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
foreach (var line in request.Lines) foreach (var line in request.Lines)
if (!await _items.Query().AnyAsync(i => i.ItemId == line.ItemId, ct)) if (!await _items.Query().AnyAsync(i => i.ItemId == line.ItemId, ct))
throw new DomainException(ErrorCodes.Validation, $"Item {line.ItemId} does not exist.", 422); throw new DomainException(ErrorCodes.Validation, $"Item {line.ItemId} does not exist.", 422);
@@ -103,6 +106,7 @@ public sealed class TransferService : ITransferService
DocNo = docNo, DocNo = docNo,
SrcWarehouseId = request.SrcWarehouseId, SrcWarehouseId = request.SrcWarehouseId,
DestWarehouseId = request.DestWarehouseId, DestWarehouseId = request.DestWarehouseId,
BranchId = branchId,
Status = TransferStatus.Draft, Status = TransferStatus.Draft,
CreatedBy = actor, CreatedBy = actor,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
@@ -227,7 +231,7 @@ public sealed class TransferService : ITransferService
} }
private static TransferDto Map(StockTransfer t) => new( private static TransferDto Map(StockTransfer t) => new(
t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.Status, t.CreatedBy, t.CreatedAt, t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.BranchId, t.Status, t.CreatedBy, t.CreatedAt,
t.Lines.OrderBy(l => l.TransferLineId).Select(l => new TransferLineDto( t.Lines.OrderBy(l => l.TransferLineId).Select(l => new TransferLineDto(
l.TransferLineId, l.ItemId, l.SrcBinId, l.DestBinId, l.BatchId, l.Qty, l.QtyReceived)).ToList()); l.TransferLineId, l.ItemId, l.SrcBinId, l.DestBinId, l.BatchId, l.Qty, l.QtyReceived)).ToList());
} }
@@ -1,6 +1,6 @@
"use client" "use client"
import { useEffect, useMemo, useState } from "react" import { useEffect, useState } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import Link from "next/link" import Link from "next/link"
import { ArrowLeft, ExternalLink, Plus, RefreshCw, ShieldCheck, Trash2 } from "lucide-react" import { ArrowLeft, ExternalLink, Plus, RefreshCw, ShieldCheck, Trash2 } from "lucide-react"
@@ -265,17 +265,10 @@ export default function NewGrnPage() {
// branch to pin to and the Admin must pick one explicitly for this GRN. // branch to pin to and the Admin must pick one explicitly for this GRN.
const lockedBranchId = isAdmin ? activeBranchId : myBranchId const lockedBranchId = isAdmin ? activeBranchId : myBranchId
const showBranchPicker = branchingEnabled && isAdmin && !lockedBranchId const showBranchPicker = branchingEnabled && isAdmin && !lockedBranchId
// The branch this GRN is being raised for — no longer used to filter warehouses
// (warehouses are shared infrastructure across branches), only to scope the document itself.
const effectiveBranchId = branchingEnabled ? (isAdmin ? (activeBranchId ?? branchFilterId) : myBranchId) : null const effectiveBranchId = branchingEnabled ? (isAdmin ? (activeBranchId ?? branchFilterId) : myBranchId) : null
const visibleWarehouses = useMemo( const visibleWarehouses = warehouses ?? []
() => (effectiveBranchId === null ? (warehouses ?? []) : (warehouses ?? []).filter((w) => w.branchId === effectiveBranchId)),
[warehouses, effectiveBranchId]
)
// Switching branch can strip the currently-picked warehouse out of the visible set —
// clear it rather than silently submitting a warehouse from the previous branch.
useEffect(() => {
setWarehouseId((prev) => (prev !== null && !visibleWarehouses.some((w) => w.warehouseId === prev) ? null : prev))
}, [visibleWarehouses])
function switchMode(next: Mode) { function switchMode(next: Mode) {
setMode(next) setMode(next)
@@ -459,6 +452,10 @@ export default function NewGrnPage() {
setHeaderError("Select a purchase order.") setHeaderError("Select a purchase order.")
return return
} }
if (branchingEnabled && !effectiveBranchId) {
setHeaderError("Select a branch for this GRN.")
return
}
if (lines.length === 0) { if (lines.length === 0) {
setSubmitError("Add at least one line.") setSubmitError("Add at least one line.")
return return
@@ -513,6 +510,7 @@ export default function NewGrnPage() {
poId: mode === "po" ? poId : null, poId: mode === "po" ? poId : null,
vendorId: mode === "direct" ? vendorId : null, vendorId: mode === "direct" ? vendorId : null,
warehouseId: warehouseId as number, warehouseId: warehouseId as number,
branchId: effectiveBranchId ?? 0,
lines: payloadLines, lines: payloadLines,
}) })
toast.success("GRN created", `${grn.docNo} is ready to confirm.`) toast.success("GRN created", `${grn.docNo} is ready to confirm.`)
+4
View File
@@ -61,6 +61,8 @@ export interface CreateGrnRequest {
/** Required when poId is null (direct/emergency receipt, FR-GRN-02). */ /** Required when poId is null (direct/emergency receipt, FR-GRN-02). */
vendorId?: number | null vendorId?: number | null
warehouseId: number warehouseId: number
/** Owning branch — warehouses are shared across branches, so this is what actually scopes the GRN. */
branchId: number
lines: CreateGrnLineInput[] lines: CreateGrnLineInput[]
} }
@@ -96,6 +98,7 @@ export interface Grn {
poId: number | null poId: number | null
vendorId: number vendorId: number
warehouseId: number warehouseId: number
branchId: number
status: GrnStatus status: GrnStatus
createdBy: number createdBy: number
createdAt: string createdAt: string
@@ -116,6 +119,7 @@ export interface GrnSummary {
poId: number | null poId: number | null
vendorId: number vendorId: number
warehouseId: number warehouseId: number
branchId: number
status: GrnStatus status: GrnStatus
createdBy: number createdBy: number
createdAt: string createdAt: string