售后金额上浮
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -623,8 +623,9 @@ class PurchaseOrderController extends BaseController
|
||||
->get(['id', 'store_id', 'total_amount']);
|
||||
|
||||
$stores = StoreModel::withTrashed()
|
||||
->with('level:id,name,percent')
|
||||
->whereIn('id', $orders->pluck('store_id')->unique())
|
||||
->get(['id', 'name'])
|
||||
->get(['id', 'name', 'level_id'])
|
||||
->keyBy('id');
|
||||
|
||||
$bills = BillModel::query()
|
||||
@@ -642,9 +643,13 @@ class PurchaseOrderController extends BaseController
|
||||
'0'
|
||||
);
|
||||
$bill = $bills->get((int) $storeId);
|
||||
$store = $stores->get((int) $storeId);
|
||||
$rows[] = [
|
||||
'store_id' => (int) $storeId,
|
||||
'store_name' => $stores->get((int) $storeId)->name ?? ('门店#' . $storeId),
|
||||
'store_name' => $store->name ?? ('门店#' . $storeId),
|
||||
// 客户等级(售后金额上浮折算预览用;无等级=不上浮)
|
||||
'level_name' => $store?->level?->name,
|
||||
'level_percent' => (string) ($store?->level?->percent ?? '0'),
|
||||
'order_count' => $storeOrders->count(),
|
||||
'product_amount' => $productAmount,
|
||||
'box_price' => $boxPrice,
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Services;
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Models\BillModel;
|
||||
use App\Models\ContainerReturnModel;
|
||||
use App\Models\CustomerLevelModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
@@ -20,7 +21,8 @@ use Throwable;
|
||||
* 2. 商品金额 = 门店订单商品金额汇总(快照,生成后不可修改)
|
||||
* 3. 附加金额 = 周转筐数量×筐单价 + 托盘数量×托盘单价(单价取站点配置快照;
|
||||
* 数量正数=压筐附加金额,负数=回筐抵扣金额)
|
||||
* 4. 售后金额 = 按门店填写的调整金额(可正负:正数=加收,负数=售后减免)
|
||||
* 4. 售后金额 = 按门店填写的调整金额 × 门店客户等级上浮比例((100+percent)/100,与售价同口径;
|
||||
* 可正负:正数=加收,负数=售后减免)
|
||||
* 5. 总金额 = 商品金额 + 配送费 + 附加金额 + 售后金额;回写门店订单 bill_id 完成关联,订单状态置为已完成
|
||||
* 6. 压回筐记录:筐/托盘数量非 0 时写入完整快照(数量/单价/金额均可为负)
|
||||
*/
|
||||
@@ -82,6 +84,14 @@ readonly class BillGenerateService
|
||||
$boxPrice = (string) site_config('services.box_amount', 0);
|
||||
$trayPrice = (string) site_config('services.tray_amount', 0);
|
||||
$billDate = now()->toDateString();
|
||||
// 各门店客户等级上浮比例(售后金额折算用;无等级按 0 不上浮)
|
||||
$levelPercents = StoreModel::withTrashed()
|
||||
->with('level:id,percent')
|
||||
->whereIn('id', $ordersByStore->keys())
|
||||
->get(['id', 'level_id'])
|
||||
->mapWithKeys(static fn (StoreModel $store) => [
|
||||
$store->id => (string) ($store->level?->percent ?? '0'),
|
||||
]);
|
||||
$bills = [];
|
||||
foreach ($ordersByStore as $storeId => $storeOrders) {
|
||||
$row = $submitted[(int) $storeId];
|
||||
@@ -96,7 +106,11 @@ readonly class BillGenerateService
|
||||
bcmul((string) (int) $row['tray_num'], $trayPrice, 2),
|
||||
2
|
||||
);
|
||||
$afterSale = bcadd((string) ($row['after_sale'] ?? '0'), '0', 2);
|
||||
// 售后金额按客户等级上浮比例折算(输入 15、上浮 1% → 实收 15.15;负数同比例放大)
|
||||
$afterSale = CustomerLevelModel::calcLevelPrice(
|
||||
(string) ($row['after_sale'] ?? '0'),
|
||||
$levelPercents[(int) $storeId] ?? '0',
|
||||
);
|
||||
$totalAmount = bcadd(bcadd(bcadd($productAmount, $deliveryFee, 2), $addedAmount, 2), $afterSale, 2);
|
||||
|
||||
$bill = BillModel::create([
|
||||
|
||||
@@ -133,6 +133,56 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
$this->assertSame('5.00', (string) $bill2->total_amount, '未传售后金额不影响总金额');
|
||||
}
|
||||
|
||||
/** 售后金额按客户等级上浮折算:输入 15、上浮 1% → 实收 15.15;billPrepare 回显等级(负数同比例放大) */
|
||||
public function test_generate_bill_after_sale_marked_up_by_level(): void
|
||||
{
|
||||
$level = CustomerLevelModel::factory()->create(['percent' => 1]);
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$level2 = CustomerLevelModel::factory()->create(['percent' => 2]);
|
||||
$store2 = StoreModel::factory()->create(['level_id' => $level2->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
|
||||
// 两店各下一单:店1 等级价 5.05 × 4 = 20.20;店2 等级价 5.10 × 2 = 10.20
|
||||
foreach ([[$store, 4], [$store2, 2]] as [$s, $qty]) {
|
||||
$this->actingAsMiniStore($s);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||
->assertJsonPath('success', true);
|
||||
}
|
||||
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||
->assertJsonPath('success', true);
|
||||
$purchase = PurchaseOrderModel::first();
|
||||
$this->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true);
|
||||
|
||||
// billPrepare 回显客户等级与上浮比例(弹窗预览用)
|
||||
$prepare = $this->getJson("/purchase/order/{$purchase->id}/bill/prepare")
|
||||
->assertJsonPath('success', true)
|
||||
->json('data.stores');
|
||||
$prepareByStore = collect($prepare)->keyBy('store_id');
|
||||
$this->assertSame($level->name, $prepareByStore[$store->id]['level_name']);
|
||||
$this->assertSame('1.00', (string) $prepareByStore[$store->id]['level_percent']);
|
||||
$this->assertSame($level2->name, $prepareByStore[$store2->id]['level_name']);
|
||||
$this->assertSame('2.00', (string) $prepareByStore[$store2->id]['level_percent']);
|
||||
|
||||
// 店1 售后 15 × 101% = 15.15;店2 售后 -10 × 102% = -10.20
|
||||
$this->postJson("/purchase/order/{$purchase->id}/bill", [
|
||||
'stores' => [
|
||||
['store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0, 'after_sale' => 15],
|
||||
['store_id' => $store2->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0, 'after_sale' => -10],
|
||||
],
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
$bill = BillModel::where('store_id', $store->id)->first();
|
||||
$this->assertSame('15.15', (string) $bill->after_sale, '15 × 101%');
|
||||
$this->assertSame('35.35', (string) $bill->total_amount, '商品 20.20 + 售后 15.15');
|
||||
|
||||
$bill2 = BillModel::where('store_id', $store2->id)->first();
|
||||
$this->assertSame('-10.20', (string) $bill2->after_sale, '-10 × 102%(负数同比例放大)');
|
||||
$this->assertSame('0.00', (string) $bill2->total_amount, '商品 10.20 + 售后 -10.20');
|
||||
}
|
||||
|
||||
/** 生成账单:售后金额为正数时加收计入总金额 */
|
||||
public function test_generate_bill_positive_after_sale_added_to_total(): void
|
||||
{
|
||||
|
||||
@@ -147,6 +147,10 @@ export const BILL_STATUS_MAP: Record<number, { text: string; color: string }> =
|
||||
export interface IBillPrepareStore {
|
||||
store_id: number;
|
||||
store_name: string;
|
||||
/** 客户等级名(无等级为 null) */
|
||||
level_name?: string | null;
|
||||
/** 客户等级上浮比例(售后金额折算用;无等级为 '0') */
|
||||
level_percent?: string;
|
||||
order_count: number;
|
||||
/** 商品金额(订单汇总,不可修改) */
|
||||
product_amount: string;
|
||||
|
||||
@@ -86,6 +86,10 @@ const calcUnitRefPrice = (total: number, spec: string): number => {
|
||||
return Number.isFinite(pack) && pack > 0 ? total / pack : total;
|
||||
};
|
||||
|
||||
/** 售后金额按客户等级上浮折算:实收 = 输入 × (100+percent)/100(两位小数截断,与后端 calcLevelPrice 同口径) */
|
||||
const calcLevelAmount = (amount: number, percent: number): number =>
|
||||
Math.trunc(amount * (100 + percent)) / 100;
|
||||
|
||||
/**
|
||||
* 采购单管理(C1 在门店订单页合并生成 / C2-C3 导出 / C4 明细矩阵修改)
|
||||
* 采购单无独立明细表,明细直接溯源门店订货明细:商品行 × 门店列
|
||||
@@ -209,17 +213,20 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
const [billSaving, setBillSaving] = useState(false);
|
||||
const [billForm] = Form.useForm<{ stores: BillGenerateStoreParams[] }>();
|
||||
|
||||
// 弹窗内实时预览:附加金额 = 筐×筐单价 + 托盘×托盘单价;总额 = 商品 + 配送费 + 附加 + 售后金额(可正负)
|
||||
// 弹窗内实时预览:附加金额 = 筐×筐单价 + 托盘×托盘单价;售后金额 = 输入 × 客户等级上浮比例;总额 = 商品 + 配送费 + 附加 + 售后(可正负)
|
||||
const watchBillStores = Form.useWatch('stores', billForm) ?? [];
|
||||
const billPreview = (billPrepare?.stores ?? []).map((row, index) => {
|
||||
const input = watchBillStores[index] ?? {};
|
||||
const deliveryFee = Number(input.delivery_fee ?? 0);
|
||||
const afterSale = Number(input.after_sale ?? 0);
|
||||
const levelPercent = Number(row.level_percent ?? 0);
|
||||
const afterSaleInput = Number(input.after_sale ?? 0);
|
||||
const afterSale = calcLevelAmount(afterSaleInput, levelPercent);
|
||||
const added =
|
||||
Number(input.box_num ?? 0) * Number(row.box_price) +
|
||||
Number(input.tray_num ?? 0) * Number(row.tray_price);
|
||||
return {
|
||||
added,
|
||||
afterSale,
|
||||
total: Number(row.product_amount) + deliveryFee + added + afterSale,
|
||||
};
|
||||
});
|
||||
@@ -1460,7 +1467,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
{billPrepare && (
|
||||
<>
|
||||
<div className="py-2 text-gray-500">
|
||||
每个门店单独生成一张账单;商品金额由订单汇总不可修改,请填写各门店的配送费、周转筐/托盘数量(正数=压筐附加金额,负数=回筐抵扣金额)、售后金额(可正负,计入总金额:正数=加收,负数=售后减免)与备注(可选,随账单存档)。
|
||||
每个门店单独生成一张账单;商品金额由订单汇总不可修改,请填写各门店的配送费、周转筐/托盘数量(正数=压筐附加金额,负数=回筐抵扣金额)、售后金额(可正负,按客户等级上浮比例折算后计入总金额:如输入 15、上浮 1% 则实收 15.15)与备注(可选,随账单存档)。
|
||||
{billAllGenerated ? '该采购单已全部生成账单,仅可查看。' : '生成后采购单中的全部订单将关联到对应门店账单。'}
|
||||
</div>
|
||||
<Form form={billForm} layout="vertical" onFinish={handleBillSave}>
|
||||
@@ -1471,9 +1478,18 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
<div className="w-40 shrink-0">门店</div>
|
||||
<div className="w-28 shrink-0 text-center">商品金额</div>
|
||||
<div className="w-32 shrink-0 text-center">配送费(元)</div>
|
||||
<div className="w-32 shrink-0 text-center">周转筐(¥{billPrepare.stores[0]?.box_price ?? '0.00'}/个)</div>
|
||||
<div className="w-32 shrink-0 text-center">周转托盘(¥{billPrepare.stores[0]?.tray_price ?? '0.00'}/个)</div>
|
||||
<div className="w-36 shrink-0 text-center">售后金额(元)</div>
|
||||
<div className="w-32 shrink-0 text-center">
|
||||
<div>周转筐</div>
|
||||
<div>(¥{billPrepare.stores[0]?.box_price ?? '0.00'}/个)</div>
|
||||
</div>
|
||||
<div className="w-32 shrink-0 text-center">
|
||||
<div>周转托盘</div>
|
||||
<div>(¥{billPrepare.stores[0]?.tray_price ?? '0.00'}/个)</div>
|
||||
</div>
|
||||
<div className="w-50 shrink-0 text-center">
|
||||
<div>售后金额</div>
|
||||
<div>(元,按等级上浮)</div>
|
||||
</div>
|
||||
<div className="w-36 shrink-0 text-center">备注</div>
|
||||
<div className="w-28 shrink-0 text-center">附加金额</div>
|
||||
<div className="flex-1 text-center">账单总金额</div>
|
||||
@@ -1489,6 +1505,11 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
{row?.order_count ?? 0} 笔订单
|
||||
{billed && <Tag className="ml-1!" color="success">已生成</Tag>}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{row?.level_name
|
||||
? `${row.level_name} +${Number(row.level_percent ?? 0)}%`
|
||||
: '无等级(不上浮)'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-28 shrink-0 text-center">
|
||||
<Text strong>¥{row?.product_amount ?? '0.00'}</Text>
|
||||
@@ -1497,7 +1518,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="m-0! w-32 shrink-0 px-1!"
|
||||
className="m-0! w-32 shrink-0 px-1! flex justify-center"
|
||||
name={[field.name, 'delivery_fee']}
|
||||
rules={[{ required: true, message: '请输入配送费' }]}
|
||||
>
|
||||
@@ -1510,7 +1531,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="m-0! w-32 shrink-0 px-1!"
|
||||
className="m-0! w-32 shrink-0 px-1! flex justify-center"
|
||||
name={[field.name, 'box_num']}
|
||||
rules={[{ required: true, message: '请输入周转筐数量' }]}
|
||||
>
|
||||
@@ -1522,7 +1543,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="m-0! w-32 shrink-0 px-1!"
|
||||
className="m-0! w-32 shrink-0 px-1! flex justify-center"
|
||||
name={[field.name, 'tray_num']}
|
||||
rules={[{ required: true, message: '请输入周转托盘数量' }]}
|
||||
>
|
||||
@@ -1533,17 +1554,28 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
placeholder="正压负回"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="m-0! w-36 shrink-0 px-1!"
|
||||
name={[field.name, 'after_sale']}
|
||||
>
|
||||
<div className="w-50 shrink-0 px-1! flex justify-center">
|
||||
<Space.Compact block>
|
||||
<Space.Addon>
|
||||
{/* 售后金额按客户等级上浮折算预览(实收 = 输入 × (100+上浮%)/100);已生成账单行为存档值不再折算 */}
|
||||
{(
|
||||
<div className="text-xs text-orange-500 text-center">
|
||||
实收 ¥{billPreview[field.name]?.afterSale.toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
</Space.Addon>
|
||||
<Form.Item className="m-0!" name={[field.name, 'after_sale']}>
|
||||
<InputNumber
|
||||
className="w-full"
|
||||
precision={2}
|
||||
prefix={'¥'}
|
||||
disabled={billed}
|
||||
placeholder="可正负,计入总额"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
|
||||
</div>
|
||||
<Form.Item
|
||||
className="m-0! w-36 shrink-0 px-1!"
|
||||
name={[field.name, 'remark']}
|
||||
|
||||
Reference in New Issue
Block a user