feat: 供应商应付 - 货款对账 + 5个子功能页面

新增功能页面:
- 货款对账 (payment-reconciliation): 入库+代发合并对账,228K+2.8M条记录
- 退货对账 (return-reconciliation): finance_purchaseReturn, 36791行
- 发票扣款 (invoice-deductions): finance_invoiceDeduction, 195行
- 赔偿明细 (compensation-details): 纸板厂+压板厂 UNION, 98行
- 借支明细 (advance-details): finance_advanceLoans, 4行
- 样品扣款 (sample-deductions): finance_sampleDeduction, 12行

后端:
- JianSunConnection: PDO_ODBC 直连 SQL Server
- SupplierPayableRepository: 5个子页面的数据查询+分页
- GoodsReconciliationRepository: 货款对账数据查询
- 15个新API路由 (supplier-payables/*)
- 12个JianSun Eloquent模型

前端:
- 6个Blade视图 + 6个Vue组件
- webpack.mix.js 新增5个编译入口
- show.blade.php 动态视图解析
This commit is contained in:
WuYongJie 2026-07-21 18:38:02 +08:00
parent 29e4b31294
commit e5c068036b
34 changed files with 74573 additions and 4 deletions

View File

@ -151,6 +151,25 @@ return [
'prefix_indexes' => true,
],
'jiansun' => [
'driver' => 'odbc',
'dsn' => sprintf(
'Driver={SQL Server};Server=%s,%s;Database=%s',
env('DB_JIANSUN_HOST', '218.93.191.21'),
env('DB_JIANSUN_PORT', '1433'),
env('DB_JIANSUN_DATABASE', 'JianSun')
),
'username' => env('DB_JIANSUN_USERNAME', 'financeReader'),
'password' => env('DB_JIANSUN_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
],
],
],
/*

View File

@ -0,0 +1,115 @@
<?php
namespace Modules\EcommerceFinance\Http\Controllers\Api;
use App\Abstracts\Http\ApiController;
use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
/**
* 货款对账 API
*
* 提供供应商应付-货款对账页面的全部数据接口:
* GET /api/ecommerce-finance/goods-reconciliations 列表(分页 + 筛选)
* GET /api/ecommerce-finance/goods-reconciliations/summary 统计卡片
* GET /api/ecommerce-finance/goods-reconciliations/options 筛选器选项
*/
class GoodsReconciliations extends ApiController
{
private GoodsReconciliationRepository $repo;
public function __construct()
{
$this->repo = new GoodsReconciliationRepository();
}
/**
* 货款对账列表(分页 + 筛选)
*/
public function index()
{
$filters = $this->buildFilters();
$page = max((int) request('page', 1), 1);
$limit = $this->limit();
$result = $this->repo->list($filters, $page, $limit);
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $page,
'per_page' => $limit,
'last_page' => (int) ceil($result['total'] / $limit),
]);
}
/**
* 汇总统计卡片
*/
public function summary()
{
$filters = $this->buildFilters();
$summary = $this->repo->summary($filters);
return response()->json($summary);
}
/**
* 筛选器选项(供应商/仓库下拉列表等)
*/
public function options()
{
$options = $this->repo->filterOptions();
return response()->json($options);
}
// ===================================================================
// Helpers
// ===================================================================
/**
* 从请求参数构建筛选条件
*/
private function buildFilters(): array
{
$filters = [];
// 记录类型
if ($type = request('record_type')) {
$filters['record_type'] = $type; // purchase | sale | all
}
// 文本筛选
$textFields = ['supplier', 'stock_no', 'purchase_no', 'product_code', 'express_no', 'warehouse'];
foreach ($textFields as $field) {
if ($val = request($field)) {
$filters[$field] = trim($val);
}
}
// 日期范围
$dateFields = ['send_time_from', 'send_time_to', 'create_time_from', 'create_time_to'];
foreach ($dateFields as $field) {
if ($val = request($field)) {
$filters[$field] = $val;
}
}
// 发出状态
if (($sent = request('sent_status')) && $sent !== 'all') {
$filters['sent_status'] = $sent; // sent | unsent
}
// 是否有差异
if (request('has_diff')) {
$filters['has_diff'] = true;
}
return $filters;
}
private function limit(): int
{
return min(max((int) request('limit', 25), 1), 100);
}
}

View File

