feat(inventory): 재고 현황 요약/상세 플로우를 릴리스
- lib/features/inventory/summary 계층과 warehouse select 위젯을 추가해 목록/상세, 자동 새로고침, 필터, 상세 시트를 구현 - PermissionBootstrapper, scope 파서, 라우트 가드로 inventory.view 기반 권한 부여와 메뉴 노출을 통합(lib/core, lib/main.dart 등) - Inventory Summary API/QA/Audit 문서와 PR 템플릿, CHANGELOG를 신규 스펙과 검증 커맨드로 업데이트 - DTO 직렬화 의존성을 추가하고 Golden·Widget·단위 테스트를 작성했으며 flutter analyze / flutter test --coverage를 통과
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
/// 재고 이벤트와 연결된 거래처 유형.
|
||||
enum InventoryCounterpartyType { vendor, customer, unknown }
|
||||
|
||||
/// 재고 이벤트의 거래처 정보를 표현한다.
|
||||
class InventoryCounterparty {
|
||||
const InventoryCounterparty({required this.type, this.name});
|
||||
|
||||
final InventoryCounterpartyType type;
|
||||
final String? name;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'inventory_event.dart';
|
||||
import 'inventory_product.dart';
|
||||
import 'inventory_warehouse_balance.dart';
|
||||
|
||||
/// 재고 현황 단건 조회 결과.
|
||||
class InventoryDetail {
|
||||
const InventoryDetail({
|
||||
required this.product,
|
||||
required this.totalQuantity,
|
||||
required this.warehouseBalances,
|
||||
required this.recentEvents,
|
||||
this.updatedAt,
|
||||
this.lastRefreshedAt,
|
||||
});
|
||||
|
||||
final InventoryProduct product;
|
||||
final int totalQuantity;
|
||||
final List<InventoryWarehouseBalance> warehouseBalances;
|
||||
final List<InventoryEvent> recentEvents;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? lastRefreshedAt;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'inventory_counterparty.dart';
|
||||
import 'inventory_transaction_reference.dart';
|
||||
import 'inventory_warehouse.dart';
|
||||
|
||||
/// 재고 변동 이벤트 요약/상세 정보.
|
||||
class InventoryEvent {
|
||||
const InventoryEvent({
|
||||
required this.eventId,
|
||||
required this.eventKind,
|
||||
required this.eventLabel,
|
||||
required this.deltaQuantity,
|
||||
required this.occurredAt,
|
||||
this.counterparty,
|
||||
this.warehouse,
|
||||
this.transaction,
|
||||
this.line,
|
||||
});
|
||||
|
||||
/// 이벤트 식별자.
|
||||
final int eventId;
|
||||
|
||||
/// 이벤트 종류(`receipt`, `issue`, `rental_out`, `rental_return`).
|
||||
final String eventKind;
|
||||
|
||||
/// 현지화된 이벤트 라벨.
|
||||
final String eventLabel;
|
||||
|
||||
/// 수량 증감.
|
||||
final int deltaQuantity;
|
||||
|
||||
/// 발생 시각(UTC).
|
||||
final DateTime occurredAt;
|
||||
|
||||
/// 거래처 정보.
|
||||
final InventoryCounterparty? counterparty;
|
||||
|
||||
/// 이벤트가 발생한 창고 정보.
|
||||
final InventoryWarehouse? warehouse;
|
||||
|
||||
/// 연결된 전표 정보.
|
||||
final InventoryTransactionReference? transaction;
|
||||
|
||||
/// 연결된 라인 정보.
|
||||
final InventoryEventLineReference? line;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/// 재고 요약 목록 조회 필터.
|
||||
class InventorySummaryFilter {
|
||||
const InventorySummaryFilter({
|
||||
this.page = 1,
|
||||
this.pageSize = 50,
|
||||
this.query,
|
||||
this.productName,
|
||||
this.vendorName,
|
||||
this.warehouseId,
|
||||
this.includeEmpty = false,
|
||||
this.updatedSince,
|
||||
this.sort,
|
||||
this.order,
|
||||
});
|
||||
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final String? query;
|
||||
final String? productName;
|
||||
final String? vendorName;
|
||||
final int? warehouseId;
|
||||
final bool includeEmpty;
|
||||
final DateTime? updatedSince;
|
||||
final String? sort;
|
||||
final String? order;
|
||||
|
||||
/// API 요청에 사용할 쿼리 파라미터 맵을 생성한다.
|
||||
Map<String, dynamic> toQuery() {
|
||||
final queryMap = <String, dynamic>{'page': page, 'page_size': pageSize};
|
||||
void put(String key, dynamic value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (value is String) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return;
|
||||
}
|
||||
queryMap[key] = trimmed;
|
||||
return;
|
||||
}
|
||||
queryMap[key] = value;
|
||||
}
|
||||
|
||||
put('q', query);
|
||||
put('product_name', productName);
|
||||
put('vendor_name', vendorName);
|
||||
if (warehouseId != null) {
|
||||
queryMap['warehouse_id'] = warehouseId;
|
||||
}
|
||||
if (includeEmpty) {
|
||||
queryMap['include_empty'] = 'true';
|
||||
}
|
||||
if (updatedSince != null) {
|
||||
queryMap['updated_since'] = updatedSince!.toUtc().toIso8601String();
|
||||
}
|
||||
put('sort', sort);
|
||||
final normalizedOrder = order?.trim().toLowerCase();
|
||||
if (normalizedOrder != null && normalizedOrder.isNotEmpty) {
|
||||
queryMap['order'] = normalizedOrder;
|
||||
}
|
||||
return queryMap;
|
||||
}
|
||||
}
|
||||
|
||||
/// 재고 단건 조회 필터.
|
||||
class InventoryDetailFilter {
|
||||
const InventoryDetailFilter({this.warehouseId, this.eventLimit = 20});
|
||||
|
||||
final int? warehouseId;
|
||||
final int eventLimit;
|
||||
|
||||
Map<String, dynamic> toQuery() {
|
||||
final map = <String, dynamic>{'event_limit': eventLimit};
|
||||
if (warehouseId != null) {
|
||||
map['warehouse_id'] = warehouseId;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'inventory_vendor.dart';
|
||||
|
||||
/// 재고 요약/상세 뷰에서 공통으로 사용하는 제품 정보.
|
||||
class InventoryProduct {
|
||||
const InventoryProduct({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
this.vendor,
|
||||
});
|
||||
|
||||
/// 제품 식별자.
|
||||
final int id;
|
||||
|
||||
/// 제품 코드.
|
||||
final String code;
|
||||
|
||||
/// 제품 명칭.
|
||||
final String name;
|
||||
|
||||
/// 공급사 정보. 없을 수도 있다.
|
||||
final InventoryVendor? vendor;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'inventory_event.dart';
|
||||
import 'inventory_product.dart';
|
||||
import 'inventory_warehouse_balance.dart';
|
||||
|
||||
/// 재고 현황 목록 항목 엔티티.
|
||||
class InventorySummary {
|
||||
const InventorySummary({
|
||||
required this.product,
|
||||
required this.totalQuantity,
|
||||
required this.warehouseBalances,
|
||||
this.recentEvent,
|
||||
this.updatedAt,
|
||||
this.lastRefreshedAt,
|
||||
});
|
||||
|
||||
final InventoryProduct product;
|
||||
final int totalQuantity;
|
||||
final List<InventoryWarehouseBalance> warehouseBalances;
|
||||
final InventoryEvent? recentEvent;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? lastRefreshedAt;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import 'inventory_summary.dart';
|
||||
|
||||
/// 재고 요약 목록과 뷰 리프레시 메타데이터를 함께 담는 결과 모델.
|
||||
class InventorySummaryListResult {
|
||||
const InventorySummaryListResult({
|
||||
required this.result,
|
||||
this.lastRefreshedAt,
|
||||
});
|
||||
|
||||
final PaginatedResult<InventorySummary> result;
|
||||
final DateTime? lastRefreshedAt;
|
||||
|
||||
InventorySummaryListResult copyWith({
|
||||
PaginatedResult<InventorySummary>? result,
|
||||
DateTime? lastRefreshedAt,
|
||||
}) {
|
||||
return InventorySummaryListResult(
|
||||
result: result ?? this.result,
|
||||
lastRefreshedAt: lastRefreshedAt ?? this.lastRefreshedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/// 재고 이벤트가 속한 전표 정보를 요약한 참조 모델.
|
||||
class InventoryTransactionReference {
|
||||
const InventoryTransactionReference({
|
||||
required this.id,
|
||||
required this.transactionNo,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String transactionNo;
|
||||
}
|
||||
|
||||
/// 재고 이벤트 라인 정보 참조 모델.
|
||||
class InventoryEventLineReference {
|
||||
const InventoryEventLineReference({
|
||||
required this.id,
|
||||
required this.lineNo,
|
||||
required this.quantity,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int lineNo;
|
||||
final int quantity;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/// 재고 요약에서 사용하는 공급사 정보를 표현하는 값 객체.
|
||||
class InventoryVendor {
|
||||
const InventoryVendor({this.id, required this.name});
|
||||
|
||||
/// 공급사 식별자. 미정의일 수 있다.
|
||||
final int? id;
|
||||
|
||||
/// 공급사 명칭.
|
||||
final String name;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/// 재고 현황에서 참조하는 창고 정보를 표현한다.
|
||||
class InventoryWarehouse {
|
||||
const InventoryWarehouse({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
/// 창고 식별자.
|
||||
final int id;
|
||||
|
||||
/// 창고 코드.
|
||||
final String code;
|
||||
|
||||
/// 창고 명칭.
|
||||
final String name;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'inventory_warehouse.dart';
|
||||
|
||||
/// 특정 창고의 재고 수량을 나타내는 모델.
|
||||
class InventoryWarehouseBalance {
|
||||
const InventoryWarehouseBalance({
|
||||
required this.warehouse,
|
||||
required this.quantity,
|
||||
});
|
||||
|
||||
/// 창고 정보.
|
||||
final InventoryWarehouse warehouse;
|
||||
|
||||
/// 창고 내 잔량.
|
||||
final int quantity;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import '../entities/inventory_detail.dart';
|
||||
import '../entities/inventory_filters.dart';
|
||||
import '../entities/inventory_summary_list_result.dart';
|
||||
|
||||
/// 재고 현황 데이터를 제공하는 저장소 인터페이스.
|
||||
abstract class InventoryRepository {
|
||||
/// 재고 요약 목록을 조회한다.
|
||||
Future<InventorySummaryListResult> listSummaries({
|
||||
InventorySummaryFilter? filter,
|
||||
});
|
||||
|
||||
/// 특정 제품의 상세 정보를 조회한다.
|
||||
Future<InventoryDetail> fetchDetail(
|
||||
int productId, {
|
||||
InventoryDetailFilter? filter,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user