feat(order): 订单支出台账(快递费/吊卡费等杂费)

订单上原本只有「卖给客户多少钱」,而一张单真实花出去的还有寄样品的快递费、
印吊卡的费用、商检报关拖车。不记下来就算不出这单赚没赚 —— 小单子几百块
杂费能把毛利吃掉一截。

· 新表 order_expenses,一张订单挂多笔:类型/金额/币种/日期/收款方/单号/
  已付未付/备注。单独成表而不是往 orders 加列,因为杂费是多笔不定项的,
  寄三次样品就是三条快递费,各有单号;塞进 orders 只能存一个合计,事后
  对不出「这 800 块是哪来的」。

· 跨币种强制填汇率并存快照。杂费基本是人民币、订单常是美元;不存快照的话
  汇率一变历史订单毛利跟着变,账永远对不上。同币种强制按 1,防止前端传个
  7.2 把美元支出又乘一遍。

· 新权限点 order.expense,不复用 order.edit。掏钱寄样品的是销售业务员,
  而销售业务组只有 order.view —— 挂在 order.edit 上要么他记不了账,要么
  为了让他记账得把「改订单金额」一起给他。启动时 syncNewPermissions()
  会自动把这个新点补给四个默认组。

· 订单列表加「杂费」列(合计 + 未付)。SUM 外层再 ROUND 一次:不加会漏出
  348.15999999999997 这种浮点尾巴直接显示在页面上。

· 金额逐行取两位再相加,保证明细几行加起来等于合计。

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Drsakura
2026-10-22 15:22:56 +08:00
co-authored by Claude Opus 5
parent 3e9fd2dc97
commit f32a2daaef
6 changed files with 643 additions and 5 deletions
+25 -2
View File
@@ -2,12 +2,16 @@ import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { SkeletonTableRows } from '../components/Skeleton'
import { useAuth, hasPermission } from '../auth'
import OrderExpenses from '../components/OrderExpenses'
function OrderManagement() {
const [orders, setOrders] = useState([])
const [loading, setLoading] = useState(true)
const { user } = useAuth()
const canEdit = hasPermission(user, 'order.edit')
/* 支出单独一个权限点:掏钱寄样品的是业务员,而业务组没有 order.edit */
const canExpense = hasPermission(user, 'order.expense')
const [expenseOrder, setExpenseOrder] = useState(null)
useEffect(() => {
fetchOrders()
@@ -51,12 +55,13 @@ function OrderManagement() {
<th>订单日期</th>
<th>交货日期</th>
<th>金额</th>
<th>杂费</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<SkeletonTableRows rows={5} widths={[1.2, 1.6, 1, 1, 1.2, 0.8, 1]} />
<SkeletonTableRows rows={5} widths={[1.2, 1.6, 1, 1, 1.2, 1, 0.8, 1]} />
</tbody>
</table>
</div>
@@ -80,6 +85,7 @@ function OrderManagement() {
<th>订单日期</th>
<th>交货日期</th>
<th>金额</th>
<th>杂费</th>
<th>状态</th>
<th>操作</th>
</tr>
@@ -87,7 +93,7 @@ function OrderManagement() {
<tbody>
{orders.length === 0 ? (
<tr>
<td colSpan="7" className="empty-state">暂无订单数据</td>
<td colSpan="8" className="empty-state">暂无订单数据</td>
</tr>
) : (
orders.map(order => (
@@ -105,6 +111,14 @@ function OrderManagement() {
? `${order.currency} ${order.total_amount.toFixed(2)}`
: '-'}
</td>
<td>
{order.expense_count ? (
<button type="button" className="btn-link" onClick={() => setExpenseOrder(order)}>
{order.currency} {Number(order.expense_total || 0).toFixed(2)}
{order.expense_unpaid > 0 && <em className="order-expense-unpaid"> 未付 {Number(order.expense_unpaid).toFixed(2)}</em>}
</button>
) : <span className="text-dim">—</span>}
</td>
<td>
<span className={`status-badge status-${order.status}`}>
{getStatusText(order.status)}
@@ -112,6 +126,7 @@ function OrderManagement() {
</td>
<td>
<button className="btn-link">查看</button>
{canExpense && <button type="button" className="btn-link" onClick={() => setExpenseOrder(order)}>支出</button>}
{canEdit && <button className="btn-link">编辑</button>}
</td>
</tr>
@@ -120,6 +135,14 @@ function OrderManagement() {
</tbody>
</table>
</div>
{expenseOrder && (
<OrderExpenses
order={expenseOrder}
onClose={() => setExpenseOrder(null)}
onChanged={fetchOrders}
/>
)}
</div>
)
}