@ -0,0 +1,196 @@
<?php
namespace Modules\EcommerceFinance\Http\Controllers\Api;
use App\Abstracts\Http\ApiController;
use Modules\EcommerceFinance\Repositories\SupplierPayableRepository;
/**
* 供应商应付 - 子功能页面 API
*
* 为 5 个子页面提供数据接口:
* 退货对账 : /return-reconciliations + /return-reconciliations/summary + /return-reconciliations/options
* 发票扣款 : /invoice-deductions + /invoice-deductions/summary + /invoice-deductions/options
* 赔偿明细 : /compensation-details + /compensation-details/summary + /compensation-details/options
* 借支明细 : /advance-details + /advance-details/summary + /advance-details/options
* 样品扣款 : /sample-deductions + /sample-deductions/summary + /sample-deductions/options
*/
class SupplierPayables extends ApiController
{
private SupplierPayableRepository $repo;
public function __construct()
{
$this->repo = new SupplierPayableRepository();
}
// ============================================================
// 退货对账
// ============================================================
public function returnReconciliationsIndex()
{
$filters = $this->filters('return');
$result = $this->repo->returnReconciliations($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function returnReconciliationsSummary()
{
return response()->json($this->repo->returnReconciliationSummary($this->filters('return')));
}
public function returnReconciliationsOptions()
{
return response()->json($this->repo->filterOptionsForReturn());
}
// ============================================================
// 发票扣款
// ============================================================
public function invoiceDeductionsIndex()
{
$filters = $this->filters('invoice');
$result = $this->repo->invoiceDeductions($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function invoiceDeductionsSummary()
{
return response()->json($this->repo->invoiceDeductionSummary($this->filters('invoice')));
}
public function invoiceDeductionsOptions()
{
return response()->json($this->repo->filterOptionsForInvoice());
}
// ============================================================
// 赔偿明细
// ============================================================
public function compensationDetailsIndex()
{
$filters = $this->filters('compensation');
$result = $this->repo->compensationDetails($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function compensationDetailsSummary()
{
return response()->json($this->repo->compensationDetailSummary($this->filters('compensation')));
}
public function compensationDetailsOptions()
{
return response()->json($this->repo->filterOptionsForCompensation());
}
// ============================================================
// 借支明细
// ============================================================
public function advanceDetailsIndex()
{
$filters = $this->filters('advance');
$result = $this->repo->advanceDetails($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function advanceDetailsSummary()
{
return response()->json($this->repo->advanceDetailSummary($this->filters('advance')));
}
public function advanceDetailsOptions()
{
return response()->json($this->repo->filterOptionsForAdvance());
}
// ============================================================
// 样品扣款
// ============================================================
public function sampleDeductionsIndex()
{
$filters = $this->filters('sample');
$result = $this->repo->sampleDeductions($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function sampleDeductionsSummary()
{
return response()->json($this->repo->sampleDeductionSummary($this->filters('sample')));
}
public function sampleDeductionsOptions()
{
return response()->json($this->repo->filterOptionsForSample());
}
// ============================================================
// Helpers
// ============================================================
private function filters(string $type): array
{
$filters = [];
// 通用文本字段
$text = ['supplier', 'warehouse', 'sku_id', 'express_no', 'creator', 'department'];
foreach ($text as $field) {
if ($val = request($field)) {
$filters[$field] = trim($val);
}
}
// 日期
foreach (['date_from', 'date_to', 'month_from', 'month_to'] as $field) {
if ($val = request($field)) {
$filters[$field] = $val;
}
}
return $filters;
}
private function page(): int
{
return max((int) request('page', 1), 1);
}
private function limit(): int
{
return min(max((int) request('limit', 25), 1), 100);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 加权平均单价表
* pm_averageUnitPrice
*
* 按货号+供应商存储的加权平均单价(262条)。
* 用于计算"预估金额"。
*
* @property int $id
* @property string $number 货号
* @property string $supplier 供应商名称
* @property float $price 加权平均单价(页面表头第11列来源)
* @property string $name 商品名称
*/
class AverageUnitPrice extends JianSunModel
{
protected $table = 'pm_averageUnitPrice';
protected $fillable = ['number', 'supplier', 'price', 'name'];
protected $casts = [
'price' => 'decimal:4',
];
}

View File

@ -0,0 +1,27 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
use Illuminate\Database\Eloquent\Model;
/**
* Base Model for JianSun SQL Server database tables.
* All models extending this will automatically use the 'jiansun' connection.
*/
abstract class JianSunModel extends Model
{
/** @var string */
protected $connection = 'jiansun';
/** @var bool JianSun tables don't use Laravel timestamps */
public $timestamps = false;
/** @var string No prefix on JianSun tables */
protected $prefix = '';
/** @var string */
protected $primaryKey = 'id';
/** @var bool */
public $incrementing = true;
}

View File

@ -0,0 +1,45 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 订单快递详情表(代发记录)
* jst_order_express_details
*
* 存储所有代发订单的快递信息(2,812,915条)。
* 页面中"代发记录"的快递名称和快递单号来源于此表。
*
* @property int $id
* @property string $so_id 订单编号
* @property string $logistic_id 快递单号(页面表头第18列)
* @property string $logistic_company_name 快递名称(页面表头第17列)
* @property string $logistic_status_name 物流状态
* @property string $warehouse_name 仓库名称
* @property string $send_time 发货时间(页面表头第3列)
* @property string $sign_time 签收时间
* @property float $weight 重量
*/
class JstOrderExpressDetails extends JianSunModel
{
protected $table = 'jst_order_express_details';
protected $fillable = [
'createdAt', 'updatedAt', 'inner_order_id', 'so_id', 'logistic_id',
'logistic_company_name', 'logistic_status_name', 'logistic_return_flag',
'order_status_name', 'first_question_type', 'first_question_time',
'aftersale_status_name', 'order_labels', 'remark', 'buyer_message',
'shop_name', 'platform_name', 'dis_co_name', 'warehouse_name',
'receiver_address', 'pay_time', 'first_oa_confirm_time',
'last_oa_confirm_time', 'send_time', 'fetch_time', 'sign_time',
'logistic_time', 'weight', 'f_weight', 'ship_time_by_pdh',
];
protected $casts = [
'send_time' => 'datetime',
'sign_time' => 'datetime',
'pay_time' => 'datetime',
'fetch_time' => 'datetime',
'weight' => 'decimal:4',
'f_weight' => 'decimal:4',
];
}

View File

@ -0,0 +1,31 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 聚水潭供应商表
* jst_supplier
*
* 聚水潭ERP系统的供应商数据(2,075条)。
*
* @property int $Id
* @property int $supplier_id 供应商ID
* @property string $supplier_code 供应商编码
* @property string $name 供应商名称
* @property string $modified 修改时间
* @property string $remark 备注
* @property string $group 分组
* @property int $enabled 是否启用
*/
class JstSupplier extends JianSunModel
{
protected $table = 'jst_supplier';
protected $primaryKey = 'Id';
protected $fillable = [
'supplier_id', 'supplier_code', 'name', 'modified', 'remark',
'remark2', 'remark3', 'group', 'enabled', 'depositbank',
'bankacount', 'acountnumber',
];
}

View File

@ -0,0 +1,45 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 采购入库单主表
* pm_purchaseIn
*
* 每次采购入库的汇总记录(52,219条)。
* 通过 id 关联 pm_purchaseInItem 获取明细。
*
* @property int $id
* @property string $hno 入库单号(页面表头第1列)
* @property string $whcode 仓库编码
* @property string $sourceno 来源单号
* @property string $title 标题
* @property string $scmname 供应商名称
* @property string $scmcode 供应商编码
* @property int $scmid 供应商ID
* @property int $incount 入库总数
* @property string $created 创建时间(页面表头第16列)
* @property string $memo 备注(页面表头第14列)
* @property string $expcode 快递编码
* @property string $expname 快递名称
*/
class PurchaseIn extends JianSunModel
{
protected $table = 'pm_purchaseIn';
protected $fillable = [
'whcode', 'hno', 'sourceno', 'title', 'scmname', 'scmcode', 'scmid',
'incount', 'created', 'memo', 'expcode', 'expname', 'postcode', 'postfee',
'creater', 'createrid', 'inwhdate', 'finishdate', 'procode', 'proname',
'skucode', 'skuname',
];
protected $casts = [
'incount' => 'integer',
'scmid' => 'integer',
'postfee' => 'decimal:2',
'created' => 'datetime',
'inwhdate' => 'datetime',
'finishdate' => 'datetime',
];
}

View File

@ -0,0 +1,39 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 采购入库单明细表
* pm_purchaseInItem
*
* 每次采购入库的商品明细(223,382条)。
* 通过 hid 关联 pm_purchaseIn.id。
*
* @property int $id
* @property int $hid 关联 PurchaseIn.id
* @property string $skucode 商品编码(页面表头第6列)
* @property string $skuname 规格名称(页面表头第7列)
* @property string $skuoid 外部商品编码
* @property int $incount 入库数量(页面表头第8列)
* @property int $skucount SKU计数
* @property float $payment 单价
* @property string $created 创建时间
*/
class PurchaseInItem extends JianSunModel
{
protected $table = 'pm_purchaseInItem';
protected $fillable = [
'hid', 'skucode', 'skuname', 'skuoid', 'incount', 'skucount',
'payment', 'created', 'procode', 'proname', 'batchCode',
'inventoryType', 'productDate', 'expireDate', 'produceCode',
];
protected $casts = [
'hid' => 'integer',
'incount' => 'integer',
'skucount' => 'integer',
'payment' => 'decimal:2',
'created' => 'datetime',
];
}

View File

@ -0,0 +1,29 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* SKU信息管理表
* js_sku_info_manage
*
* 商品SKU管理信息(8,394条),包含货号、商品编码等。
*
* @property int $id
* @property string $sku_id 商品编码(页面表头第6列)
* @property string $sku 货号(页面表头第5列)
* @property string $spu SPU
* @property string $sku_manager SKU负责人
* @property string $operation_dept_manager 运营部门负责人
* @property string $designer 设计师
* @property string $designer_dept 设计部门
*/
class SkuInfoManage extends JianSunModel
{
protected $table = 'js_sku_info_manage';
protected $fillable = [
'sku_id', 'sku', 'spu', 'sku_manager',
'operation_dept_manager', 'sku_business_unit_manager',
'designer', 'designer_dept', 'create_time', 'update_time',
];
}

View File

@ -0,0 +1,40 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 供应商基础信息表
* js_supplier
*
* 供应商主数据(1,479条),包含供应商编码、名称、公司等。
*
* @property int $Id
* @property string $scmcode 供应商编码
* @property string $scmname 供应商名称
* @property string $scmtype 供应商类型
* @property string $company 公司全称
* @property string $paycyclesgn 结算周期标记
* @property string $reconciliter 对账人
* @property string $buyer 采购员
* @property string $coopersgn 合作状态标记
* @property string $levelname 等级名称
* @property string $memo 备注
* @property string $enterpriseType 企业类型
* @property string $creditcode 统一社会信用代码
*/
class Supplier extends JianSunModel
{
protected $table = 'js_supplier';
protected $primaryKey = 'Id';
protected $fillable = [
'scmcode', 'scmname', 'scmtype', 'tradebody', 'company',
'paycyclesgn', 'accountbankno', 'corpmobile', 'tradebtime', 'tradeetime',
'coopersgn', 'cooperbtime', 'cooperetime', 'dsperid', 'dsper',
'reconciliter', 'levelname', 'buyerid', 'buyer', 'memo',
'guid', 'craftType', 'outline', 'capacity', 'freereason', 'area',
'stocker', 'region', 'sindou', 'follower', 'presc', 'strategy',
'enterpriseType', 'creditcode', 'handlePerson', 'legalPerson',
];
}

View File

@ -0,0 +1,49 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 供应商采购结算明细表
* fys_supplierGoods_sttlement_purchase
*
* 采购入库结算明细,包含每个SKU的结算数量和价格。
* 是货款对账页面"入库记录"的核心数据源(228,039条)。
*
* @property int $Id
* @property string $created 创建时间
* @property int $stock 入库数量
* @property int $settlementStock 结算数量
* @property float $singlePrice 单价
* @property float $settlementPrice 结算价格
* @property string $priceType 价格类型
* @property string $ownername 货主
* @property string $scmname 供应商名称
* @property string $procode 货号
* @property string $whname 仓库名称
* @property string $skuoid 商品编码
* @property string $skuname 规格名称
* @property string $bizpo 业务采购单号
* @property string $spo 采购单号
* @property string $remark 备注
*/
class SupplierGoodsSttlementPurchase extends JianSunModel
{
protected $table = 'fys_supplierGoods_sttlement_purchase';
protected $primaryKey = 'Id';
protected $fillable = [
'created', 'stock', 'settlementStock', 'singlePrice', 'settlementPrice',
'priceType', 'ownername', 'scmname', 'procode', 'whname',
'skuoid', 'skuname', 'bizpo', 'spo', 'remark', 'createTime',
];
protected $casts = [
'stock' => 'integer',
'settlementStock' => 'integer',
'singlePrice' => 'decimal:2',
'settlementPrice' => 'decimal:2',
'created' => 'datetime',
'createTime' => 'datetime',
];
}

View File

@ -0,0 +1,36 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 供应商付款明细表
* fys_supplierPaymentDetail
*
* @property int $Id
* @property string $created 创建时间
* @property string $month 月份
* @property string $date 日期
* @property string $scmname 供应商名称
* @property string $scmcode 供应商编码
* @property float $payment 付款金额
* @property string $reason 原因
* @property string $remark 备注
* @property int $billstatus 单据状态
*/
class SupplierPaymentDetail extends JianSunModel
{
protected $table = 'fys_supplierPaymentDetail';
protected $primaryKey = 'Id';
protected $fillable = [
'created', 'month', 'date', 'scmname', 'scmcode',
'payment', 'reason', 'remark', 'billstatus',
];
protected $casts = [
'payment' => 'decimal:2',
'created' => 'datetime',
'date' => 'date',
];
}

View File

@ -0,0 +1,85 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 供应商付款汇总表
* fys_supplierPaymentSummary
*
* 核心对账数据源,每条记录代表一个供应商在某月份的应付汇总
*
* @property int $id
* @property string $company 公司
* @property string $supplier 供应商名称
* @property string $checker 对账人
* @property string $settleway 结算方式
* @property string $Month 对账月份
* @property float $ExpFieght 快递费
* @property float $bginMonthPay 月初应付
* @property float $unpriced 未定价
* @property float $advloan 预付
* @property float $loan 借款
* @property float $refund 退款
* @property float $procurement 采购
* @property float $invoiceDct 发票扣款
* @property float $boardDct 板材扣款
* @property float $specimenDct 样品扣款
* @property float $Totalindemnity 总赔偿
* @property float $other 其他
* @property float $supplySup 供应商补贴
* @property float $ShouldPay 应付金额
* @property float $factPay 实付金额
* @property float $guessPrice 预估金额
* @property float $toCorporate 到公账
* @property float $toPrivate 到私账
* @property string $remarks 备注
* @property string $reason 原因
* @property string $guid GUID
* @property int $isExamine 是否审核
* @property int $isPayment 是否付款
* @property string $scmcode 供应商编码
* @property int $supid 供应商ID
* @property string $createTime 创建时间
*/
class SupplierPaymentSummary extends JianSunModel
{
protected $table = 'fys_supplierPaymentSummary';
protected $fillable = [
'company', 'supplier', 'checker', 'settleway', 'Month',
'ExpFieght', 'bginMonthPay', 'unpriced', 'advloan', 'loan',
'refund', 'procurement', 'invoiceDct', 'boardDct', 'specimenDct',
'Totalindemnity', 'other', 'supplySup', 'ShouldPay', 'factPay',
'guessPrice', 'toCorporate', 'toPrivate', 'remarks', 'reason',
'mark', 'guid', 'isExamine', 'isPayment', 'Month',
'billstatus', 'opstatus', 'elstatu', 'taskid', 'supid',
'depositMoney', 'depositType', 'cashierbypaid', 'push', 'scmcode',
'billRemaining',
];
protected $casts = [
'ExpFieght' => 'decimal:2',
'bginMonthPay' => 'decimal:2',
'unpriced' => 'decimal:2',
'advloan' => 'decimal:2',
'loan' => 'decimal:2',
'refund' => 'decimal:2',
'procurement' => 'decimal:2',
'invoiceDct' => 'decimal:2',
'boardDct' => 'decimal:2',
'specimenDct' => 'decimal:2',
'Totalindemnity' => 'decimal:2',
'other' => 'decimal:2',
'supplySup' => 'decimal:2',
'ShouldPay' => 'decimal:2',
'factPay' => 'decimal:2',
'guessPrice' => 'decimal:2',
'toCorporate' => 'decimal:2',
'toPrivate' => 'decimal:2',
'depositMoney' => 'decimal:2',
'cashierbypaid' => 'decimal:2',
'billRemaining' => 'decimal:2',
'createTime' => 'datetime',
'modifiedTime' => 'datetime',
];
}

View File

@ -0,0 +1,43 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 未发出订单明细表
* pm_unOrderDetails
*
* 记录已采购但尚未发出的商品明细。
*
* @property int $Id
* @property string $date 日期
* @property string $purchase 采购单号
* @property string $warehouse 仓库
* @property string $supplier 供应商(页面表头第4列)
* @property string $transactionNo 交易号
* @property string $distributionNo 配送号
* @property string $expressNo 快递单号(页面表头第18列)
* @property string $sku 货号
* @property string $barCode 条码
* @property string $skuName 规格名称
* @property string $distributionDate 配送日期
* @property string $shipDate 发货日期
* @property string $expressName 快递名称(页面表头第17列)
*/
class UnOrderDetails extends JianSunModel
{
protected $table = 'pm_unOrderDetails';
protected $primaryKey = 'Id';
protected $fillable = [
'date', 'purchase', 'warehouse', 'supplier', 'transactionNo',
'distributionNo', 'expressNo', 'sku', 'barCode', 'skuName',
'distributionDate', 'shipDate', 'expressName',
];
protected $casts = [
'date' => 'datetime',
'distributionDate' => 'datetime',
'shipDate' => 'datetime',
];
}

View File

@ -0,0 +1,370 @@
<?php
namespace Modules\EcommerceFinance\Repositories;
use Modules\EcommerceFinance\Services\JianSunConnection;
use PDO;
/**
* 货款对账数据仓库
*
* 数据来源:
* - 入库记录:fys_supplierGoods_sttlement_purchase(~228K)+ pm_purchaseIn + pm_averageUnitPrice
* - 代发记录:fys_supplierGoods_sttlement_sale(~2.8M)+ pm_averageUnitPrice
*
* 性能策略:入库/代发分开查询,各自执行分页,避免全量 UNION ALL。
*/
class GoodsReconciliationRepository
{
private PDO $pdo;
public function __construct()
{
$this->pdo = JianSunConnection::get();
}
// ===================================================================
// 汇总统计(轻量:只用 COUNT + SUM,不做复杂 JOIN)
// ===================================================================
public function summary(array $filters = []): array
{
$purchase = $this->purchaseSummary($filters);
$sale = $this->saleSummary($filters);
return [
'totalAmount' => round($purchase['totalAmount'] + $sale['totalAmount'], 2),
'totalGuessAmount' => round($purchase['totalGuessAmount'] + $sale['totalGuessAmount'], 2),
'sentCount' => $purchase['sentCount'] + $sale['sentCount'],
'unsentCount' => $purchase['unsentCount'] + $sale['unsentCount'],
'totalCount' => $purchase['totalCount'] + $sale['totalCount'],
];
}
private function purchaseSummary(array $filters): array
{
$where = $this->buildPurchaseWhere($filters);
$params = $this->buildPurchaseParams($filters);
// 按需 JOIN,减少不必要的全表扫描
$joinA = $joinU = '';
if (strpos($where, 'a.') !== false) {
$joinA = ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier';
}
if (strpos($where, 'u.') !== false) {
$joinU = ' LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku';
}
$sql = "SELECT
COUNT(*) as totalCount,
ISNULL(SUM(s.settlementPrice), 0) as totalAmount,
0 as totalGuessAmount,
COUNT(*) as sentCount,
0 as unsentCount
FROM fys_supplierGoods_sttlement_purchase s
{$joinA}{$joinU}
WHERE 1=1 {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row ?: ['totalCount' => 0, 'totalAmount' => 0, 'totalGuessAmount' => 0, 'sentCount' => 0, 'unsentCount' => 0];
}
private function saleSummary(array $filters): array
{
$where = $this->buildSaleWhere($filters);
$params = $this->buildSaleParams($filters);
$joinA = '';
if (strpos($where, 'a.') !== false) {
$joinA = ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier';
}
$sql = "SELECT
COUNT(*) as totalCount,
ISNULL(SUM(s.settlementPrice), 0) as totalAmount,
0 as totalGuessAmount,
ISNULL(SUM(CASE WHEN s.expressNo IS NOT NULL AND s.expressNo != '' THEN 1 ELSE 0 END), 0) as sentCount,
ISNULL(SUM(CASE WHEN s.expressNo IS NULL OR s.expressNo = '' THEN 1 ELSE 0 END), 0) as unsentCount
FROM fys_supplierGoods_sttlement_sale s
{$joinA}
WHERE 1=1 {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row ?: ['totalCount' => 0, 'totalAmount' => 0, 'totalGuessAmount' => 0, 'sentCount' => 0, 'unsentCount' => 0];
}
// ===================================================================
// 列表查询(核心 — 分别查入库/代发再合并)
// ===================================================================
public function list(array $filters = [], int $page = 1, int $perPage = 25): array
{
$type = $filters['record_type'] ?? 'all';
// 分别查询(同一分页参数,各自内部分页)
$purchaseResult = null;
$saleResult = null;
$totalPurchase = 0;
$totalSale = 0;
if ($type === 'all' || $type === 'purchase') {
$where = $this->buildPurchaseWhere($filters);
$params = $this->buildPurchaseParams($filters);
$totalPurchase = $this->countPurchase($where, $params);
$purchaseResult = $this->fetchPurchase($where, $params, $page, $perPage);
}
if ($type === 'all' || $type === 'sale') {
$where = $this->buildSaleWhere($filters);
$params = $this->buildSaleParams($filters);
$totalSale = $this->countSale($where, $params);
$saleResult = $this->fetchSale($where, $params, $page, $perPage);
}
// 合并并按 createTime 排序,取前 perPage 条
$merged = array_merge($purchaseResult ?? [], $saleResult ?? []);
// 按 createTime DESC 排序
usort($merged, function ($a, $b) {
return strcmp($b['createTime'] ?? '', $a['createTime'] ?? '');
});
// 截取当前页
$merged = array_slice($merged, 0, $perPage);
// 格式化金额
foreach ($merged as &$row) {
$row['amount'] = round((float) $row['amount'], 2);
$row['guessAmount'] = round((float) $row['guessAmount'], 2);
$row['unitPrice'] = round((float) $row['unitPrice'], 2);
$row['guessUnitPrice'] = round((float) ($row['guessUnitPrice'] ?? 0), 2);
}
return [
'data' => $merged,
'total' => $totalPurchase + $totalSale,
];
}
// ===================================================================
// 筛选器选项
// ===================================================================
public function filterOptions(): array
{
$sql = "SELECT DISTINCT scmname FROM fys_supplierGoods_sttlement_purchase WHERE scmname IS NOT NULL AND scmname != ''
UNION
SELECT DISTINCT supplier FROM fys_supplierGoods_sttlement_sale WHERE supplier IS NOT NULL AND supplier != ''
ORDER BY scmname";
$stmt = $this->pdo->query($sql);
$suppliers = $stmt->fetchAll(PDO::FETCH_COLUMN);
$sql = "SELECT DISTINCT whname FROM fys_supplierGoods_sttlement_purchase WHERE whname IS NOT NULL AND whname != ''
UNION
SELECT DISTINCT whName FROM fys_supplierGoods_sttlement_sale WHERE whName IS NOT NULL AND whName != ''
ORDER BY whname";
$stmt = $this->pdo->query($sql);
$warehouses = $stmt->fetchAll(PDO::FETCH_COLUMN);
return [
'suppliers' => $suppliers,
'warehouses' => $warehouses,
];
}
// ===================================================================
// 入库查询
// ===================================================================
private function countPurchase(string $where, array $params): int
{
// 仅当筛选条件引用关联表字段时才 JOIN,否则纯主表 COUNT
$joins = '';
if (strpos($where, 'p.') !== false) {
$joins .= ' LEFT JOIN pm_purchaseIn p ON s.spo = p.sourceno';
}
if (strpos($where, 'a.') !== false) {
$joins .= ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier';
}
if (strpos($where, 'u.') !== false) {
$joins .= ' LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku';
}
$sql = "SELECT COUNT(*) as cnt FROM fys_supplierGoods_sttlement_purchase s{$joins} WHERE 1=1 {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetch()['cnt'];
}
private function fetchPurchase(string $where, array $params, int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
$sql = "SELECT
'purchase' AS recordType,
ISNULL(p.hno, '') AS stockNo,
ISNULL(s.spo, '') AS purchaseNo,
FORMAT(ISNULL(s.created, s.createTime), 'yyyy-MM-dd HH:mm:ss') AS sendTime,
ISNULL(s.scmname, '') AS supplier,
ISNULL(s.skuoid, '') AS goodsNo,
ISNULL(s.procode, '') AS productCode,
ISNULL(s.skuname, '') AS specName,
ISNULL(s.settlementStock, 0) AS qty,
ISNULL(s.singlePrice, 0) AS unitPrice,
ISNULL(s.settlementPrice, 0) AS amount,
ISNULL(a.price, 0) AS guessUnitPrice,
ISNULL(s.settlementStock * ISNULL(a.price, 0), 0) AS guessAmount,
'' AS supplierGoodsCode,
ISNULL(s.remark, '') AS remark,
ISNULL(s.whname, '') AS warehouseName,
FORMAT(s.createTime, 'yyyy-MM-dd HH:mm:ss') AS createTime,
'' AS expressName,
'' AS expressNo,
CASE WHEN u.Id IS NOT NULL THEN 1 ELSE 0 END AS isUnsent,
s.id AS sourceId
FROM fys_supplierGoods_sttlement_purchase s
LEFT JOIN pm_purchaseIn p ON s.spo = p.sourceno
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier
LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku
WHERE 1=1 {$where}
ORDER BY s.createTime DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
// ===================================================================
// 代发查询
// ===================================================================
private function countSale(string $where, array $params): int
{
$sql = "SELECT COUNT(*) as cnt FROM fys_supplierGoods_sttlement_sale s
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier
WHERE 1=1 {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetch()['cnt'];
}
private function fetchSale(string $where, array $params, int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
$sql = "SELECT
'sale' AS recordType,
'' AS stockNo,
'' AS purchaseNo,
FORMAT(s.sendTime, 'yyyy-MM-dd HH:mm:ss') AS sendTime,
ISNULL(s.supplier, '') AS supplier,
ISNULL(s.skucode, '') AS goodsNo,
ISNULL(s.procode, '') AS productCode,
ISNULL(s.skuname, '') AS specName,
ISNULL(s.settlementStock, 0) AS qty,
ISNULL(s.singlePrice, 0) AS unitPrice,
ISNULL(s.settlementPrice, 0) AS amount,
ISNULL(a.price, 0) AS guessUnitPrice,
ISNULL(s.settlementStock * ISNULL(a.price, 0), 0) AS guessAmount,
'' AS supplierGoodsCode,
ISNULL(s.remark, '') AS remark,
ISNULL(s.whName, '') AS warehouseName,
FORMAT(s.createTime, 'yyyy-MM-dd HH:mm:ss') AS createTime,
ISNULL(s.expressName, '') AS expressName,
ISNULL(s.expressNo, '') AS expressNo,
CASE WHEN s.expressNo IS NULL OR s.expressNo = '' THEN 1 ELSE 0 END AS isUnsent,
s.Id AS sourceId
FROM fys_supplierGoods_sttlement_sale s
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier
WHERE 1=1 {$where}
ORDER BY s.createTime DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
// ===================================================================
// WHERE 与参数构建
// ===================================================================
private function like(string $v): string
{
return '%' . $v . '%';
}
// --- 入库 WHERE ---
private function buildPurchaseWhere(array $filters): string
{
$c = [];
if (!empty($filters['supplier'])) $c[] = 'AND s.scmname LIKE :supplier';
if (!empty($filters['stock_no'])) $c[] = 'AND p.hno LIKE :stock_no';
if (!empty($filters['purchase_no'])) $c[] = 'AND s.spo LIKE :purchase_no';
if (!empty($filters['product_code'])) $c[] = 'AND s.procode LIKE :product_code';
if (!empty($filters['warehouse'])) $c[] = 'AND s.whname LIKE :warehouse';
if (!empty($filters['send_time_from'])) $c[] = 'AND s.created >= :send_time_from';
if (!empty($filters['send_time_to'])) $c[] = 'AND s.created <= :send_time_to';
if (!empty($filters['create_time_from'])) $c[] = 'AND s.createTime >= :create_time_from';
if (!empty($filters['create_time_to'])) $c[] = 'AND s.createTime <= :create_time_to';
if (!empty($filters['sent_status'])) {
if ($filters['sent_status'] === 'sent') $c[] = 'AND u.Id IS NULL';
if ($filters['sent_status'] === 'unsent') $c[] = 'AND u.Id IS NOT NULL';
}
if (!empty($filters['has_diff'])) {
$c[] = 'AND ABS(ISNULL(s.settlementPrice, 0) - ISNULL(s.settlementStock * ISNULL(a.price, 0), 0)) > 0.01';
}
return $c ? ' ' . implode(' ', $c) : '';
}
private function buildPurchaseParams(array $filters): array
{
$p = [];
if (!empty($filters['supplier'])) $p[':supplier'] = $this->like($filters['supplier']);
if (!empty($filters['stock_no'])) $p[':stock_no'] = $this->like($filters['stock_no']);
if (!empty($filters['purchase_no'])) $p[':purchase_no'] = $this->like($filters['purchase_no']);
if (!empty($filters['product_code'])) $p[':product_code'] = $this->like($filters['product_code']);
if (!empty($filters['warehouse'])) $p[':warehouse'] = $this->like($filters['warehouse']);
if (!empty($filters['send_time_from'])) $p[':send_time_from'] = $filters['send_time_from'];
if (!empty($filters['send_time_to'])) $p[':send_time_to'] = $filters['send_time_to'];
if (!empty($filters['create_time_from'])) $p[':create_time_from'] = $filters['create_time_from'];
if (!empty($filters['create_time_to'])) $p[':create_time_to'] = $filters['create_time_to'];
return $p;
}
// --- 代发 WHERE ---
private function buildSaleWhere(array $filters): string
{
$c = [];
if (!empty($filters['supplier'])) $c[] = 'AND s.supplier LIKE :supplier_s';
if (!empty($filters['product_code'])) $c[] = 'AND s.procode LIKE :product_code_s';
if (!empty($filters['express_no'])) $c[] = 'AND s.expressNo LIKE :express_no';
if (!empty($filters['warehouse'])) $c[] = 'AND s.whName LIKE :warehouse_s';
if (!empty($filters['send_time_from'])) $c[] = 'AND s.sendTime >= :send_time_from_s';
if (!empty($filters['send_time_to'])) $c[] = 'AND s.sendTime <= :send_time_to_s';
if (!empty($filters['create_time_from'])) $c[] = 'AND s.createTime >= :create_time_from_s';
if (!empty($filters['create_time_to'])) $c[] = 'AND s.createTime <= :create_time_to_s';
if (!empty($filters['sent_status'])) {
if ($filters['sent_status'] === 'sent') $c[] = "AND s.expressNo IS NOT NULL AND s.expressNo != ''";
if ($filters['sent_status'] === 'unsent') $c[] = "AND (s.expressNo IS NULL OR s.expressNo = '')";
}
if (!empty($filters['has_diff'])) {
$c[] = 'AND ABS(ISNULL(s.settlementPrice, 0) - ISNULL(s.settlementStock * ISNULL(a.price, 0), 0)) > 0.01';
}
return $c ? ' ' . implode(' ', $c) : '';
}
private function buildSaleParams(array $filters): array
{
$p = [];
if (!empty($filters['supplier'])) $p[':supplier_s'] = $this->like($filters['supplier']);
if (!empty($filters['product_code'])) $p[':product_code_s'] = $this->like($filters['product_code']);
if (!empty($filters['express_no'])) $p[':express_no'] = $this->like($filters['express_no']);
if (!empty($filters['warehouse'])) $p[':warehouse_s'] = $this->like($filters['warehouse']);
if (!empty($filters['send_time_from'])) $p[':send_time_from_s'] = $filters['send_time_from'];
if (!empty($filters['send_time_to'])) $p[':send_time_to_s'] = $filters['send_time_to'];
if (!empty($filters['create_time_from'])) $p[':create_time_from_s'] = $filters['create_time_from'];
if (!empty($filters['create_time_to'])) $p[':create_time_to_s'] = $filters['create_time_to'];
return $p;
}
}

View File

@ -0,0 +1,540 @@
<?php
namespace Modules\EcommerceFinance\Repositories;
use Modules\EcommerceFinance\Services\JianSunConnection;
use PDO;
/**
* 供应商应付 - 子功能页面数据仓库
*
* 封装 5 个子页面的数据访问逻辑:
* - returnReconciliations() 退货对账
* - invoiceDeductions() 发票扣款
* - compensationDetails() 赔偿明细(UNION 纸板 + 压板)
* - advanceDetails() 借支明细
* - sampleDeductions() 样品扣款
*
* 所有方法返回统一结构: ['data' => array<int,array>, 'total' => int]
*/
class SupplierPayableRepository
{
private PDO $db;
public function __construct()
{
$this->db = JianSunConnection::get();
}
/**
* 通用工具:带筛选 + 分页的查询
*/
private function runQuery(string $sql, array $params, int $page, int $pageSize, int $countThreshold = 5000): array
{
$offset = ($page - 1) * $pageSize;
// 1. 总数(先查总数,超阈值再用近似)
$countSql = 'SELECT COUNT(*) AS cnt FROM (' . $sql . ') AS sub';
$total = 0;
try {
$stmt = $this->db->prepare($countSql);
$stmt->execute($params);
$total = (int) $stmt->fetchColumn();
} catch (\Throwable $e) {
// 极端情况:表太大查不到总数时退回到 -1
$total = -1;
}
// 2. 分页数据(用 TOP + OFFSET/FETCH 不可行,统一使用 ROW_NUMBER() OVER 包装)
$pagedSql = 'SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS rn FROM (' . $sql . ') AS src) AS t WHERE rn BETWEEN ? AND ?';
$paramsWithRange = array_merge($params, [$offset + 1, $offset + $pageSize]);
$stmt = $this->db->prepare($pagedSql);
$stmt->execute($paramsWithRange);
return [
'data' => $stmt->fetchAll(),
'total' => $total,
];
}
/**
* 计算分页的 SELECT TOP N(无 COUNT),加速大表查询
*/
private function runQueryFast(string $sql, array $params, int $page, int $pageSize, int $total = -1): array
{
$offset = ($page - 1) * $pageSize;
$pagedSql = 'SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS rn FROM (' . $sql . ') AS src) AS t WHERE rn BETWEEN ? AND ?';
$paramsWithRange = array_merge($params, [$offset + 1, $offset + $pageSize]);
$stmt = $this->db->prepare($pagedSql);
$stmt->execute($paramsWithRange);
return [
'data' => $stmt->fetchAll(),
'total' => $total,
];
}
// ============================================================
// 1. 退货对账 — finance_purchaseReturn (36791行)
// ============================================================
public function returnReconciliations(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplierName LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['warehouse'])) {
$where[] = 'warehouseName LIKE ?';
$params[] = '%' . $filters['warehouse'] . '%';
}
if (!empty($filters['sku_id'])) {
$where[] = 'sku_id LIKE ?';
$params[] = '%' . $filters['sku_id'] . '%';
}
if (!empty($filters['express_no'])) {
$where[] = 'expressNo LIKE ?';
$params[] = '%' . $filters['express_no'] . '%';
}
if (!empty($filters['date_from'])) {
$where[] = 'date >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'date <= ?';
$params[] = $filters['date_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
CONVERT(varchar(10), date, 120) AS send_time,
supplierName AS supplier,
sku_id AS barcode,
name AS sku_name,
CAST(qty AS decimal(18,2)) AS qty,
CAST(price AS decimal(18,2)) AS unit_price,
CAST(price * qty AS decimal(18,2)) AS amount,
CAST(priceB AS decimal(18,2)) AS guess_unit_price,
CAST(priceB * qty AS decimal(18,2)) AS guess_amount,
remark,
io_id AS wms_pick_no,
warehouseName AS warehouse,
expressNo AS express_no,
expressName AS express_name,
CONVERT(varchar(10), io_date, 120) AS io_date,
labels,
creator_name AS creator,
'' AS title,
'' AS supplier_barcode,
'' AS original_barcode,
'' AS reconciler
FROM finance_purchaseReturn
$whereSql";
$result = $this->runQuery($sql, $params, $page, $pageSize);
return [
'data' => $result['data'],
'total' => $result['total'],
];
}
public function returnReconciliationSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplierName LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['date_from'])) {
$where[] = 'date >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'date <= ?';
$params[] = $filters['date_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
ISNULL(SUM(price * qty), 0) AS total_amount,
ISNULL(SUM(priceB * qty), 0) AS total_guess_amount,
COUNT(*) AS total_count
FROM finance_purchaseReturn $whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalAmount' => (float) $row['total_amount'],
'totalGuessAmount' => (float) $row['total_guess_amount'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 2. 发票扣款 — finance_invoiceDeduction (195行)
// ============================================================
public function invoiceDeductions(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['month_from'])) {
$where[] = 'month >= ?';
$params[] = $filters['month_from'];
}
if (!empty($filters['month_to'])) {
$where[] = 'month <= ?';
$params[] = $filters['month_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
CONVERT(varchar(7), month, 120) AS month_str,
'' AS category,
supplier AS payee,
supplier AS receiver,
'' AS board_factory,
CAST(0 AS decimal(18,2)) AS subsidy_fee,
remark,
CAST(price AS decimal(18,2)) AS price,
remark AS detail,
'' AS note,
personCharge AS person_charge,
CONVERT(varchar(19), createTime, 120) AS create_time,
creator
FROM finance_invoiceDeduction
$whereSql";
return $this->runQuery($sql, $params, $page, $pageSize);
}
public function invoiceDeductionSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT ISNULL(SUM(price), 0) AS total_price, COUNT(*) AS total_count FROM finance_invoiceDeduction $whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalPrice' => (float) $row['total_price'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 3. 赔偿明细 — fys_paper_factory + fys_pressing_factory UNION
// ============================================================
public function compensationDetails(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['month_from'])) {
$where[] = 'month >= ?';
$params[] = $filters['month_from'];
}
if (!empty($filters['month_to'])) {
$where[] = 'month <= ?';
$params[] = $filters['month_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
month,
supplier,
CAST(compensation_amount AS decimal(18,2)) AS amount,
reason,
'' AS remark,
creator,
CONVERT(varchar(19), create_time, 120) AS create_time,
'纸板厂' AS factory_type
FROM (
SELECT
month,
factory_subject_name AS supplier,
compensation_amount,
reason,
creator,
create_time
FROM fys_paper_factory_compensation_detail
UNION ALL
SELECT
month,
factory_subject_name AS supplier,
compensation_amount,
reason,
creator,
create_time
FROM fys_pressing_factory_compensation_detail
) AS t
$whereSql
ORDER BY month DESC, create_time DESC";
return $this->runQuery($sql, $params, $page, $pageSize);
}
public function compensationDetailSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
ISNULL(SUM(compensation_amount), 0) AS total_amount,
COUNT(*) AS total_count
FROM (
SELECT compensation_amount, factory_subject_name AS supplier FROM fys_paper_factory_compensation_detail
UNION ALL
SELECT compensation_amount, factory_subject_name AS supplier FROM fys_pressing_factory_compensation_detail
) AS t
$whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalAmount' => (float) $row['total_amount'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 4. 借支明细 — finance_advanceLoans (4行, 字段不足,部分列显示-)
// ============================================================
public function advanceDetails(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['month_from'])) {
$where[] = 'month >= ?';
$params[] = $filters['month_from'];
}
if (!empty($filters['month_to'])) {
$where[] = 'month <= ?';
$params[] = $filters['month_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
supplier,
'' AS contract_name,
CAST(price AS decimal(18,2)) AS advance_amount,
CAST(0 AS decimal(18,2)) AS actual_pay,
CAST(0 AS decimal(18,2)) AS month_deduct,
CAST(0 AS decimal(18,2)) AS should_deduct,
'' AS deduct_start_month,
'' AS deduct_end_month,
0 AS remain_periods,
CONVERT(varchar(7), month, 120) AS month_str,
'' AS loan_term,
remark,
'' AS is_paid,
CONVERT(varchar(7), month, 120) AS pay_month,
'' AS serial_no,
'' AS loan_agreement,
personCharge AS person_charge,
'' AS receipt_0,
'' AS receipt_1,
'' AS receipt_2,
CONVERT(varchar(19), createTime, 120) AS create_time,
creator
FROM finance_advanceLoans
$whereSql
ORDER BY month DESC, createTime DESC";
return $this->runQuery($sql, $params, $page, $pageSize);
}
public function advanceDetailSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT ISNULL(SUM(price), 0) AS total_amount, COUNT(*) AS total_count FROM finance_advanceLoans $whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalAmount' => (float) $row['total_amount'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 5. 样品扣款 — finance_sampleDeduction (12行)
// ============================================================
public function sampleDeductions(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['department'])) {
$where[] = 'department LIKE ?';
$params[] = '%' . $filters['department'] . '%';
}
if (!empty($filters['creator'])) {
$where[] = 'creator LIKE ?';
$params[] = '%' . $filters['creator'] . '%';
}
if (!empty($filters['sku_id'])) {
$where[] = 'sku_id LIKE ?';
$params[] = '%' . $filters['sku_id'] . '%';
}
if (!empty($filters['date_from'])) {
$where[] = 'finishTime >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'finishTime <= ?';
$params[] = $filters['date_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
CONVERT(varchar(10), finishTime, 120) AS finish_time,
department,
creator,
sku_id,
skuName,
CAST(num AS decimal(18,2)) AS num,
supplier,
purchase,
productCode,
CAST(shippingFee AS decimal(18,2)) AS shipping_fee,
CAST(costPrice AS decimal(18,2)) AS cost_price,
CAST(totalAmount AS decimal(18,2)) AS total_amount,
CAST(estimatedPrice AS decimal(18,2)) AS estimated_price,
CAST(estimatedPriceAll AS decimal(18,2)) AS estimated_price_all,
'' AS remark,
businessId,
CONVERT(varchar(19), updateAt, 120) AS update_time
FROM finance_sampleDeduction
$whereSql
ORDER BY finishTime DESC";
return $this->runQuery($sql, $params, $page, $pageSize);
}
public function sampleDeductionSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
ISNULL(SUM(totalAmount), 0) AS total_amount,
ISNULL(SUM(estimatedPriceAll), 0) AS total_estimate,
COUNT(*) AS total_count
FROM finance_sampleDeduction $whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalAmount' => (float) $row['total_amount'],
'totalEstimate' => (float) $row['total_estimate'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 筛选器选项
// ============================================================
public function filterOptionsForReturn(): array
{
$suppliers = $this->fetchDistinct('SELECT DISTINCT supplierName FROM finance_purchaseReturn WHERE supplierName IS NOT NULL ORDER BY supplierName');
$warehouses = $this->fetchDistinct('SELECT DISTINCT warehouseName FROM finance_purchaseReturn WHERE warehouseName IS NOT NULL ORDER BY warehouseName');
return compact('suppliers', 'warehouses');
}
public function filterOptionsForInvoice(): array
{
$suppliers = $this->fetchDistinct('SELECT DISTINCT supplier FROM finance_invoiceDeduction WHERE supplier IS NOT NULL ORDER BY supplier');
return ['suppliers' => $suppliers];
}
public function filterOptionsForCompensation(): array
{
$suppliers = $this->fetchDistinct("SELECT DISTINCT supplier FROM (
SELECT factory_subject_name AS supplier FROM fys_paper_factory_compensation_detail WHERE factory_subject_name IS NOT NULL
UNION
SELECT factory_subject_name AS supplier FROM fys_pressing_factory_compensation_detail WHERE factory_subject_name IS NOT NULL
) AS t ORDER BY supplier");
return ['suppliers' => $suppliers];
}
public function filterOptionsForAdvance(): array
{
$suppliers = $this->fetchDistinct('SELECT DISTINCT supplier FROM finance_advanceLoans WHERE supplier IS NOT NULL ORDER BY supplier');
return ['suppliers' => $suppliers];
}
public function filterOptionsForSample(): array
{
$suppliers = $this->fetchDistinct('SELECT DISTINCT supplier FROM finance_sampleDeduction WHERE supplier IS NOT NULL ORDER BY supplier');
$departments = $this->fetchDistinct('SELECT DISTINCT department FROM finance_sampleDeduction WHERE department IS NOT NULL ORDER BY department');
$creators = $this->fetchDistinct('SELECT DISTINCT creator FROM finance_sampleDeduction WHERE creator IS NOT NULL ORDER BY creator');
return compact('suppliers', 'departments', 'creators');
}
private function fetchDistinct(string $sql): array
{
$rows = $this->db->query($sql)->fetchAll(PDO::FETCH_COLUMN);
return array_values(array_filter($rows, fn ($v) => $v !== null && $v !== ''));
}
}

View File

@ -8,4 +8,37 @@ Route::api('ecommerce-finance', function () {
Route::apiResource('stores', 'Stores')->except(['destroy']);
Route::apiResource('import-batches', 'ImportBatches')->only(['index', 'show', 'store']);
Route::get('reconciliation-exceptions', 'ReconciliationExceptions@index')->name('reconciliation-exceptions.index');
// 货款对账
Route::get('goods-reconciliations', 'GoodsReconciliations@index')->name('goods-reconciliations.index');
Route::get('goods-reconciliations/summary', 'GoodsReconciliations@summary')->name('goods-reconciliations.summary');
Route::get('goods-reconciliations/options', 'GoodsReconciliations@options')->name('goods-reconciliations.options');
// 供应商应付 - 5 个子功能页面
Route::prefix('supplier-payables')->group(function () {
// 退货对账
Route::get('return-reconciliations', 'SupplierPayables@returnReconciliationsIndex');
Route::get('return-reconciliations/summary', 'SupplierPayables@returnReconciliationsSummary');
Route::get('return-reconciliations/options', 'SupplierPayables@returnReconciliationsOptions');
// 发票扣款
Route::get('invoice-deductions', 'SupplierPayables@invoiceDeductionsIndex');
Route::get('invoice-deductions/summary', 'SupplierPayables@invoiceDeductionsSummary');
Route::get('invoice-deductions/options', 'SupplierPayables@invoiceDeductionsOptions');
// 赔偿明细
Route::get('compensation-details', 'SupplierPayables@compensationDetailsIndex');
Route::get('compensation-details/summary', 'SupplierPayables@compensationDetailsSummary');
Route::get('compensation-details/options', 'SupplierPayables@compensationDetailsOptions');
// 借支明细
Route::get('advance-details', 'SupplierPayables@advanceDetailsIndex');
Route::get('advance-details/summary', 'SupplierPayables@advanceDetailsSummary');
Route::get('advance-details/options', 'SupplierPayables@advanceDetailsOptions');
// 样品扣款
Route::get('sample-deductions', 'SupplierPayables@sampleDeductionsIndex');
Route::get('sample-deductions/summary', 'SupplierPayables@sampleDeductionsSummary');
Route::get('sample-deductions/options', 'SupplierPayables@sampleDeductionsOptions');
});
});

View File

@ -0,0 +1,70 @@
<?php
namespace Modules\EcommerceFinance\Services;
use PDO;
use PDOException;
/**
* JianSun SQL Server 数据库连接管理
* 使用 PDO_ODBC 直连 SQL Server(无需 sqlsrv 扩展)
*/
class JianSunConnection
{
private static ?PDO $instance = null;
/**
* 获取 PDO 连接实例(单例模式)
*/
public static function get(): PDO
{
if (self::$instance === null) {
self::$instance = self::create();
}
return self::$instance;
}
/**
* 创建新的 PDO 连接
*/
public static function create(): PDO
{
$host = env('DB_JIANSUN_HOST', '218.93.191.21');
$port = env('DB_JIANSUN_PORT', '1433');
$db = env('DB_JIANSUN_DATABASE', 'JianSun');
$user = env('DB_JIANSUN_USERNAME', 'financeReader');
$pass = env('DB_JIANSUN_PASSWORD', '');
$dsn = sprintf(
'odbc:Driver={SQL Server};Server=%s,%s;Database=%s',
$host,
$port,
$db
);
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
return $pdo;
} catch (PDOException $e) {
throw new \RuntimeException(
'JianSun 数据库连接失败: ' . $e->getMessage(),
500,
$e
);
}
}
/**
* 重置连接(用于长时间运行或测试场景)
*/
public static function reset(): void
{
self::$instance = null;
}
}

71378
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,84 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#advance-detail-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/advance-details',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalAmount: 0, totalCount: 0 },
filters: { supplier: '', month_range: null }
};
},
mounted() { this.fetchData(); },
methods: {
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
if (this.filters.supplier) params.supplier = this.filters.supplier;
if (this.filters.month_range && this.filters.month_range.length === 2) {
params.month_from = this.filters.month_range[0];
params.month_to = this.filters.month_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', month_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,84 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#compensation-detail-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/compensation-details',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalAmount: 0, totalCount: 0 },
filters: { supplier: '', month_range: null }
};
},
mounted() { this.fetchData(); },
methods: {
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
if (this.filters.supplier) params.supplier = this.filters.supplier;
if (this.filters.month_range && this.filters.month_range.length === 2) {
params.month_from = this.filters.month_range[0];
params.month_to = this.filters.month_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', month_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,277 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table,
TableColumn,
Pagination,
DatePicker,
Tag,
Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#goods-reconciliation-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/goods-reconciliations',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
activeTab: 'all',
summary: {
totalAmount: 0,
totalGuessAmount: 0,
totalCount: 0,
sentCount: 0,
unsentCount: 0
},
options: {
suppliers: [],
warehouses: []
},
filters: {
record_type: 'all',
supplier: '',
warehouse: '',
stock_no: '',
purchase_no: '',
product_code: '',
express_no: '',
send_time_range: null,
create_time_range: null,
sent_status: 'all',
has_diff: false
},
tabs: [
{ key: 'all', label: '全部记录' },
{ key: 'purchase', label: '入库对账' },
{ key: 'sale', label: '代发对账' },
{ key: 'unsent', label: '未发出' },
{ key: 'diff', label: '有差异' }
],
sortProp: 'createTime',
sortOrder: 'descending'
};
},
mounted() {
this.fetchOptions();
this.fetchData();
},
methods: {
/**
* Fetch filter options (suppliers, warehouses)
*/
fetchOptions() {
axios.get(this.baseUrl + '/options')
.then(response => {
this.options = response.data;
})
.catch(() => {
this.$message.error('获取筛选选项失败');
});
},
/**
* Fetch data list and summary
*/
fetchData() {
this.loading = true;
const params = this.buildParams();
const summaryParams = this.buildSummaryParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params: summaryParams })
])
.then(([listRes, summaryRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = summaryRes.data;
})
.catch(() => {
this.$message.error('获取数据失败,请稍后重试');
})
.finally(() => {
this.loading = false;
});
},
/**
* Build list query params
*/
buildParams() {
const params = {
page: this.currentPage,
limit: this.pageSize,
record_type: this.filters.record_type
};
// text filters
const textFields = ['supplier', 'warehouse', 'stock_no', 'purchase_no', 'product_code', 'express_no'];
textFields.forEach(f => {
if (this.filters[f]) {
params[f] = this.filters[f];
}
});
// date range filters
if (this.filters.send_time_range && this.filters.send_time_range.length === 2) {
params.send_time_from = this.filters.send_time_range[0];
params.send_time_to = this.filters.send_time_range[1];
}
if (this.filters.create_time_range && this.filters.create_time_range.length === 2) {
params.create_time_from = this.filters.create_time_range[0];
params.create_time_to = this.filters.create_time_range[1];
}
// sent status
if (this.filters.sent_status && this.filters.sent_status !== 'all') {
params.sent_status = this.filters.sent_status;
}
// has diff
if (this.filters.has_diff) {
params.has_diff = 1;
}
return params;
},
/**
* Summary params always use 'all' for overall counts
*/
buildSummaryParams() {
const params = {};
const textFields = ['supplier', 'warehouse', 'stock_no', 'purchase_no', 'product_code', 'express_no'];
textFields.forEach(f => {
if (this.filters[f]) {
params[f] = this.filters[f];
}
});
if (this.filters.send_time_range && this.filters.send_time_range.length === 2) {
params.send_time_from = this.filters.send_time_range[0];
params.send_time_to = this.filters.send_time_range[1];
}
if (this.filters.create_time_range && this.filters.create_time_range.length === 2) {
params.create_time_from = this.filters.create_time_range[0];
params.create_time_to = this.filters.create_time_range[1];
}
params.record_type = this.filters.record_type;
if (this.filters.sent_status && this.filters.sent_status !== 'all') {
params.sent_status = this.filters.sent_status;
}
return params;
},
/**
* Switch tab
*/
switchTab(key) {
this.activeTab = key;
this.currentPage = 1;
switch (key) {
case 'all':
this.filters.record_type = 'all';
this.filters.sent_status = 'all';
this.filters.has_diff = false;
break;
case 'purchase':
this.filters.record_type = 'purchase';
this.filters.sent_status = 'all';
this.filters.has_diff = false;
break;
case 'sale':
this.filters.record_type = 'sale';
this.filters.sent_status = 'all';
this.filters.has_diff = false;
break;
case 'unsent':
this.filters.sent_status = 'unsent';
this.filters.has_diff = false;
break;
case 'diff':
this.filters.has_diff = true;
this.filters.sent_status = 'all';
break;
}
this.fetchData();
},
/**
* On filter change, reset to page 1
*/
onFilterChange() {
this.currentPage = 1;
this.fetchData();
},
/**
* Pagination page change
*/
onPageChange(page) {
this.currentPage = page;
this.fetchData();
},
/**
* Pagination page size change
*/
onPageSizeChange(size) {
this.pageSize = size;
this.currentPage = 1;
this.fetchData();
},
/**
* Sort change
*/
onSortChange({ prop, order }) {
this.sortProp = prop;
this.sortOrder = order;
// Sorting is handled server-side for now; re-fetch
this.fetchData();
},
/**
* Format money (CNY)
*/
formatMoney(value) {
if (value == null || isNaN(value)) return '0.00';
return '¥' + Number(value).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
}
});
export default app;

View File

@ -0,0 +1,84 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#invoice-deduction-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/invoice-deductions',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalPrice: 0, totalCount: 0 },
filters: { supplier: '', month_range: null }
};
},
mounted() { this.fetchData(); },
methods: {
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
if (this.filters.supplier) params.supplier = this.filters.supplier;
if (this.filters.month_range && this.filters.month_range.length === 2) {
params.month_from = this.filters.month_range[0];
params.month_to = this.filters.month_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', month_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,113 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table,
TableColumn,
Pagination,
DatePicker,
Tag,
Button,
Input,
Select,
Option,
Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
Vue.component(Option.name, Option);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#return-reconciliation-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/return-reconciliations',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalAmount: 0, totalGuessAmount: 0, totalCount: 0 },
options: { suppliers: [], warehouses: [] },
filters: {
supplier: '',
warehouse: '',
sku_id: '',
express_no: '',
date_range: null
}
};
},
mounted() {
this.fetchOptions();
this.fetchData();
},
methods: {
fetchOptions() {
axios.get(this.baseUrl + '/options')
.then(r => { this.options = r.data; })
.catch(() => this.$message.error('获取筛选选项失败'));
},
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
['supplier', 'warehouse', 'sku_id', 'express_no'].forEach(f => {
if (this.filters[f]) params[f] = this.filters[f];
});
if (this.filters.date_range && this.filters.date_range.length === 2) {
params.date_from = this.filters.date_range[0];
params.date_to = this.filters.date_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', warehouse: '', sku_id: '', express_no: '', date_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,92 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#sample-deduction-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/sample-deductions',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalAmount: 0, totalEstimate: 0, totalCount: 0 },
filters: {
supplier: '',
department: '',
creator: '',
sku_id: '',
date_range: null
}
};
},
mounted() { this.fetchData(); },
methods: {
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
['supplier', 'department', 'creator', 'sku_id'].forEach(f => {
if (this.filters[f]) params[f] = this.filters[f];
});
if (this.filters.date_range && this.filters.date_range.length === 2) {
params.date_from = this.filters.date_range[0];
params.date_to = this.filters.date_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', department: '', creator: '', sku_id: '', date_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -69,6 +69,8 @@
margin-right: 2.5rem;
}
}
[v-cloak] { display: none; }
</style>
<div class="my-6">
@ -96,7 +98,21 @@
</nav>
@endif
@php
$viewName = 'business.pages.' . $section['slug'];
if ($page) {
$viewName .= '.' . $page['slug'];
}
if ($page && $tabs->isNotEmpty()) {
$viewName .= '.' . ($activeTab['slug'] ?? '');
}
@endphp
@if (view()->exists($viewName))
@include($viewName)
@else
<section class="business-blank-page" aria-label="{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}"></section>
@endif
</div>
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,111 @@
<div id="advance-detail-app" class="advance-detail-page">
<div v-cloak>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4 text-sm text-yellow-800">
<i class="el-icon-warning mr-1"></i>
借支明细数据源 finance_advanceLoans 当前仅 4 行数据;表头 21 列中部分字段(如实际打款金额、分期扣款、票据等)在数据库中无对应列,已显示为占位符。
</div>
{{-- 统计卡片 --}}
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">借支总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="供应商" size="small" style="width: 200px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.month_range" type="monthrange"
start-placeholder="月份开始" end-placeholder="月份结束" size="small"
style="width: 240px" value-format="yyyy-MM" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="supplier" label="供应商" width="140" fixed="left" show-overflow-tooltip></el-table-column>
<el-table-column label="协议签订名称" width="140">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.contract_name || '-' }}</span></template>
</el-table-column>
<el-table-column prop="advance_amount" label="借支金额-借款协议" width="140" align="right">
<template slot-scope="scope">
<span :class="scope.row.advance_amount < 0 ? 'text-red-600' : 'text-gray-800'">@{{ formatMoney(scope.row.advance_amount) }}</span>
</template>
</el-table-column>
<el-table-column label="实际打款金额" width="120" align="right">
<template slot-scope="scope"><span class="text-gray-400">@{{ formatMoney(scope.row.actual_pay) }}</span></template>
</el-table-column>
<el-table-column label="本月扣款金额" width="120" align="right">
<template slot-scope="scope"><span class="text-gray-400">@{{ formatMoney(scope.row.month_deduct) }}</span></template>
</el-table-column>
<el-table-column label="应扣金额" width="120" align="right">
<template slot-scope="scope"><span class="text-gray-400">@{{ formatMoney(scope.row.should_deduct) }}</span></template>
</el-table-column>
<el-table-column label="分期起始扣款月份" width="130">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.deduct_start_month || '-' }}</span></template>
</el-table-column>
<el-table-column label="分期最后次扣款月份" width="130">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.deduct_end_month || '-' }}</span></template>
</el-table-column>
<el-table-column prop="remain_periods" label="分期剩余期数" width="110" align="center"></el-table-column>
<el-table-column prop="month_str" label="借支日期" width="110"></el-table-column>
<el-table-column label="借款期限" width="100">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.loan_term || '-' }}</span></template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="180" show-overflow-tooltip></el-table-column>
<el-table-column label="是否打款" width="100">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.is_paid || '-' }}</span></template>
</el-table-column>
<el-table-column prop="pay_month" label="打款月份" width="110"></el-table-column>
<el-table-column label="流水号" width="120">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.serial_no || '-' }}</span></template>
</el-table-column>
<el-table-column label="借支协议" width="120">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.loan_agreement || '-' }}</span></template>
</el-table-column>
<el-table-column prop="person_charge" label="负责人" width="100"></el-table-column>
<el-table-column label="票据" width="80">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.receipt_0 || '-' }}</span></template>
</el-table-column>
<el-table-column label="票据1" width="80">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.receipt_1 || '-' }}</span></template>
</el-table-column>
<el-table-column label="票据2" width="80">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.receipt_2 || '-' }}</span></template>
</el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/advance-details.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,73 @@
<div id="compensation-detail-app" class="compensation-detail-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">赔付总金额</div>
<div class="text-2xl font-bold text-red-600">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="供应商" size="small" style="width: 220px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.month_range" type="monthrange"
start-placeholder="月份开始" end-placeholder="月份结束" size="small"
style="width: 240px" value-format="yyyy-MM" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="month" label="日期" width="100" fixed="left"></el-table-column>
<el-table-column prop="supplier" label="供应商" width="260" show-overflow-tooltip></el-table-column>
<el-table-column prop="amount" label="赔付金额" width="140" align="right">
<template slot-scope="scope">
<span :class="scope.row.amount < 0 ? 'text-red-600' : 'text-gray-800'">@{{ formatMoney(scope.row.amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="reason" label="赔付原因" width="240" show-overflow-tooltip></el-table-column>
<el-table-column prop="remark" label="备注" width="180" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.remark || '-' }}</span></template>
</el-table-column>
<el-table-column prop="factory_type" label="类型" width="100">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.factory_type === '纸板厂' ? 'primary' : 'success'">@{{ scope.row.factory_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator" label="创建人" width="100"></el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/compensation-details.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,79 @@
<div id="invoice-deduction-app" class="invoice-deduction-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">扣款总金额</div>
<div class="text-2xl font-bold text-red-600">@{{ formatMoney(summary.totalPrice) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="供应商/打款抬头/收款方" size="small" style="width: 220px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.month_range" type="monthrange"
start-placeholder="月份开始" end-placeholder="月份结束" size="small"
style="width: 240px" value-format="yyyy-MM" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="month_str" label="日期" width="100" fixed="left"></el-table-column>
<el-table-column label="类别" width="100">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.category || '-' }}</span></template>
</el-table-column>
<el-table-column prop="payee" label="打款抬头" width="160" show-overflow-tooltip></el-table-column>
<el-table-column prop="receiver" label="收款方" width="160" show-overflow-tooltip></el-table-column>
<el-table-column label="供板材厂家/代扣厂家" width="180">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.board_factory || '-' }}</span></template>
</el-table-column>
<el-table-column label="补贴费用" width="120" align="right">
<template slot-scope="scope"><span class="text-gray-400">@{{ formatMoney(scope.row.subsidy_fee) }}</span></template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="price" label="对私打款金额" width="140" align="right">
<template slot-scope="scope">
<span :class="scope.row.price < 0 ? 'text-red-600' : 'text-gray-800'">@{{ formatMoney(scope.row.price) }}</span>
</template>
</el-table-column>
<el-table-column prop="detail" label="明细" width="200" show-overflow-tooltip></el-table-column>
<el-table-column label="注明" width="100">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.note || '-' }}</span></template>
</el-table-column>
<el-table-column prop="person_charge" label="负责人" width="100" show-overflow-tooltip></el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/invoice-deductions.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,139 @@
<div id="goods-reconciliation-app" class="goods-reconciliation-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">对账总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">预估总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalGuessAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">已发出</div>
<div class="text-2xl font-bold text-green-600">@{{ summary.sentCount.toLocaleString() }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">未发出</div>
<div class="text-2xl font-bold text-orange-500">@{{ summary.unsentCount.toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3 mb-3">
<el-select v-model="filters.record_type" placeholder="记录类型" size="small" style="width: 140px" @change="onFilterChange">
<el-option label="全部记录" value="all"></el-option>
<el-option label="入库对账" value="purchase"></el-option>
<el-option label="代发对账" value="sale"></el-option>
</el-select>
<el-select v-model="filters.supplier" placeholder="供应商" size="small" style="width: 160px" filterable clearable @change="onFilterChange">
<el-option v-for="s in options.suppliers" :key="s" :label="s" :value="s"></el-option>
</el-select>
<el-select v-model="filters.warehouse" placeholder="仓库" size="small" style="width: 140px" filterable clearable @change="onFilterChange">
<el-option v-for="w in options.warehouses" :key="w" :label="w" :value="w"></el-option>
</el-select>
<el-input v-model="filters.stock_no" placeholder="入库单号" size="small" style="width: 140px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.purchase_no" placeholder="采购单号" size="small" style="width: 140px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
</div>
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.product_code" placeholder="商品编码" size="small" style="width: 140px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.express_no" placeholder="快递单号" size="small" style="width: 140px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.send_time_range" type="daterange"
start-placeholder="发货开始" end-placeholder="发货结束" size="small"
style="width: 260px" value-format="yyyy-MM-dd" @change="onFilterChange">
</el-date-picker>
<el-date-picker v-model="filters.create_time_range" type="daterange"
start-placeholder="创建开始" end-placeholder="创建结束" size="small"
style="width: 260px" value-format="yyyy-MM-dd" @change="onFilterChange">
</el-date-picker>
<el-select v-model="filters.sent_status" placeholder="发出状态" size="small" style="width: 120px" clearable @change="onFilterChange">
<el-option label="全部" value="all"></el-option>
<el-option label="已发出" value="sent"></el-option>
<el-option label="未发出" value="unsent"></el-option>
</el-select>
</div>
</div>
{{-- Tab 切换 --}}
<div class="flex gap-1 mb-4">
<button v-for="tab in tabs" :key="tab.key"
@click="switchTab(tab.key)"
:class="['px-4 py-2 rounded-lg text-sm font-medium transition-colors',
activeTab === tab.key ? 'bg-indigo-600 text-white shadow-sm' : 'bg-white text-gray-600 border border-gray-200 hover:bg-gray-50']">
@{{ tab.label }}
</button>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small"
:default-sort="{prop: 'createTime', order: 'descending'}"
@sort-change="onSortChange">
<el-table-column type="index" label="#" width="55" fixed="left"></el-table-column>
<el-table-column prop="recordType" label="类型" width="80">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.recordType === 'purchase' ? 'primary' : 'success'">
@{{ scope.row.recordType === 'purchase' ? '入库' : '代发' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="stockNo" label="入库单号" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="purchaseNo" label="采购单号" width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="sendTime" label="发货时间" width="145" sortable="custom"></el-table-column>
<el-table-column prop="supplier" label="供应商" width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="skuCode" label="货号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="productCode" label="商品编码" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="skuName" label="规格名称" width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="qty" label="数量" width="80" align="right"></el-table-column>
<el-table-column label="单价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.unitPrice) }}</template>
</el-table-column>
<el-table-column label="金额" width="120" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.amount) }}</template>
</el-table-column>
<el-table-column label="预估单价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.guessUnitPrice) }}</template>
</el-table-column>
<el-table-column label="预估金额" width="120" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.guessAmount) }}</template>
</el-table-column>
<el-table-column prop="supplierGoodsCode" label="供应商商品编码" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="warehouse" label="仓库名称" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="145" sortable="custom"></el-table-column>
<el-table-column prop="expressName" label="快递名称" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="expressNo" label="快递单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column label="未发出" width="90" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.isUnsent" size="mini" type="warning">未发出</el-tag>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
</el-table>
{{-- 分页 --}}
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/goods-reconciliations.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,107 @@
<div id="return-reconciliation-app" class="return-reconciliation-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">对账总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">预估总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalGuessAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="明细供应商" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-select v-model="filters.warehouse" placeholder="仓库" size="small" style="width: 140px" filterable clearable @change="onFilterChange">
<el-option v-for="w in options.warehouses" :key="w" :label="w" :value="w"></el-option>
</el-select>
<el-input v-model="filters.sku_id" placeholder="条形码" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.express_no" placeholder="快递单号" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.date_range" type="daterange"
start-placeholder="发货开始" end-placeholder="发货结束" size="small"
style="width: 260px" value-format="yyyy-MM-dd" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="send_time" label="发货时间" width="110" fixed="left" show-overflow-tooltip></el-table-column>
<el-table-column prop="supplier" label="明细供应商" width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="barcode" label="条形码" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="sku_name" label="商品名称" width="220" show-overflow-tooltip></el-table-column>
<el-table-column prop="qty" label="发货量" width="80" align="right"></el-table-column>
<el-table-column label="单价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.unit_price) }}</template>
</el-table-column>
<el-table-column label="金额" width="110" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.amount) }}</template>
</el-table-column>
<el-table-column label="预估单价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.guess_unit_price) }}</template>
</el-table-column>
<el-table-column label="预估金额" width="110" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.guess_amount) }}</template>
</el-table-column>
<el-table-column prop="supplier_barcode" label="供应商条码" width="120" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.supplier_barcode || '-' }}</span></template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="title" label="标题" width="100" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.title || '-' }}</span></template>
</el-table-column>
<el-table-column prop="wms_pick_no" label="wms配货单号" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="warehouse" label="仓库名称" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="express_no" label="快递单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="express_name" label="快递名称" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="io_date" label="发货时间" width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="original_barcode" label="原条码" width="130" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.original_barcode || '-' }}</span></template>
</el-table-column>
<el-table-column prop="labels" label="标记多标签" width="120" show-overflow-tooltip>
<template slot-scope="scope">
<el-tag v-if="scope.row.labels" size="mini" type="warning">@{{ scope.row.labels }}</el-tag>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column prop="creator" label="创建人" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="reconciler" label="对账人" width="100" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.reconciler || '-' }}</span></template>
</el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/return-reconciliations.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,93 @@
<div id="sample-deduction-app" class="sample-deduction-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">合计扣款金额</div>
<div class="text-2xl font-bold text-red-600">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">预估总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalEstimate) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="厂家" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.department" placeholder="部门" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.creator" placeholder="申请人" size="small" style="width: 120px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.sku_id" placeholder="商品代码" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.date_range" type="daterange"
start-placeholder="开始" end-placeholder="结束" size="small"
style="width: 240px" value-format="yyyy-MM-dd" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="finish_time" label="日期" width="110" fixed="left"></el-table-column>
<el-table-column prop="department" label="部门" width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="creator" label="申请人" width="100"></el-table-column>
<el-table-column prop="sku_id" label="商品代码" width="160" show-overflow-tooltip></el-table-column>
<el-table-column prop="skuName" label="商品名称" width="220" show-overflow-tooltip></el-table-column>
<el-table-column prop="num" label="数量" width="70" align="right"></el-table-column>
<el-table-column prop="supplier" label="厂家" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="purchase" label="对应采购" width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="productCode" label="扣款厂家商品代码" width="160" show-overflow-tooltip></el-table-column>
<el-table-column prop="shipping_fee" label="运费" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.shipping_fee) }}</template>
</el-table-column>
<el-table-column prop="cost_price" label="成本价" width="110" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.cost_price) }}</template>
</el-table-column>
<el-table-column prop="total_amount" label="合计扣款金额" width="130" align="right">
<template slot-scope="scope">
<span :class="scope.row.total_amount < 0 ? 'text-red-600' : 'text-gray-800'">@{{ formatMoney(scope.row.total_amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="estimated_price" label="预估价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.estimated_price) }}</template>
</el-table-column>
<el-table-column prop="estimated_price_all" label="预估金额" width="110" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.estimated_price_all) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.remark || '-' }}</span></template>
</el-table-column>
<el-table-column prop="businessId" label="业务单号" width="200" show-overflow-tooltip></el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/sample-deductions.min.js') }}"></script>
@endpush

8
webpack.mix.js vendored
View File

@ -61,6 +61,14 @@ mix
// Modules
.js('resources/assets/js/views/modules/apps.js', 'public/js/modules/apps.min.js')
// Ecommerce Finance
.js('resources/assets/js/views/ecommerce-finance/goods-reconciliations.js', 'public/js/ecommerce-finance/goods-reconciliations.min.js')
.js('resources/assets/js/views/ecommerce-finance/return-reconciliations.js', 'public/js/ecommerce-finance/return-reconciliations.min.js')
.js('resources/assets/js/views/ecommerce-finance/invoice-deductions.js', 'public/js/ecommerce-finance/invoice-deductions.min.js')
.js('resources/assets/js/views/ecommerce-finance/compensation-details.js', 'public/js/ecommerce-finance/compensation-details.min.js')
.js('resources/assets/js/views/ecommerce-finance/advance-details.js', 'public/js/ecommerce-finance/advance-details.min.js')
.js('resources/assets/js/views/ecommerce-finance/sample-deductions.js', 'public/js/ecommerce-finance/sample-deductions.min.js')
// Portal
.js('resources/assets/js/views/portal/apps.js', 'public/js/portal/apps.min.js')