批量对账

This commit is contained in:
liu
2026-09-07 22:53:50 +08:00
parent 379aeac2b3
commit 7de51391e4
9 changed files with 469 additions and 39 deletions
+27 -7
View File
@@ -61,7 +61,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
}
/**
* 导出行:标题/空行/列头(品名、汇总、市场、各门店)/明细
* 导出行:标题+备注(合并区)/空行(合并区)/列头(品名、汇总、市场、各门店)/明细
*/
public function collection(): Collection
{
@@ -74,11 +74,16 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
$rows = [];
$rowIndex = 0;
// 标题行
$rows[] = [$this->supplier->name . ' · ' . $this->marketLabel . ' · 采购单 ' . $this->purchase->purchase_no];
// 标题行(A1:C2 合并占两行),D1 起放采购单备注(D1:I2 合并,红色 22 号字)
$rows[] = [
$this->supplier->name . ' · ' . $this->marketLabel . ' · 采购单 ' . $this->purchase->purchase_no,
'',
'',
trim((string) $this->purchase->remark),
];
$this->specialRows[++$rowIndex] = 'title';
// 空行
// 空行(被标题/备注合并区域覆盖)
$rows[] = [''];
$rowIndex++;
@@ -117,7 +122,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
}
/**
* 标题/列头加粗;全表居中 + 全边框;
* 标题合并 A1:C2、备注合并 D1:I2(红色 22 号字);标题/列头加粗;全表居中 + 全边框 + 列头行自动换行
* 汇总列(浅黄)/门店数量列(浅蓝)按值条件填色(0 不填、列头固定填色),冻结列头
*/
public function styles(Worksheet $sheet): array
@@ -126,14 +131,18 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
$sheet->freezePane('A' . ($this->headerRow + 1));
$widths = [24, 10, 12];
// 标题合并前三列占两行(A1:C2),备注合并六列两行(D1:I2)
$sheet->mergeCells('A1:C2');
$sheet->mergeCells('D1:I2');
$widths = [24, 10, 6];
foreach ($widths as $index => $width) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
}
$storeIds = array_map('intval', array_keys($this->stores));
$lastColumn = Coordinate::stringFromColumnIndex(3 + max(count($storeIds), 1));
foreach ($storeIds as $i => $storeId) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex(4 + $i))->setWidth(12);
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex(4 + $i))->setWidth(6);
}
// 全部单元格水平/垂直居中
@@ -142,6 +151,14 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// 列头行自动换行(门店列窄,门店名换行显示)
$sheet->getStyle('A' . $this->headerRow . ':' . $lastColumn . $this->headerRow)
->getAlignment()
->setWrapText(true);
// 备注合并单元格自动换行(长备注多行显示)
$sheet->getStyle('D1')->getAlignment()->setWrapText(true);
// 表格区域(列头 → 数据末行)添加所有边框
$sheet->getStyle('A' . $this->headerRow . ':' . $lastColumn . $this->lastRow)
->getBorders()
@@ -179,6 +196,9 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
}
}
// 备注单元格(D1):红色 22 号字,须放在行样式之后应用以覆盖标题行字号
$styles['D1'] = ['font' => ['bold' => true, 'size' => 22, 'color' => ['argb' => 'FFFF0000']]];
return $styles;
}
@@ -13,6 +13,7 @@ use App\Http\Requests\Purchase\PurchaseStoreItemRequest;
use App\Models\BillModel;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\PurchaseItemCheckModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
@@ -173,6 +174,12 @@ class PurchaseOrderController extends BaseController
return $row;
}, $rows),
'bills' => $bills,
// 单品「已对账」标记(入库持久化,商品ID列表)
'checked_product_ids' => PurchaseItemCheckModel::query()
->where('purchase_id', $purchase->id)
->pluck('product_id')
->map(static fn ($v) => (int) $v)
->all(),
]);
}
@@ -795,6 +802,103 @@ class PurchaseOrderController extends BaseController
});
}
/**
* 切换单品「已对账」标记(入库持久化:未标记→标记,已标记→取消;与采购单状态无关,对账期间可反复勾选)
*/
#[PutRoute(route: '/{id}/check/{productId}', authorize: 'query', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])]
public function toggleItemCheck(int $id, int $productId, Request $request): JsonResponse
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$hasItem = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->where('product_id', $productId)
->exists();
if (! $hasItem) {
throw new RepositoryException('该采购单下无此商品的订货明细');
}
$marked = PurchaseItemCheckModel::query()
->where('purchase_id', $purchase->id)
->where('product_id', $productId)
->first();
if ($marked !== null) {
$marked->delete();
return $this->success(['checked' => false], '已取消对账标记');
}
PurchaseItemCheckModel::create([
'purchase_id' => $purchase->id,
'product_id' => $productId,
'operator_id' => (int) $request->user()->id,
]);
return $this->success(['checked' => true], '已标记为已对账');
}
/**
* 批量设置单品「已对账」标记(全选用:checked=true 批量标记 / false 批量取消;
* 仅处理本采购单内有订货明细的商品,重复标记幂等;与采购单状态无关)
*/
#[PutRoute(route: '/{id}/check', authorize: 'query', where: ['id' => '[0-9]+'])]
public function batchItemCheck(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'product_ids' => 'required|array|min:1',
'product_ids.*' => 'integer',
'checked' => 'required|boolean',
], [
'product_ids.required' => '请选择商品',
'product_ids.min' => '请选择商品',
]);
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
// 仅处理本采购单内有订货明细的商品(无效商品静默忽略)
$productIds = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->whereIn('product_id', array_map('intval', $data['product_ids']))
->distinct()
->pluck('product_id')
->map(static fn ($v) => (int) $v);
if (! (bool) $data['checked']) {
PurchaseItemCheckModel::query()
->where('purchase_id', $purchase->id)
->whereIn('product_id', $productIds)
->delete();
return $this->success(['count' => $productIds->count()], '已取消对账标记');
}
// 批量标记:跳过已标记行,仅补插缺失行(幂等)
$marked = PurchaseItemCheckModel::query()
->where('purchase_id', $purchase->id)
->whereIn('product_id', $productIds)
->pluck('product_id')
->map(static fn ($v) => (int) $v);
$operatorId = (int) $request->user()->id;
$now = now();
$rows = $productIds->diff($marked)
->map(static fn (int $productId) => [
'purchase_id' => $purchase->id,
'product_id' => $productId,
'operator_id' => $operatorId,
'created_at' => $now,
'updated_at' => $now,
])
->values()
->all();
if ($rows !== []) {
PurchaseItemCheckModel::insert($rows);
}
return $this->success(['count' => $productIds->count()], '已标记为已对账');
}
/**
* 采购单编辑闸:仅进行中(待采购)允许修改明细
*/
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Modules\SystemUser\Models\SysUserModel;
/**
* 采购单单品对账标记模型(商品明细「已对账」勾选,入库持久化;存在记录=已标记)
*/
class PurchaseItemCheckModel extends Model
{
protected $table = 'purchase_item_check';
protected $primaryKey = 'id';
protected $fillable = [
'purchase_id',
'product_id',
'operator_id',
];
protected $casts = [
'purchase_id' => 'integer',
'product_id' => 'integer',
'operator_id' => 'integer',
'created_at' => 'datetime:Y-m-d H:i:s',
];
/**
* 关联采购单
*/
public function purchase(): BelongsTo
{
return $this->belongsTo(PurchaseOrderModel::class, 'purchase_id', 'id');
}
/**
* 标记人(后台系统用户)
*/
public function operator(): BelongsTo
{
return $this->belongsTo(SysUserModel::class, 'operator_id', 'id');
}
}