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:
@@ -22,6 +22,11 @@ public class Grn
|
||||
public int WarehouseId { 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 int CreatedBy { get; set; }
|
||||
|
||||
@@ -17,6 +17,11 @@ public class SalesInvoice
|
||||
public int WarehouseId { 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 SalesInvoiceStatus Status { get; set; } = SalesInvoiceStatus.Draft;
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ public class StockAdjustment
|
||||
public int ReasonCodeId { 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 int CreatedBy { get; set; }
|
||||
|
||||
@@ -16,6 +16,11 @@ public class StockCount
|
||||
public int WarehouseId { 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 CountStatus Status { get; set; } = CountStatus.Draft;
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ public class StockTransfer
|
||||
public int DestWarehouseId { 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 int CreatedBy { get; set; }
|
||||
|
||||
@@ -15,13 +15,13 @@ public sealed record GrnLineDto(
|
||||
HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers);
|
||||
|
||||
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? PurchaseInvoiceId, string? PurchaseInvoiceNo, IReadOnlyList<GrnLineDto> Lines);
|
||||
|
||||
/// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary>
|
||||
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? 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>
|
||||
public int? VendorId { 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@ public sealed record SalesInvoiceTotalsDto(
|
||||
|
||||
public sealed record SalesInvoiceDto(
|
||||
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,
|
||||
DateTime? UpdatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
||||
SalesInvoiceTotalsDto Totals, IReadOnlyList<SalesInvoiceLineDto> Lines);
|
||||
|
||||
public sealed record SalesInvoiceSummaryDto(
|
||||
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);
|
||||
|
||||
public sealed record SalesInvoicePaymentDto(
|
||||
@@ -65,6 +65,9 @@ public sealed class CreateSalesInvoiceRequest
|
||||
{
|
||||
[Required] public int CustomerId { 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, 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 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, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -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 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,
|
||||
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>
|
||||
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);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
@@ -31,6 +31,9 @@ public sealed class CreateAdjustmentLineInput
|
||||
public sealed class CreateAdjustmentRequest
|
||||
{
|
||||
[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>
|
||||
public int? ReasonCodeId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreateAdjustmentLineInput> Lines { get; set; } = new();
|
||||
|
||||
@@ -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 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);
|
||||
|
||||
/// <summary>Row shape for <c>GET /stock-counts</c> — line count instead of the lines themselves.</summary>
|
||||
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);
|
||||
|
||||
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
|
||||
{
|
||||
[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, MinLength(1)] public List<int> ItemIds { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ public sealed record TransferLineDto(
|
||||
int TransferLineId, int ItemId, int? SrcBinId, int? DestBinId, int? BatchId, decimal Qty, decimal QtyReceived);
|
||||
|
||||
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);
|
||||
|
||||
/// <summary>Row shape for <c>GET /stock-transfers</c> — line count instead of the lines themselves.</summary>
|
||||
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);
|
||||
|
||||
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 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.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.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.PurchaseInvoice).WithMany().HasForeignKey(g => g.PurchaseInvoiceId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(g => g.Status);
|
||||
builder.HasIndex(g => g.PoId);
|
||||
builder.HasIndex(g => g.BranchId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration<SalesIn
|
||||
.HasForeignKey(x => x.WarehouseId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(x => x.Branch)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.BranchId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.Property(x => x.InvoiceType)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(SalesInvoiceType.B2C);
|
||||
@@ -47,6 +52,7 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration<SalesIn
|
||||
|
||||
builder.HasIndex(x => x.Status);
|
||||
builder.HasIndex(x => x.InvoiceDate);
|
||||
builder.HasIndex(x => x.BranchId);
|
||||
|
||||
builder.HasMany(x => x.Lines)
|
||||
.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.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.HasIndex(a => a.BranchId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,11 @@ public sealed class StockCountConfiguration : IEntityTypeConfiguration<StockCoun
|
||||
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||
|
||||
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.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.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.HasIndex(t => t.Status);
|
||||
builder.HasIndex(t => t.BranchId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8004
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"));
|
||||
|
||||
b.Property<int>("BranchId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -1493,6 +1496,8 @@ namespace ERPCore.Migrations
|
||||
|
||||
b.HasKey("GrnId");
|
||||
|
||||
b.HasIndex("BranchId");
|
||||
|
||||
b.HasIndex("CreatedBy");
|
||||
|
||||
b.HasIndex("DocNo")
|
||||
@@ -4187,6 +4192,9 @@ namespace ERPCore.Migrations
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<int>("BranchId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -4279,6 +4287,8 @@ namespace ERPCore.Migrations
|
||||
|
||||
b.HasKey("SalesInvoiceId");
|
||||
|
||||
b.HasIndex("BranchId");
|
||||
|
||||
b.HasIndex("CreatorUserId");
|
||||
|
||||
b.HasIndex("CustomerId");
|
||||
@@ -4859,6 +4869,9 @@ namespace ERPCore.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("AdjustmentId"));
|
||||
|
||||
b.Property<int>("BranchId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -4896,6 +4909,8 @@ namespace ERPCore.Migrations
|
||||
|
||||
b.HasKey("AdjustmentId");
|
||||
|
||||
b.HasIndex("BranchId");
|
||||
|
||||
b.HasIndex("CreatedBy");
|
||||
|
||||
b.HasIndex("DocNo")
|
||||
@@ -4958,6 +4973,9 @@ namespace ERPCore.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("CountId"));
|
||||
|
||||
b.Property<int>("BranchId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CountType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
@@ -4990,6 +5008,8 @@ namespace ERPCore.Migrations
|
||||
|
||||
b.HasKey("CountId");
|
||||
|
||||
b.HasIndex("BranchId");
|
||||
|
||||
b.HasIndex("CreatedBy");
|
||||
|
||||
b.HasIndex("DocNo")
|
||||
@@ -5182,6 +5202,9 @@ namespace ERPCore.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("TransferId"));
|
||||
|
||||
b.Property<int>("BranchId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -5212,6 +5235,8 @@ namespace ERPCore.Migrations
|
||||
|
||||
b.HasKey("TransferId");
|
||||
|
||||
b.HasIndex("BranchId");
|
||||
|
||||
b.HasIndex("CreatedBy");
|
||||
|
||||
b.HasIndex("DestWarehouseId");
|
||||
@@ -6380,6 +6405,12 @@ namespace ERPCore.Migrations
|
||||
|
||||
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")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatedBy")
|
||||
@@ -6408,6 +6439,8 @@ namespace ERPCore.Migrations
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Branch");
|
||||
|
||||
b.Navigation("Creator");
|
||||
|
||||
b.Navigation("PurchaseInvoice");
|
||||
@@ -7142,6 +7175,12 @@ namespace ERPCore.Migrations
|
||||
|
||||
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")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatorUserId");
|
||||
@@ -7158,6 +7197,8 @@ namespace ERPCore.Migrations
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Branch");
|
||||
|
||||
b.Navigation("Creator");
|
||||
|
||||
b.Navigation("Customer");
|
||||
@@ -7423,6 +7464,12 @@ namespace ERPCore.Migrations
|
||||
|
||||
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")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatedBy")
|
||||
@@ -7441,6 +7488,8 @@ namespace ERPCore.Migrations
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Branch");
|
||||
|
||||
b.Navigation("Creator");
|
||||
|
||||
b.Navigation("ReasonCode");
|
||||
@@ -7484,6 +7533,12 @@ namespace ERPCore.Migrations
|
||||
|
||||
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")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatedBy")
|
||||
@@ -7496,6 +7551,8 @@ namespace ERPCore.Migrations
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Branch");
|
||||
|
||||
b.Navigation("Creator");
|
||||
|
||||
b.Navigation("Warehouse");
|
||||
@@ -7603,6 +7660,12 @@ namespace ERPCore.Migrations
|
||||
|
||||
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")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatedBy")
|
||||
@@ -7621,6 +7684,8 @@ namespace ERPCore.Migrations
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Branch");
|
||||
|
||||
b.Navigation("Creator");
|
||||
|
||||
b.Navigation("DestWarehouse");
|
||||
|
||||
@@ -32,6 +32,7 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
private readonly IStockMutator _mutator;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly IBranchConfigService _branchConfig;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly IGeneralLedgerService _gl;
|
||||
private readonly string _glInventoryAccountCode;
|
||||
@@ -41,7 +42,7 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
public AdjustmentService(
|
||||
IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses, IRepository<Item> items,
|
||||
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)
|
||||
{
|
||||
_adjustments = adjustments;
|
||||
@@ -52,6 +53,7 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
_mutator = mutator;
|
||||
_numbers = numbers;
|
||||
_currentUser = currentUser;
|
||||
_branchConfig = branchConfig;
|
||||
_uow = uow;
|
||||
_gl = gl;
|
||||
_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}%"));
|
||||
}
|
||||
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);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(a => a.AdjustmentId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.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))
|
||||
.ToListAsync(ct);
|
||||
|
||||
@@ -145,6 +148,8 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
|
||||
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)
|
||||
?? throw new DomainException(ErrorCodes.Validation, $"Reason code {request.ReasonCodeId} does not exist.", 422);
|
||||
if (reason.Context != ReasonContext.Adjustment)
|
||||
@@ -169,6 +174,7 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
{
|
||||
DocNo = docNo,
|
||||
WarehouseId = request.WarehouseId,
|
||||
BranchId = branchId,
|
||||
ReasonCodeId = request.ReasonCodeId.Value,
|
||||
Status = AdjustmentStatus.Posted,
|
||||
CreatedBy = _currentUser.AuditUserId,
|
||||
@@ -196,7 +202,7 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
}
|
||||
|
||||
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.Lines.OrderBy(l => l.AdjLineId)
|
||||
.Select(l => new AdjustmentLineDto(l.AdjLineId, l.ItemId, l.BinId, l.BatchId, l.QtyDelta)).ToList(),
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class CountService : ICountService
|
||||
private readonly IStockMutator _mutator;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly IBranchConfigService _branchConfig;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly IGeneralLedgerService _gl;
|
||||
private readonly string _glInventoryAccountCode;
|
||||
@@ -43,7 +44,7 @@ public sealed class CountService : ICountService
|
||||
public CountService(
|
||||
IRepository<StockCount> counts, IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses,
|
||||
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)
|
||||
{
|
||||
_counts = counts;
|
||||
@@ -55,6 +56,7 @@ public sealed class CountService : ICountService
|
||||
_mutator = mutator;
|
||||
_numbers = numbers;
|
||||
_currentUser = currentUser;
|
||||
_branchConfig = branchConfig;
|
||||
_uow = uow;
|
||||
_gl = gl;
|
||||
_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 (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 rows = await q.OrderByDescending(c => c.CountId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.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))
|
||||
.ToListAsync(ct);
|
||||
|
||||
@@ -133,6 +136,8 @@ public sealed class CountService : ICountService
|
||||
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
|
||||
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();
|
||||
foreach (var id in itemIds)
|
||||
if (!await _items.Query().AnyAsync(i => i.ItemId == id, ct))
|
||||
@@ -153,6 +158,7 @@ public sealed class CountService : ICountService
|
||||
{
|
||||
DocNo = docNo,
|
||||
WarehouseId = request.WarehouseId,
|
||||
BranchId = branchId,
|
||||
CountType = request.CountType,
|
||||
Status = CountStatus.Draft,
|
||||
CreatedBy = _currentUser.AuditUserId,
|
||||
@@ -222,6 +228,7 @@ public sealed class CountService : ICountService
|
||||
{
|
||||
DocNo = docNo,
|
||||
WarehouseId = count.WarehouseId,
|
||||
BranchId = count.BranchId,
|
||||
ReasonCodeId = reason.ReasonCodeId,
|
||||
Status = AdjustmentStatus.Posted,
|
||||
CreatedBy = _currentUser.AuditUserId,
|
||||
@@ -250,7 +257,7 @@ public sealed class CountService : ICountService
|
||||
}
|
||||
|
||||
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)
|
||||
.Select(l => new CountLineDto(l.CountLineId, l.ItemId, l.BinId, l.SystemQty, l.CountedQty, l.Variance)).ToList());
|
||||
}
|
||||
|
||||
@@ -62,11 +62,11 @@ public sealed class DashboardService : IDashboardService
|
||||
.ToList();
|
||||
|
||||
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 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);
|
||||
|
||||
// PurchaseOrder/Requisition/Rfq carry no warehouse/branch at header level yet
|
||||
|
||||
@@ -38,6 +38,7 @@ public sealed class GrnService : IGrnService
|
||||
private readonly IFifoCostingService _fifo;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly IBranchConfigService _branchConfig;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly IGeneralLedgerService _gl;
|
||||
private readonly string _glInventoryAccountCode;
|
||||
@@ -49,8 +50,8 @@ public sealed class GrnService : IGrnService
|
||||
IRepository<Item> items, IRepository<Warehouse> warehouses,
|
||||
IRepository<Bin> bins, IRepository<Vendor> vendors, IRepository<Batch> batches,
|
||||
IRepository<StockLayer> layers, IRepository<StockLedger> ledger,
|
||||
IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow,
|
||||
IGeneralLedgerService gl, IConfiguration configuration)
|
||||
IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig,
|
||||
IUnitOfWork uow, IGeneralLedgerService gl, IConfiguration configuration)
|
||||
{
|
||||
_grns = grns;
|
||||
_pos = pos;
|
||||
@@ -65,6 +66,7 @@ public sealed class GrnService : IGrnService
|
||||
_fifo = fifo;
|
||||
_numbers = numbers;
|
||||
_currentUser = currentUser;
|
||||
_branchConfig = branchConfig;
|
||||
_uow = uow;
|
||||
_gl = gl;
|
||||
_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 (vendorId is not null) q = q.Where(g => g.VendorId == vendorId);
|
||||
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
|
||||
// Warehouse, same as Sales (docs/BRANCH_ISOLATION_PLAN.md "Anchor mechanism").
|
||||
if (branchId is not null) q = q.Where(g => g.Warehouse!.BranchId == branchId);
|
||||
// Warehouses are shared across branches — GRN carries its own BranchId, set at creation.
|
||||
if (branchId is not null) q = q.Where(g => g.BranchId == branchId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(g => g.GrnId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.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.PurchaseInvoiceId, g.PurchaseInvoice != null ? g.PurchaseInvoice.DocNo : null))
|
||||
.ToListAsync(ct);
|
||||
@@ -116,6 +117,8 @@ public sealed class GrnService : IGrnService
|
||||
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422);
|
||||
|
||||
var branchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
|
||||
|
||||
// Resolve vendor + PO context.
|
||||
PurchaseOrder? po = null;
|
||||
int vendorId;
|
||||
@@ -211,6 +214,7 @@ public sealed class GrnService : IGrnService
|
||||
PoId = request.PoId,
|
||||
VendorId = vendorId,
|
||||
WarehouseId = request.WarehouseId,
|
||||
BranchId = branchId,
|
||||
Status = GrnStatus.Draft,
|
||||
CreatedBy = _currentUser.AuditUserId,
|
||||
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);
|
||||
|
||||
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.Lines.OrderBy(l => l.GrnLineId).Select(l => new GrnLineDto(
|
||||
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 ISalesDocumentWorkflowService _workflow;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly IBranchConfigService _branchConfig;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
@@ -33,7 +34,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
IRepository<SalesInvoice> invoices, IRepository<Customer> customers, IRepository<Item> items,
|
||||
IRepository<Uom> uoms, IRepository<Warehouse> warehouses, ISalesDomainService sales, ISalesPostingService posting, ISalesMappingService mapping,
|
||||
ISalesDocumentWorkflowService workflow,
|
||||
ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow)
|
||||
ICurrentUser currentUser, IBranchConfigService branchConfig, INumberSequenceService numbers, IUnitOfWork uow)
|
||||
{
|
||||
_invoices = invoices;
|
||||
_customers = customers;
|
||||
@@ -45,6 +46,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
_mapping = mapping;
|
||||
_workflow = workflow;
|
||||
_currentUser = currentUser;
|
||||
_branchConfig = branchConfig;
|
||||
_numbers = numbers;
|
||||
_uow = uow;
|
||||
}
|
||||
@@ -60,9 +62,8 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
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 (warehouseId is not null) q = q.Where(x => x.WarehouseId == warehouseId);
|
||||
// Sales carries no BranchId of its own — it inherits branch scope transitively via
|
||||
// its Warehouse (docs/BRANCH_ISOLATION_PLAN.md "Anchor mechanism: Warehouse carries the branch").
|
||||
if (branchId is not null) q = q.Where(x => x.Warehouse!.BranchId == branchId);
|
||||
// Warehouses are shared across branches — the invoice carries its own owning BranchId.
|
||||
if (branchId is not null) q = q.Where(x => x.BranchId == branchId);
|
||||
|
||||
var total = await q.CountAsync(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)
|
||||
{
|
||||
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 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,
|
||||
CustomerId = request.CustomerId,
|
||||
WarehouseId = request.WarehouseId,
|
||||
BranchId = branchId,
|
||||
InvoiceType = request.InvoiceType,
|
||||
Status = SalesInvoiceStatus.Draft,
|
||||
CreatedBy = _currentUser.AuditUserId,
|
||||
@@ -119,6 +122,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, null, false, ct);
|
||||
invoice.CustomerId = request.CustomerId;
|
||||
invoice.WarehouseId = request.WarehouseId;
|
||||
invoice.BranchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
|
||||
invoice.InvoiceType = request.InvoiceType;
|
||||
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);
|
||||
@@ -201,5 +205,5 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
|
||||
private SalesInvoiceSummaryDto MapSummary(SalesInvoice x) => new(
|
||||
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)
|
||||
=> new(
|
||||
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.GlJournalNo, invoice.GlPostedAt,
|
||||
MapInvoiceTotals(invoice),
|
||||
|
||||
@@ -34,11 +34,13 @@ public sealed class TransferService : ITransferService
|
||||
private readonly IFifoCostingService _fifo;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly IBranchConfigService _branchConfig;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public TransferService(
|
||||
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;
|
||||
_warehouses = warehouses;
|
||||
@@ -46,6 +48,7 @@ public sealed class TransferService : ITransferService
|
||||
_fifo = fifo;
|
||||
_numbers = numbers;
|
||||
_currentUser = currentUser;
|
||||
_branchConfig = branchConfig;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
@@ -62,15 +65,14 @@ public sealed class TransferService : ITransferService
|
||||
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 (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
|
||||
// rather than only the branch that happens to hold the source warehouse.
|
||||
if (branchId is not null) q = q.Where(t => t.SrcWarehouse!.BranchId == branchId || t.DestWarehouse!.BranchId == branchId);
|
||||
// Warehouses are shared across branches — the transfer carries its own owning BranchId.
|
||||
if (branchId is not null) q = q.Where(t => t.BranchId == branchId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(t => t.TransferId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.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))
|
||||
.ToListAsync(ct);
|
||||
|
||||
@@ -90,6 +92,7 @@ public sealed class TransferService : ITransferService
|
||||
throw new DomainException(ErrorCodes.Validation, "destWarehouseId must differ from srcWarehouseId.", 422);
|
||||
await EnsureWarehouseAsync(request.SrcWarehouseId, ct);
|
||||
await EnsureWarehouseAsync(request.DestWarehouseId, ct);
|
||||
var branchId = await _branchConfig.ResolveBranchIdAsync(request.BranchId, ct);
|
||||
foreach (var line in request.Lines)
|
||||
if (!await _items.Query().AnyAsync(i => i.ItemId == line.ItemId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"Item {line.ItemId} does not exist.", 422);
|
||||
@@ -103,6 +106,7 @@ public sealed class TransferService : ITransferService
|
||||
DocNo = docNo,
|
||||
SrcWarehouseId = request.SrcWarehouseId,
|
||||
DestWarehouseId = request.DestWarehouseId,
|
||||
BranchId = branchId,
|
||||
Status = TransferStatus.Draft,
|
||||
CreatedBy = actor,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
@@ -227,7 +231,7 @@ public sealed class TransferService : ITransferService
|
||||
}
|
||||
|
||||
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(
|
||||
l.TransferLineId, l.ItemId, l.SrcBinId, l.DestBinId, l.BatchId, l.Qty, l.QtyReceived)).ToList());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
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.
|
||||
const lockedBranchId = isAdmin ? activeBranchId : myBranchId
|
||||
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 visibleWarehouses = useMemo(
|
||||
() => (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])
|
||||
const visibleWarehouses = warehouses ?? []
|
||||
|
||||
function switchMode(next: Mode) {
|
||||
setMode(next)
|
||||
@@ -459,6 +452,10 @@ export default function NewGrnPage() {
|
||||
setHeaderError("Select a purchase order.")
|
||||
return
|
||||
}
|
||||
if (branchingEnabled && !effectiveBranchId) {
|
||||
setHeaderError("Select a branch for this GRN.")
|
||||
return
|
||||
}
|
||||
if (lines.length === 0) {
|
||||
setSubmitError("Add at least one line.")
|
||||
return
|
||||
@@ -513,6 +510,7 @@ export default function NewGrnPage() {
|
||||
poId: mode === "po" ? poId : null,
|
||||
vendorId: mode === "direct" ? vendorId : null,
|
||||
warehouseId: warehouseId as number,
|
||||
branchId: effectiveBranchId ?? 0,
|
||||
lines: payloadLines,
|
||||
})
|
||||
toast.success("GRN created", `${grn.docNo} is ready to confirm.`)
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface CreateGrnRequest {
|
||||
/** Required when poId is null (direct/emergency receipt, FR-GRN-02). */
|
||||
vendorId?: number | null
|
||||
warehouseId: number
|
||||
/** Owning branch — warehouses are shared across branches, so this is what actually scopes the GRN. */
|
||||
branchId: number
|
||||
lines: CreateGrnLineInput[]
|
||||
}
|
||||
|
||||
@@ -96,6 +98,7 @@ export interface Grn {
|
||||
poId: number | null
|
||||
vendorId: number
|
||||
warehouseId: number
|
||||
branchId: number
|
||||
status: GrnStatus
|
||||
createdBy: number
|
||||
createdAt: string
|
||||
@@ -116,6 +119,7 @@ export interface GrnSummary {
|
||||
poId: number | null
|
||||
vendorId: number
|
||||
warehouseId: number
|
||||
branchId: number
|
||||
status: GrnStatus
|
||||
createdBy: number
|
||||
createdAt: string
|
||||
|
||||
Reference in New Issue
Block a user