文件还在测试中
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\Material;
|
||||
use App\Models\PSI\Product;
|
||||
use App\Models\PSI\Purchase;
|
||||
use App\Models\PSI\Sales;
|
||||
use App\Models\PSI\StockMove;
|
||||
|
||||
/** PSI 进销存入口与子路由。权限:super_admin 或 psi_role ∈ {admin,user} */
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function nav(string $seg): array
|
||||
{
|
||||
return \psi_nav();
|
||||
}
|
||||
|
||||
public function dispatch(array $s)
|
||||
{
|
||||
$res = $s[0] ?? 'dashboard';
|
||||
$action = $s[1] ?? '';
|
||||
$id = $s[2] ?? null;
|
||||
|
||||
// 仪表盘始终可进
|
||||
if ($res === 'dashboard') {
|
||||
return $this->dashboard();
|
||||
}
|
||||
|
||||
// 用户管理(仅该系统管理员):统一管理本系统用户及其页面权限
|
||||
if ($res === 'users') {
|
||||
if (!\subsys_admin('psi')) {
|
||||
\Core\App::forbidden('需要 PSI 管理员权限');
|
||||
return;
|
||||
}
|
||||
$uc = new \App\Controllers\PSI\UsersController();
|
||||
if ($action === '' || $action === 'index') return $uc->index();
|
||||
if ($action === 'create') return $uc->create();
|
||||
if ($action === 'store') return $uc->store();
|
||||
if ($action === 'edit') return $uc->edit($id);
|
||||
if ($action === 'update') return $uc->update($id);
|
||||
if ($action === 'destroy') return $uc->destroy($id);
|
||||
if ($action === 'reset') return $uc->reset($id);
|
||||
\Core\App::notFound('未知操作: ' . $action);
|
||||
return;
|
||||
}
|
||||
|
||||
// 订单管理:合并原「后台订单」到 PSI,方便管理人员在同一系统内快速处理
|
||||
if ($res === 'orders') {
|
||||
if (!\subsys_page_can('psi', 'orders')) {
|
||||
\Core\App::forbidden('您没有访问订单管理的权限');
|
||||
return;
|
||||
}
|
||||
$oc = new \App\Controllers\PSI\OrdersController();
|
||||
if ($action === '' || $action === 'index') return $oc->index();
|
||||
if ($action === 'show') return $oc->show($id);
|
||||
if ($action === 'markPaid') return $oc->markPaid($id);
|
||||
if ($action === 'destroy') return $oc->destroy($id);
|
||||
\Core\App::notFound('未知操作: ' . $action);
|
||||
return;
|
||||
}
|
||||
|
||||
// 销售订单:录入 / 打印 / 关联出库
|
||||
if ($res === 'sales_orders') {
|
||||
if (!\subsys_page_can('psi', 'sales_orders')) { \Core\App::forbidden('您没有访问销售订单的权限'); return; }
|
||||
$c = new \App\Controllers\PSI\SalesOrdersController();
|
||||
if ($action === '' || $action === 'index') return $c->index();
|
||||
if ($action === 'create') return $c->create();
|
||||
if ($action === 'store') return $c->store();
|
||||
if ($action === 'show') return $c->show($id);
|
||||
if ($action === 'edit') return $c->edit($id);
|
||||
if ($action === 'update') return $c->update($id);
|
||||
if ($action === 'destroy') return $c->destroy($id);
|
||||
if ($action === 'print') return $c->printDoc($id);
|
||||
\Core\App::notFound('未知操作: ' . $action); return;
|
||||
}
|
||||
|
||||
// 采购订单:录入 / 打印 / 收货入库
|
||||
if ($res === 'purchase_orders') {
|
||||
if (!\subsys_page_can('psi', 'purchase_orders')) { \Core\App::forbidden('您没有访问采购订单的权限'); return; }
|
||||
$c = new \App\Controllers\PSI\PurchaseOrdersController();
|
||||
if ($action === '' || $action === 'index') return $c->index();
|
||||
if ($action === 'create') return $c->create();
|
||||
if ($action === 'store') return $c->store();
|
||||
if ($action === 'show') return $c->show($id);
|
||||
if ($action === 'edit') return $c->edit($id);
|
||||
if ($action === 'update') return $c->update($id);
|
||||
if ($action === 'destroy') return $c->destroy($id);
|
||||
if ($action === 'receive') return $c->receive($id);
|
||||
if ($action === 'print') return $c->printDoc($id);
|
||||
\Core\App::notFound('未知操作: ' . $action); return;
|
||||
}
|
||||
|
||||
// 出库单:录入 / 打印(关联销售订单、扣减库存)
|
||||
if ($res === 'outbounds') {
|
||||
if (!\subsys_page_can('psi', 'outbounds')) { \Core\App::forbidden('您没有访问出库单的权限'); return; }
|
||||
$c = new \App\Controllers\PSI\OutboundsController();
|
||||
if ($action === '' || $action === 'index') return $c->index();
|
||||
if ($action === 'create') return $c->create();
|
||||
if ($action === 'store') return $c->store();
|
||||
if ($action === 'show') return $c->show($id);
|
||||
if ($action === 'edit') return $c->edit($id);
|
||||
if ($action === 'update') return $c->update($id);
|
||||
if ($action === 'destroy') return $c->destroy($id);
|
||||
if ($action === 'print') return $c->printDoc($id);
|
||||
\Core\App::notFound('未知操作: ' . $action); return;
|
||||
}
|
||||
|
||||
// 报表中心:采购订单明细 / 销售·采购订单明细 / 交付明细
|
||||
if ($res === 'reports') {
|
||||
if (!\subsys_page_can('psi', 'reports')) { \Core\App::forbidden('您没有访问报表中心的权限'); return; }
|
||||
$c = new \App\Controllers\PSI\ReportsController();
|
||||
if ($action === '' || $action === 'index') return $c->index();
|
||||
if ($action === 'poDetail') return $c->poDetail();
|
||||
if ($action === 'soPo') return $c->soPo();
|
||||
if ($action === 'delivery') return $c->delivery();
|
||||
\Core\App::notFound('未知操作: ' . $action); return;
|
||||
}
|
||||
|
||||
// 紧急提醒中心(所有 PSI 用户可见)
|
||||
if ($res === 'reminders') {
|
||||
$c = new \App\Controllers\PSI\RemindersController();
|
||||
return $c->handle(array_slice($s, 1));
|
||||
}
|
||||
|
||||
// 通知设置(仅 PSI 管理员,控制器内二次鉴权)
|
||||
if ($res === 'notifications') {
|
||||
$c = new \App\Controllers\PSI\NotificationsController();
|
||||
return $c->handle(array_slice($s, 1));
|
||||
}
|
||||
|
||||
// 销售出库(支持 show/print 查看与打印预览)
|
||||
if ($res === 'sales') {
|
||||
if (!\subsys_page_can('psi', 'sales')) { \Core\App::forbidden('您没有访问销售出库的权限'); return; }
|
||||
$c = new \App\Controllers\PSI\SalesController();
|
||||
if ($action === '' || $action === 'index') return $c->index();
|
||||
if ($action === 'create') return $c->create();
|
||||
if ($action === 'store') return $c->store();
|
||||
if ($action === 'show') return $c->show($id);
|
||||
if ($action === 'destroy') return $c->destroy($id);
|
||||
if ($action === 'print') return $c->printDoc($id);
|
||||
\Core\App::notFound('未知操作: ' . $action); return;
|
||||
}
|
||||
|
||||
$map = [
|
||||
'materials' => 'MaterialsController',
|
||||
'products' => 'ProductsController',
|
||||
'suppliers' => 'SuppliersController',
|
||||
'purchases' => 'PurchasesController',
|
||||
'stock' => 'StockController',
|
||||
];
|
||||
if (!isset($map[$res])) {
|
||||
\Core\App::notFound('未知页面: ' . $res);
|
||||
return;
|
||||
}
|
||||
// 页面级权限:按分系统「页面可见权限」拦截
|
||||
if (!\subsys_page_can('psi', $res)) {
|
||||
\Core\App::forbidden('您没有访问该页面的权限');
|
||||
return;
|
||||
}
|
||||
// 资源子操作路由:/PSI/{resource}[/{action}[/{id}]]
|
||||
// 支持 index/create/store/edit/update/destroy/adjust
|
||||
$method = $this->resolveSubsysAction($action);
|
||||
if ($method === null) {
|
||||
\Core\App::notFound('未知操作: ' . $action);
|
||||
return;
|
||||
}
|
||||
$class = 'App\\Controllers\\PSI\\' . $map[$res];
|
||||
$instance = new $class();
|
||||
if (!method_exists($instance, $method)) {
|
||||
\Core\App::notFound('操作不存在: ' . $method);
|
||||
return;
|
||||
}
|
||||
return $instance->$method($id);
|
||||
}
|
||||
|
||||
/** 将 URL 动作段解析为控制器方法名;不支持的动作返回 null */
|
||||
private function resolveSubsysAction(string $action): ?string
|
||||
{
|
||||
$verbs = [
|
||||
'' => 'index',
|
||||
'index' => 'index',
|
||||
'create' => 'create',
|
||||
'store' => 'store',
|
||||
'edit' => 'edit',
|
||||
'update' => 'update',
|
||||
'destroy' => 'destroy',
|
||||
'adjust' => 'adjust',
|
||||
];
|
||||
return $verbs[$action] ?? null;
|
||||
}
|
||||
|
||||
public function dashboard()
|
||||
{
|
||||
$materials = (new Material())->all();
|
||||
$products = (new Product())->all();
|
||||
$purchases = (new Purchase())->all();
|
||||
$sales = (new Sales())->all();
|
||||
|
||||
$matStock = array_sum(array_map(fn($m) => (float)($m['stock'] ?? 0), $materials));
|
||||
$prodStock = array_sum(array_map(fn($p) => (float)($p['stock'] ?? 0), $products));
|
||||
$purchaseAmt = array_sum(array_map(fn($p) => (float)($p['amount'] ?? 0), $purchases));
|
||||
$salesAmt = array_sum(array_map(fn($s) => (float)($s['amount'] ?? 0), $sales));
|
||||
$lowStock = [];
|
||||
foreach ($materials as $m) { if ((float)($m['stock'] ?? 0) < 20) $lowStock[] = ['type' => '物料', 'item' => $m]; }
|
||||
foreach ($products as $p) { if ((float)($p['stock'] ?? 0) < 20) $lowStock[] = ['type' => '成品', 'item' => $p]; }
|
||||
|
||||
$recentPurchases = array_slice(array_reverse($purchases), 0, 5);
|
||||
$recentSales = array_slice(array_reverse($sales), 0, 5);
|
||||
|
||||
// 订单/出库业务指标(表未创建时静默降级)
|
||||
$bySalesman = [];
|
||||
$salesAmount = 0; $salesAmountMonth = 0;
|
||||
$undelivered = ['count' => 0, 'amount' => 0];
|
||||
$monthDeliveries = ['count' => 0, 'amount' => 0];
|
||||
try {
|
||||
$bySalesman = \Core\Db::query(
|
||||
"SELECT o.salesman AS salesman, COUNT(DISTINCT o.id) AS orders,
|
||||
COALESCE(SUM(i.amount),0) AS amount
|
||||
FROM psi_sales_orders o
|
||||
LEFT JOIN psi_sales_order_items i ON i.so_id=o.id
|
||||
WHERE o.salesman <> '' AND o.status <> 'closed'
|
||||
GROUP BY o.salesman ORDER BY amount DESC"
|
||||
)->fetchAll();
|
||||
|
||||
$r = \Core\Db::query(
|
||||
"SELECT COALESCE(SUM(i.amount),0) AS amt,
|
||||
COALESCE(SUM(CASE WHEN MONTH(o.created_at)=MONTH(CURDATE()) AND YEAR(o.created_at)=YEAR(CURDATE()) THEN i.amount ELSE 0 END),0) AS amt_m
|
||||
FROM psi_sales_orders o LEFT JOIN psi_sales_order_items i ON i.so_id=o.id
|
||||
WHERE o.status <> 'closed'"
|
||||
)->fetch();
|
||||
$salesAmount = (float)($r['amt'] ?? 0);
|
||||
$salesAmountMonth = (float)($r['amt_m'] ?? 0);
|
||||
|
||||
$u = \Core\Db::query(
|
||||
"SELECT COUNT(*) AS c, COALESCE(SUM(i.amount),0) AS amt
|
||||
FROM psi_sales_orders o LEFT JOIN psi_sales_order_items i ON i.so_id=o.id
|
||||
WHERE o.status IN ('pending','partial')"
|
||||
)->fetch();
|
||||
$undelivered = ['count' => (int)($u['c'] ?? 0), 'amount' => (float)($u['amt'] ?? 0)];
|
||||
|
||||
$d = \Core\Db::query(
|
||||
"SELECT COUNT(*) AS c, COALESCE(SUM(i.amount),0) AS amt
|
||||
FROM psi_outbounds ob LEFT JOIN psi_outbound_items i ON i.ob_id=ob.id
|
||||
WHERE MONTH(ob.created_at)=MONTH(CURDATE()) AND YEAR(ob.created_at)=YEAR(CURDATE())"
|
||||
)->fetch();
|
||||
$monthDeliveries = ['count' => (int)($d['c'] ?? 0), 'amount' => (float)($d['amt'] ?? 0)];
|
||||
} catch (\Throwable $e) { /* 表未创建时不报错 */ }
|
||||
|
||||
return $this->renderSubsys('psi', 'psi/dashboard', [
|
||||
'materialTotal' => count($materials),
|
||||
'productTotal' => count($products),
|
||||
'matStock' => $matStock,
|
||||
'prodStock' => $prodStock,
|
||||
'purchaseAmt' => $purchaseAmt,
|
||||
'salesAmt' => $salesAmt,
|
||||
'lowStock' => $lowStock,
|
||||
'recentPurchases' => $recentPurchases,
|
||||
'recentSales' => $recentSales,
|
||||
'bySalesman' => $bySalesman,
|
||||
'salesAmount' => $salesAmount,
|
||||
'salesAmountMonth' => $salesAmountMonth,
|
||||
'undelivered' => $undelivered,
|
||||
'monthDeliveries' => $monthDeliveries,
|
||||
], $this->nav('dashboard'), 'dashboard');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\Material;
|
||||
use App\Models\PSI\Supplier;
|
||||
|
||||
/** 物料管理(面料/辅料等) */
|
||||
class MaterialsController extends Controller
|
||||
{
|
||||
use StockHelper;
|
||||
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$materials = (new Material())->all();
|
||||
$suppliers = (new Supplier())->all();
|
||||
$smap = [];
|
||||
foreach ($suppliers as $s) { $smap[$s['id']] = $s['name']; }
|
||||
return $this->renderSubsys('psi', 'psi/materials', ['materials' => $materials, 'smap' => $smap], \psi_nav(), 'materials');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
$suppliers = (new Supplier())->all();
|
||||
return $this->renderSubsys('psi', 'psi/material_form', ['material' => null, 'suppliers' => $suppliers], \psi_nav(), 'materials');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if (!csrf_check()) return $this->redirect('PSI/materials');
|
||||
(new Material())->insert($this->collect());
|
||||
return $this->redirect('PSI/materials');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
$material = (new Material())->find($id);
|
||||
if (!$material) return $this->redirect('PSI/materials');
|
||||
$suppliers = (new Supplier())->all();
|
||||
return $this->renderSubsys('psi', 'psi/material_form', ['material' => $material, 'suppliers' => $suppliers], \psi_nav(), 'materials');
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if (!csrf_check()) return $this->redirect('PSI/materials');
|
||||
$material = (new Material())->find($id);
|
||||
if (!$material) return $this->redirect('PSI/materials');
|
||||
(new Material())->update($id, $this->collect());
|
||||
return $this->redirect('PSI/materials');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
(new Material())->delete($id);
|
||||
return $this->redirect('PSI/materials');
|
||||
}
|
||||
|
||||
/** 手动调整库存(盘盈/盘亏/报损) */
|
||||
public function adjust($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!csrf_check()) return $this->redirect('PSI/materials');
|
||||
$qty = (float)$this->post('qty');
|
||||
$direction = $this->post('direction') === 'out' ? 'out' : 'in';
|
||||
$this->adjustStock('material', (int)$id, abs($qty), $direction, 'ADJ-' . date('Ymd'));
|
||||
return $this->redirect('PSI/materials');
|
||||
}
|
||||
$material = (new Material())->find($id);
|
||||
if (!$material) return $this->redirect('PSI/materials');
|
||||
return $this->renderSubsys('psi', 'psi/adjust_form', ['item' => $material, 'type' => 'material'], \psi_nav(), 'materials');
|
||||
}
|
||||
|
||||
private function collect(): array
|
||||
{
|
||||
return [
|
||||
'code' => trim($this->post('code')),
|
||||
'name' => trim($this->post('name')),
|
||||
'spec' => trim($this->post('spec')),
|
||||
'unit' => trim($this->post('unit')) ?: '个',
|
||||
'category' => trim($this->post('category')),
|
||||
'composition' => trim($this->post('composition')),
|
||||
'weight_gsm' => (float)$this->post('weight_gsm'),
|
||||
'width_cm' => (float)$this->post('width_cm'),
|
||||
'color' => trim($this->post('color')),
|
||||
'batch_no' => trim($this->post('batch_no')),
|
||||
'stock' => (float)$this->post('stock'),
|
||||
'price' => (float)$this->post('price'),
|
||||
'supplier_id' => (int)$this->post('supplier_id'),
|
||||
'remark' => trim($this->post('remark')),
|
||||
'created_at' => date('Y-m-d'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use Core\Controller;
|
||||
use App\Models\Setting;
|
||||
|
||||
/**
|
||||
* PSI 通知设置(仅 PSI 管理员可访问)
|
||||
* - 总开关 + 邮件(SMTP)/ 企业微信(群机器人 webhook)配置
|
||||
* - 负责人邮箱、被@手机号
|
||||
* - 低库存预警开关与阈值
|
||||
* 通过 PSI 仪表盘统一分发:PSI/notifications[/save]
|
||||
*/
|
||||
class NotificationsController extends Controller
|
||||
{
|
||||
private const KEYS = [
|
||||
'notify_enabled', 'notify_email_enabled', 'notify_email_smtp_host', 'notify_email_smtp_port',
|
||||
'notify_email_smtp_user', 'notify_email_smtp_pass', 'notify_email_from', 'notify_email_to',
|
||||
'notify_wechat_enabled', 'notify_wechat_webhook', 'notify_wechat_mention',
|
||||
'notify_lowstock_enabled', 'notify_lowstock_threshold',
|
||||
];
|
||||
|
||||
/** 统一入口 */
|
||||
public function handle(array $args = []): void
|
||||
{
|
||||
if (!subsys_admin('psi')) { http_response_code(403); echo '无权限:仅 PSI 管理员可配置通知'; return; }
|
||||
$action = $args[0] ?? 'index';
|
||||
if ($action === 'save') {
|
||||
$this->save();
|
||||
return;
|
||||
}
|
||||
$this->index();
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$s = new Setting();
|
||||
$v = [];
|
||||
foreach (self::KEYS as $k) {
|
||||
$v[$k] = $s->get($k, '');
|
||||
}
|
||||
// 布尔项默认值
|
||||
if ($v['notify_lowstock_enabled'] === '') $v['notify_lowstock_enabled'] = 1;
|
||||
if ($v['notify_lowstock_threshold'] === '') $v['notify_lowstock_threshold'] = 20;
|
||||
|
||||
$this->renderSubsys('psi', 'psi/notifications', ['v' => $v], \psi_nav(), 'notifications');
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !csrf_check()) {
|
||||
$this->redirect('PSI/notifications');
|
||||
}
|
||||
$s = new Setting();
|
||||
$post = $_POST;
|
||||
$s->set('notify_enabled', isset($post['notify_enabled']) ? 1 : 0);
|
||||
$s->set('notify_email_enabled', isset($post['notify_email_enabled']) ? 1 : 0);
|
||||
$s->set('notify_email_smtp_host', trim((string) ($post['notify_email_smtp_host'] ?? '')));
|
||||
$s->set('notify_email_smtp_port', (int) ($post['notify_email_smtp_port'] ?? 465));
|
||||
$s->set('notify_email_smtp_user', trim((string) ($post['notify_email_smtp_user'] ?? '')));
|
||||
$s->set('notify_email_smtp_pass', trim((string) ($post['notify_email_smtp_pass'] ?? '')));
|
||||
$s->set('notify_email_from', trim((string) ($post['notify_email_from'] ?? '')));
|
||||
$s->set('notify_email_to', trim((string) ($post['notify_email_to'] ?? '')));
|
||||
$s->set('notify_wechat_enabled', isset($post['notify_wechat_enabled']) ? 1 : 0);
|
||||
$s->set('notify_wechat_webhook', trim((string) ($post['notify_wechat_webhook'] ?? '')));
|
||||
$s->set('notify_wechat_mention', trim((string) ($post['notify_wechat_mention'] ?? '')));
|
||||
$s->set('notify_lowstock_enabled', isset($post['notify_lowstock_enabled']) ? 1 : 0);
|
||||
$s->set('notify_lowstock_threshold', max(0, (float) ($post['notify_lowstock_threshold'] ?? 20)));
|
||||
|
||||
$this->flash('通知设置已保存', 'ok');
|
||||
$this->redirect('PSI/notifications');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
use Core\Payment\OrderService;
|
||||
|
||||
/**
|
||||
* PSI 订单管理:将原「后台订单」合并到进销存系统,便于管理人员在同一系统快速处理。
|
||||
* 权限:该系统拥有 orders 页面权限的用户(默认 PSI 管理员/超管全部可见,普通用户按 perms 细粒度控制)。
|
||||
* 关键动作(标记支付、删除)仅限 PSI 管理员,避免普通操作员误改订单。
|
||||
*/
|
||||
class OrdersController extends Controller
|
||||
{
|
||||
/** 列表:最新在前 */
|
||||
public function index()
|
||||
{
|
||||
$order = new Order();
|
||||
$all = array_reverse($order->all());
|
||||
return $this->renderSubsys('psi', 'psi/orders', ['orders' => $all], \subsys_nav('psi'), 'orders');
|
||||
}
|
||||
|
||||
/** 详情:订单信息 + 付款记录 */
|
||||
public function show($id)
|
||||
{
|
||||
$order = new Order();
|
||||
$o = $order->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$payments = (new Payment())->whereAll('order_no', $o['order_no']);
|
||||
return $this->renderSubsys('psi', 'psi/order_show', [
|
||||
'o' => $o, 'payments' => $payments,
|
||||
], \subsys_nav('psi'), 'orders');
|
||||
}
|
||||
|
||||
/** 标记为已支付(仅 PSI 管理员) */
|
||||
public function markPaid($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_check()) {
|
||||
$order = new Order();
|
||||
$o = $order->find($id);
|
||||
if ($o && $o['status'] !== 'paid') {
|
||||
OrderService::markPaid($o['order_no'], 'MANUAL' . time(), $o['channel'] ?: 'manual');
|
||||
$this->flash('订单已标记为已支付', 'ok');
|
||||
}
|
||||
}
|
||||
return $this->redirect('PSI/orders/show/' . $id);
|
||||
}
|
||||
|
||||
/** 删除订单(仅 PSI 管理员) */
|
||||
public function destroy($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_check()) {
|
||||
(new Order())->delete($id);
|
||||
$this->flash('订单已删除', 'ok');
|
||||
}
|
||||
return $this->redirect('PSI/orders');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\Outbound;
|
||||
use App\Models\PSI\OutboundItem;
|
||||
use App\Models\PSI\SalesOrder;
|
||||
use App\Models\PSI\SalesOrderItem;
|
||||
use App\Models\PSI\Product;
|
||||
use App\Controllers\PSI\StockHelper;
|
||||
|
||||
/**
|
||||
* 出库单(交付):关联销售订单,扣减成品库存,回写销售订单已交付数量。
|
||||
* 编辑/删除会先回冲原库存与已交付量,再重新应用,保证数据一致。
|
||||
*/
|
||||
class OutboundsController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$list = array_reverse((new Outbound())->all());
|
||||
$itemM = new OutboundItem();
|
||||
foreach ($list as &$o) { $o['_items'] = $itemM->whereAll('ob_id', $o['id']); }
|
||||
return $this->renderSubsys('psi', 'psi/outbounds', ['list' => $list], \psi_nav(), 'outbounds');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$soNo = $this->get('so');
|
||||
$so = $soNo ? (new SalesOrder())->where('order_no', $soNo) : null;
|
||||
$soItems = $so ? (new SalesOrderItem())->whereAll('so_id', $so['id']) : [];
|
||||
return $this->renderSubsys('psi', 'psi/outbound_form', [
|
||||
'o' => null, 'items' => [], 'so' => $so, 'soItems' => $soItems, 'products' => (new Product())->all(),
|
||||
], \psi_nav(), 'outbounds');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !csrf_check()) return $this->redirect('PSI/outbounds/create');
|
||||
$items = $this->parseItems();
|
||||
if (empty($items)) { $this->flash('请至少添加一条出库明细', 'err'); return $this->redirect('PSI/outbounds/create'); }
|
||||
|
||||
$soNo = trim($this->post('so_no'));
|
||||
$so = $soNo ? (new SalesOrder())->where('order_no', $soNo) : null;
|
||||
$orderNo = \psi_gen_no('OUT');
|
||||
$id = (new Outbound())->insert([
|
||||
'order_no' => $orderNo,
|
||||
'so_no' => $soNo,
|
||||
'customer' => trim($this->post('customer')) ?: ($so['customer'] ?? ''),
|
||||
'salesman' => trim($this->post('salesman')) ?: ($so['salesman'] ?? ($_SESSION['admin_name'] ?? '')),
|
||||
'warehouse' => trim($this->post('warehouse')),
|
||||
'status' => 'delivered',
|
||||
'delivery_date' => $this->post('delivery_date') ?: null,
|
||||
'remark' => trim($this->post('remark')),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$this->applyItems($id, $items, $soNo, $orderNo);
|
||||
if ($so) {
|
||||
\psi_recompute_so($so['id']);
|
||||
$st = (new SalesOrder())->find($so['id'])['status'];
|
||||
(new Outbound())->update($id, ['status' => $st === 'delivered' ? 'delivered' : 'partial']);
|
||||
}
|
||||
$this->flash('出库单已保存', 'ok');
|
||||
if ($this->post('auto_print', '1') !== '0') {
|
||||
return $this->redirect('PSI/outbounds/print/' . $id);
|
||||
}
|
||||
return $this->redirect('PSI/outbounds/show/' . $id);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$o = (new Outbound())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = (new OutboundItem())->whereAll('ob_id', $id);
|
||||
return $this->renderSubsys('psi', 'psi/outbound_show', ['o' => $o, 'items' => $items], \psi_nav(), 'outbounds');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$o = (new Outbound())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = (new OutboundItem())->whereAll('ob_id', $id);
|
||||
$so = $o['so_no'] ? (new SalesOrder())->where('order_no', $o['so_no']) : null;
|
||||
$soItems = $so ? (new SalesOrderItem())->whereAll('so_id', $so['id']) : [];
|
||||
return $this->renderSubsys('psi', 'psi/outbound_form', [
|
||||
'o' => $o, 'items' => $items, 'so' => $so, 'soItems' => $soItems, 'products' => (new Product())->all(),
|
||||
], \psi_nav(), 'outbounds');
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !csrf_check()) return $this->redirect('PSI/outbounds/edit/' . $id);
|
||||
$o = (new Outbound())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = $this->parseItems();
|
||||
if (empty($items)) { $this->flash('请至少添加一条出库明细', 'err'); return $this->redirect('PSI/outbounds/edit/' . $id); }
|
||||
|
||||
$this->reverseItems($id); // 回冲库存与已交付
|
||||
(new Outbound())->update($id, [
|
||||
'customer' => trim($this->post('customer')),
|
||||
'salesman' => trim($this->post('salesman')) ?: ($_SESSION['admin_name'] ?? ''),
|
||||
'warehouse' => trim($this->post('warehouse')),
|
||||
'delivery_date' => $this->post('delivery_date') ?: null,
|
||||
'remark' => trim($this->post('remark')),
|
||||
]);
|
||||
$this->applyItems($id, $items, $o['so_no'], $o['order_no']);
|
||||
if ($o['so_no']) {
|
||||
$so = (new SalesOrder())->where('order_no', $o['so_no']);
|
||||
if ($so) \psi_recompute_so((int)$so['id']);
|
||||
}
|
||||
$this->flash('出库单已更新', 'ok');
|
||||
return $this->redirect('PSI/outbounds/show/' . $id);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_check()) {
|
||||
$this->reverseItems($id);
|
||||
(new OutboundItem())->deleteRaw('ob_id', $id);
|
||||
(new Outbound())->delete($id);
|
||||
$this->flash('出库单已删除', 'ok');
|
||||
}
|
||||
return $this->redirect('PSI/outbounds');
|
||||
}
|
||||
|
||||
public function printDoc($id)
|
||||
{
|
||||
$o = (new Outbound())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = (new OutboundItem())->whereAll('ob_id', $id);
|
||||
$body = \App\Core\View::render('psi/outbound_print', ['o' => $o, 'items' => $items]);
|
||||
echo \psi_print_shell('出库单 ' . $o['order_no'], $body);
|
||||
}
|
||||
|
||||
/** 应用出库明细:写明细 + 扣库存 + 回写销售订单已交付量 */
|
||||
private function applyItems(int $obId, array $items, string $soNo, string $orderNo): void
|
||||
{
|
||||
$itemM = new OutboundItem();
|
||||
$so = $soNo ? (new SalesOrder())->where('order_no', $soNo) : null;
|
||||
foreach ($items as $it) {
|
||||
$it['ob_id'] = $obId;
|
||||
$itemM->insert($it);
|
||||
if ((int)($it['product_id'] ?? 0) > 0) {
|
||||
StockHelper::adjustStock((int)$it['product_id'], (int)$it['qty'], 'out', '销售出库', $orderNo);
|
||||
}
|
||||
if ((int)($it['so_item_id'] ?? 0) > 0) {
|
||||
$si = (new SalesOrderItem())->find($it['so_item_id']);
|
||||
if ($si) {
|
||||
(new SalesOrderItem())->update($si['id'], ['delivered_qty' => (int)$si['delivered_qty'] + (int)$it['qty']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 回冲:把出库明细的库存与销售订单已交付量还原 */
|
||||
private function reverseItems(int $obId): void
|
||||
{
|
||||
$old = (new OutboundItem())->whereAll('ob_id', $obId);
|
||||
foreach ($old as $oi) {
|
||||
if ((int)($oi['product_id'] ?? 0) > 0) {
|
||||
StockHelper::adjustStock((int)$oi['product_id'], (int)$oi['qty'], 'in', '出库冲正', $oi['ob_id'] ?? '');
|
||||
}
|
||||
if ((int)($oi['so_item_id'] ?? 0) > 0) {
|
||||
$si = (new SalesOrderItem())->find($oi['so_item_id']);
|
||||
if ($si) {
|
||||
$back = max(0, (int)$si['delivered_qty'] - (int)$oi['qty']);
|
||||
(new SalesOrderItem())->update($si['id'], ['delivered_qty' => $back]);
|
||||
}
|
||||
}
|
||||
}
|
||||
(new OutboundItem())->deleteRaw('ob_id', $obId);
|
||||
}
|
||||
|
||||
private function parseItems(): array
|
||||
{
|
||||
$raw = $_POST['items'] ?? [];
|
||||
$out = [];
|
||||
foreach ($raw as $row) {
|
||||
$name = trim((string)($row['name'] ?? ''));
|
||||
$qty = (int)($row['qty'] ?? 0);
|
||||
$price = (float)($row['price'] ?? 0);
|
||||
if ($name === '' || $qty <= 0) continue;
|
||||
$out[] = [
|
||||
'so_item_id' => (int)($row['so_item_id'] ?? 0),
|
||||
'product_id' => (int)($row['product_id'] ?? 0),
|
||||
'name' => $name,
|
||||
'spec' => trim((string)($row['spec'] ?? '')),
|
||||
'unit' => trim((string)($row['unit'] ?? '')),
|
||||
'qty' => $qty,
|
||||
'price' => $price,
|
||||
'amount' => round($qty * $price, 2),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\Product;
|
||||
|
||||
/** 成品管理(降温服/成衣) */
|
||||
class ProductsController extends Controller
|
||||
{
|
||||
use StockHelper;
|
||||
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$products = (new Product())->all();
|
||||
return $this->renderSubsys('psi', 'psi/products', ['products' => $products], \psi_nav(), 'products');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
return $this->renderSubsys('psi', 'psi/product_form', ['product' => null], \psi_nav(), 'products');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if (!csrf_check()) return $this->redirect('PSI/products');
|
||||
(new Product())->insert($this->collect());
|
||||
return $this->redirect('PSI/products');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
$product = (new Product())->find($id);
|
||||
if (!$product) return $this->redirect('PSI/products');
|
||||
return $this->renderSubsys('psi', 'psi/product_form', ['product' => $product], \psi_nav(), 'products');
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if (!csrf_check()) return $this->redirect('PSI/products');
|
||||
$product = (new Product())->find($id);
|
||||
if (!$product) return $this->redirect('PSI/products');
|
||||
(new Product())->update($id, $this->collect());
|
||||
return $this->redirect('PSI/products');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
(new Product())->delete($id);
|
||||
return $this->redirect('PSI/products');
|
||||
}
|
||||
|
||||
public function adjust($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!csrf_check()) return $this->redirect('PSI/products');
|
||||
$qty = (float)$this->post('qty');
|
||||
$direction = $this->post('direction') === 'out' ? 'out' : 'in';
|
||||
$this->adjustStock('product', (int)$id, abs($qty), $direction, 'ADJ-' . date('Ymd'));
|
||||
return $this->redirect('PSI/products');
|
||||
}
|
||||
$product = (new Product())->find($id);
|
||||
if (!$product) return $this->redirect('PSI/products');
|
||||
return $this->renderSubsys('psi', 'psi/adjust_form', ['item' => $product, 'type' => 'product'], \psi_nav(), 'products');
|
||||
}
|
||||
|
||||
private function collect(): array
|
||||
{
|
||||
return [
|
||||
'code' => trim($this->post('code')),
|
||||
'name' => trim($this->post('name')),
|
||||
'spec' => trim($this->post('spec')),
|
||||
'unit' => trim($this->post('unit')) ?: '件',
|
||||
'category' => trim($this->post('category')),
|
||||
'style_no' => trim($this->post('style_no')),
|
||||
'color' => trim($this->post('color')),
|
||||
'size_run' => trim($this->post('size_run')),
|
||||
'season' => trim($this->post('season')),
|
||||
'year' => trim($this->post('year')),
|
||||
'stock' => (float)$this->post('stock'),
|
||||
'cost' => (float)$this->post('cost'),
|
||||
'price' => (float)$this->post('price'),
|
||||
'remark' => trim($this->post('remark')),
|
||||
'created_at' => date('Y-m-d'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\PurchaseOrder;
|
||||
use App\Models\PSI\PurchaseOrderItem;
|
||||
use App\Models\PSI\Material;
|
||||
use App\Models\PSI\Product;
|
||||
use App\Models\PSI\Purchase;
|
||||
use App\Controllers\PSI\StockHelper;
|
||||
|
||||
/**
|
||||
* 采购订单:录入(可打印)、收货入库(写 psi_purchases 并增加成品库存)。
|
||||
* 供应商从 psi_suppliers 选择;明细可为物料或成品。
|
||||
*/
|
||||
class PurchaseOrdersController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$orders = array_reverse((new PurchaseOrder())->all());
|
||||
$itemM = new PurchaseOrderItem();
|
||||
foreach ($orders as &$o) { $o['_items'] = $itemM->whereAll('po_id', $o['id']); }
|
||||
return $this->renderSubsys('psi', 'psi/purchase_orders', ['orders' => $orders], \psi_nav(), 'purchase_orders');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return $this->renderSubsys('psi', 'psi/purchase_order_form', [
|
||||
'o' => null, 'items' => [], 'materials' => (new Material())->all(), 'products' => (new Product())->all(),
|
||||
], \psi_nav(), 'purchase_orders');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !csrf_check()) return $this->redirect('PSI/purchase_orders/create');
|
||||
$items = $this->parseItems();
|
||||
if (empty($items)) { $this->flash('请至少添加一条采购明细', 'err'); return $this->redirect('PSI/purchase_orders/create'); }
|
||||
$id = (new PurchaseOrder())->insert([
|
||||
'order_no' => $oNo = \psi_gen_no('PO'),
|
||||
'supplier_id' => (int)$this->post('supplier_id'),
|
||||
'supplier_name' => $supp = trim($this->post('supplier_name')),
|
||||
'salesman' => $buyer = trim($this->post('salesman')) ?: ($_SESSION['admin_name'] ?? ''),
|
||||
'status' => 'pending',
|
||||
'expected_at' => $this->post('expected_at') ?: null,
|
||||
'remark' => trim($this->post('remark')),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$itemM = new PurchaseOrderItem();
|
||||
foreach ($items as $it) { $it['po_id'] = $id; $itemM->insert($it); }
|
||||
\Core\Notify::newPurchaseOrder($oNo, $supp, $buyer, $id);
|
||||
$this->flash('采购订单已创建', 'ok');
|
||||
if ($this->post('auto_print', '1') !== '0') {
|
||||
return $this->redirect('PSI/purchase_orders/print/' . $id);
|
||||
}
|
||||
return $this->redirect('PSI/purchase_orders/show/' . $id);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$o = (new PurchaseOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = (new PurchaseOrderItem())->whereAll('po_id', $id);
|
||||
return $this->renderSubsys('psi', 'psi/purchase_order_show', ['o' => $o, 'items' => $items], \psi_nav(), 'purchase_orders');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$o = (new PurchaseOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
if (in_array($o['status'], ['received'], true)) { $this->flash('已收货的采购订单不可编辑', 'err'); return $this->redirect('PSI/purchase_orders/show/' . $id); }
|
||||
$items = (new PurchaseOrderItem())->whereAll('po_id', $id);
|
||||
return $this->renderSubsys('psi', 'psi/purchase_order_form', [
|
||||
'o' => $o, 'items' => $items, 'materials' => (new Material())->all(), 'products' => (new Product())->all(),
|
||||
], \psi_nav(), 'purchase_orders');
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !csrf_check()) return $this->redirect('PSI/purchase_orders/edit/' . $id);
|
||||
$o = (new PurchaseOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = $this->parseItems();
|
||||
if (empty($items)) { $this->flash('请至少添加一条采购明细', 'err'); return $this->redirect('PSI/purchase_orders/edit/' . $id); }
|
||||
|
||||
(new PurchaseOrder())->update($id, [
|
||||
'supplier_id' => (int)$this->post('supplier_id'),
|
||||
'supplier_name' => trim($this->post('supplier_name')),
|
||||
'salesman' => trim($this->post('salesman')) ?: ($_SESSION['admin_name'] ?? ''),
|
||||
'expected_at' => $this->post('expected_at') ?: null,
|
||||
'remark' => trim($this->post('remark')),
|
||||
]);
|
||||
$old = (new PurchaseOrderItem())->whereAll('po_id', $id);
|
||||
$recv = [];
|
||||
foreach ($old as $oi) { $recv[($oi['item_type'] ?? '') . '|' . ($oi['item_id'] ?? 0) . '|' . $oi['name']] = (int)($oi['received_qty'] ?? 0); }
|
||||
(new PurchaseOrderItem())->deleteRaw('po_id', $id);
|
||||
foreach ($items as $it) {
|
||||
$key = ($it['item_type'] ?? '') . '|' . ($it['item_id'] ?? 0) . '|' . $it['name'];
|
||||
$it['received_qty'] = $recv[$key] ?? 0;
|
||||
$it['po_id'] = $id;
|
||||
(new PurchaseOrderItem())->insert($it);
|
||||
}
|
||||
$this->flash('采购订单已更新', 'ok');
|
||||
return $this->redirect('PSI/purchase_orders/show/' . $id);
|
||||
}
|
||||
|
||||
/** 收货入库:成品增加库存 + 写入采购入库流水;更新已收数量与状态 */
|
||||
public function receive($id)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !csrf_check()) return $this->redirect('PSI/purchase_orders/show/' . $id);
|
||||
$o = (new PurchaseOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
if ($o['status'] === 'received') { $this->flash('该采购订单已收货,不可重复操作', 'err'); return $this->redirect('PSI/purchase_orders/show/' . $id); }
|
||||
|
||||
$items = (new PurchaseOrderItem())->whereAll('po_id', $id);
|
||||
$purchaseM = new Purchase();
|
||||
foreach ($items as $it) {
|
||||
if ((int)$it['qty'] <= 0) continue;
|
||||
if ($it['item_type'] === 'product' && (int)$it['item_id'] > 0) {
|
||||
StockHelper::adjustStock((int)$it['item_id'], (int)$it['qty'], 'in', '采购入库', $o['order_no']);
|
||||
$purchaseM->insert([
|
||||
'product_id' => (int)$it['item_id'], 'qty' => (int)$it['qty'],
|
||||
'price' => (float)$it['price'], 'amount' => (float)$it['amount'],
|
||||
'supplier' => $o['supplier_name'], 'order_no' => $o['order_no'],
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
(new PurchaseOrderItem())->update($it['id'], ['received_qty' => (int)$it['qty']]);
|
||||
}
|
||||
(new PurchaseOrder())->update($id, ['status' => 'received']);
|
||||
$this->flash('采购订单已收货入库', 'ok');
|
||||
return $this->redirect('PSI/purchase_orders/show/' . $id);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_check()) {
|
||||
(new PurchaseOrderItem())->deleteRaw('po_id', $id);
|
||||
(new PurchaseOrder())->delete($id);
|
||||
$this->flash('采购订单已删除', 'ok');
|
||||
}
|
||||
return $this->redirect('PSI/purchase_orders');
|
||||
}
|
||||
|
||||
public function printDoc($id)
|
||||
{
|
||||
$o = (new PurchaseOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = (new PurchaseOrderItem())->whereAll('po_id', $id);
|
||||
$body = \App\Core\View::render('psi/purchase_order_print', ['o' => $o, 'items' => $items]);
|
||||
echo \psi_print_shell('采购订单 ' . $o['order_no'], $body);
|
||||
}
|
||||
|
||||
private function parseItems(): array
|
||||
{
|
||||
$raw = $_POST['items'] ?? [];
|
||||
$out = [];
|
||||
foreach ($raw as $row) {
|
||||
$name = trim((string)($row['name'] ?? ''));
|
||||
$qty = (int)($row['qty'] ?? 0);
|
||||
$price = (float)($row['price'] ?? 0);
|
||||
if ($name === '' || $qty <= 0) continue;
|
||||
$out[] = [
|
||||
'item_type' => $row['item_type'] === 'product' ? 'product' : 'material',
|
||||
'item_id' => (int)($row['item_id'] ?? 0),
|
||||
'name' => $name,
|
||||
'spec' => trim((string)($row['spec'] ?? '')),
|
||||
'unit' => trim((string)($row['unit'] ?? '')),
|
||||
'qty' => $qty,
|
||||
'price' => $price,
|
||||
'amount' => round($qty * $price, 2),
|
||||
'received_qty' => 0,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\Purchase;
|
||||
use App\Models\PSI\Supplier;
|
||||
|
||||
/** 采购入库:写入采购单并自动增加物料/成品库存 + 流水 */
|
||||
class PurchasesController extends Controller
|
||||
{
|
||||
use StockHelper;
|
||||
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$purchases = (new Purchase())->all();
|
||||
$suppliers = (new Supplier())->all();
|
||||
$smap = [];
|
||||
foreach ($suppliers as $s) { $smap[$s['id']] = $s['name']; }
|
||||
return $this->renderSubsys('psi', 'psi/purchases', ['purchases' => $purchases, 'smap' => $smap], \psi_nav(), 'purchases');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
$suppliers = (new Supplier())->all();
|
||||
$materials = (new \App\Models\PSI\Material())->all();
|
||||
$products = (new \App\Models\PSI\Product())->all();
|
||||
return $this->renderSubsys('psi', 'psi/purchase_form', [
|
||||
'purchase' => null, 'suppliers' => $suppliers,
|
||||
'materials' => $materials, 'products' => $products,
|
||||
], \psi_nav(), 'purchases');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if (!csrf_check()) return $this->redirect('PSI/purchases');
|
||||
$itemType = $this->post('item_type') === 'product' ? 'product' : 'material';
|
||||
$itemId = (int)$this->post('item_id');
|
||||
$qty = (float)$this->post('qty');
|
||||
$price = (float)$this->post('price');
|
||||
if ($itemId <= 0 || $qty <= 0) { $this->flash('请选择有效的物料/成品并填写数量'); return $this->redirect('PSI/purchases'); }
|
||||
$no = 'PI' . date('Ymd') . '-' . substr(uniqid(), -4);
|
||||
(new Purchase())->insert([
|
||||
'order_no' => $no,
|
||||
'supplier_id' => (int)$this->post('supplier_id'),
|
||||
'item_type' => $itemType,
|
||||
'item_id' => $itemId,
|
||||
'qty' => $qty,
|
||||
'price' => $price,
|
||||
'amount' => $qty * $price,
|
||||
'status' => 'stocked',
|
||||
'batch_no' => trim($this->post('batch_no')),
|
||||
'expected_at' => trim($this->post('expected_at')),
|
||||
'remark' => trim($this->post('remark')),
|
||||
'created_at' => date('Y-m-d'),
|
||||
]);
|
||||
$this->adjustStock($itemType, $itemId, $qty, 'in', $no, trim($this->post('batch_no')));
|
||||
$this->flash('采购入库成功,库存已更新', 'ok');
|
||||
return $this->redirect('PSI/purchases');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
(new Purchase())->delete($id);
|
||||
return $this->redirect('PSI/purchases');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use Core\Controller;
|
||||
use Core\Notify;
|
||||
use App\Models\PSI\Event;
|
||||
|
||||
/**
|
||||
* PSI 紧急事件 / 提醒中心
|
||||
* - 列出所有“紧急事件”(站内提醒,所有 PSI 人员可见)
|
||||
* - 支持单条标记已读 / 全部已读
|
||||
* - 支持手动发起一条紧急提醒(通知负责人)
|
||||
* 通过 PSI 仪表盘统一分发:PSI/reminders[/动作[/id]]
|
||||
*/
|
||||
class RemindersController extends Controller
|
||||
{
|
||||
/** 统一入口,由 DashboardController 分发 */
|
||||
public function handle(array $args = []): void
|
||||
{
|
||||
$action = $args[0] ?? 'index';
|
||||
$id = (int) ($args[1] ?? 0);
|
||||
|
||||
if ($action === 'create') {
|
||||
$this->create();
|
||||
return;
|
||||
}
|
||||
if ($action === 'mark' && $id > 0) {
|
||||
$this->markRead($id);
|
||||
return;
|
||||
}
|
||||
if ($action === 'markAll') {
|
||||
$this->markAll();
|
||||
return;
|
||||
}
|
||||
$this->index();
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
if (!subsys_user('psi')) { http_response_code(403); echo '无权限'; return; }
|
||||
|
||||
$events = (new Event())->all();
|
||||
$events = array_reverse($events); // 最新在前
|
||||
$uid = (int) ($_SESSION['admin_uid'] ?? 0);
|
||||
$unread = 0;
|
||||
foreach ($events as &$ev) {
|
||||
$read = json_decode($ev['read_by'] ?? '[]', true) ?: [];
|
||||
$ev['_read'] = in_array($uid, $read, true);
|
||||
if (!$ev['_read']) $unread++;
|
||||
}
|
||||
|
||||
$this->renderSubsys('psi', 'psi/reminders', [
|
||||
'events' => $events,
|
||||
'unread' => $unread,
|
||||
], \psi_nav(), 'reminders');
|
||||
}
|
||||
|
||||
public function markRead(int $id): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('PSI/reminders'); }
|
||||
if (!subsys_user('psi')) { http_response_code(403); return; }
|
||||
$uid = (int) ($_SESSION['admin_uid'] ?? 0);
|
||||
$ev = (new Event())->find($id);
|
||||
if ($ev) {
|
||||
$read = json_decode($ev['read_by'] ?? '[]', true) ?: [];
|
||||
if (!in_array($uid, $read, true)) {
|
||||
$read[] = $uid;
|
||||
(new Event())->update($id, ['read_by' => json_encode($read, JSON_UNESCAPED_UNICODE)]);
|
||||
}
|
||||
}
|
||||
$this->redirect('PSI/reminders');
|
||||
}
|
||||
|
||||
public function markAll(): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('PSI/reminders'); }
|
||||
if (!subsys_user('psi')) { http_response_code(403); return; }
|
||||
$uid = (int) ($_SESSION['admin_uid'] ?? 0);
|
||||
$events = (new Event())->all();
|
||||
foreach ($events as $ev) {
|
||||
$read = json_decode($ev['read_by'] ?? '[]', true) ?: [];
|
||||
if (!in_array($uid, $read, true)) {
|
||||
$read[] = $uid;
|
||||
(new Event())->update($ev['id'], ['read_by' => json_encode($read, JSON_UNESCAPED_UNICODE)]);
|
||||
}
|
||||
}
|
||||
$this->redirect('PSI/reminders');
|
||||
}
|
||||
|
||||
/** 手动发起一条紧急提醒(通知负责人) */
|
||||
public function create(): void
|
||||
{
|
||||
if (!subsys_user('psi')) { http_response_code(403); echo '无权限'; return; }
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!csrf_check()) { $this->flash('表单已过期,请重试', 'err'); $this->redirect('PSI/reminders'); }
|
||||
$title = trim($this->post('title', ''));
|
||||
$body = trim($this->post('body', ''));
|
||||
if ($title === '') { $this->flash('请填写提醒标题', 'err'); $this->redirect('PSI/reminders'); }
|
||||
Notify::fire('manual', $title, $body ?: $title, [
|
||||
'level' => 'urgent', 'url' => 'PSI/reminders',
|
||||
]);
|
||||
$this->flash('已发起紧急提醒,并已通知相关负责人', 'ok');
|
||||
$this->redirect('PSI/reminders');
|
||||
}
|
||||
|
||||
$this->renderSubsys('psi', 'psi/reminder_form', [
|
||||
'title' => '', 'body' => '',
|
||||
], \psi_nav(), 'reminders');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\PurchaseOrder;
|
||||
use App\Models\PSI\PurchaseOrderItem;
|
||||
use App\Models\PSI\SalesOrder;
|
||||
use App\Models\PSI\SalesOrderItem;
|
||||
use App\Models\PSI\Outbound;
|
||||
use App\Models\PSI\OutboundItem;
|
||||
|
||||
/**
|
||||
* 报表中心:采购订单明细 / 销售·采购订单明细 / 交付明细。
|
||||
* 集中展示单据之间的关联性(采购→入库、销售订单→出库交付)。
|
||||
*/
|
||||
class ReportsController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return $this->renderSubsys('psi', 'psi/reports', [
|
||||
'poCount' => count((new PurchaseOrder())->all()),
|
||||
'soCount' => count((new SalesOrder())->all()),
|
||||
'obCount' => count((new Outbound())->all()),
|
||||
], \psi_nav(), 'reports');
|
||||
}
|
||||
|
||||
/** 采购订单明细 */
|
||||
public function poDetail()
|
||||
{
|
||||
$orders = array_reverse((new PurchaseOrder())->all());
|
||||
$itemM = new PurchaseOrderItem();
|
||||
foreach ($orders as &$o) { $o['_items'] = $itemM->whereAll('po_id', $o['id']); }
|
||||
return $this->renderSubsys('psi', 'psi/report_po_detail', ['orders' => $orders], \psi_nav(), 'reports');
|
||||
}
|
||||
|
||||
/** 销售 / 采购订单明细(合并筛选) */
|
||||
public function soPo()
|
||||
{
|
||||
$type = $this->get('type', 'all');
|
||||
$so = $po = [];
|
||||
if ($type !== 'purchase') {
|
||||
$so = array_reverse((new SalesOrder())->all());
|
||||
$soItemM = new SalesOrderItem();
|
||||
foreach ($so as &$o) {
|
||||
$o['_items'] = $soItemM->whereAll('so_id', $o['id']);
|
||||
$o['_amt'] = array_sum(array_map(fn($i) => (float)($i['amount'] ?? 0), $o['_items']));
|
||||
}
|
||||
}
|
||||
if ($type !== 'sales') {
|
||||
$po = array_reverse((new PurchaseOrder())->all());
|
||||
$poItemM = new PurchaseOrderItem();
|
||||
foreach ($po as &$o) {
|
||||
$o['_items'] = $poItemM->whereAll('po_id', $o['id']);
|
||||
$o['_amt'] = array_sum(array_map(fn($i) => (float)($i['amount'] ?? 0), $o['_items']));
|
||||
}
|
||||
}
|
||||
return $this->renderSubsys('psi', 'psi/report_so_po', [
|
||||
'type' => $type, 'so' => $so, 'po' => $po,
|
||||
], \psi_nav(), 'reports');
|
||||
}
|
||||
|
||||
/** 交付明细:出库单 + 销售订单交付进度(应发/已发/未发) */
|
||||
public function delivery()
|
||||
{
|
||||
$list = array_reverse((new Outbound())->all());
|
||||
$obItemM = new OutboundItem();
|
||||
foreach ($list as &$o) { $o['_items'] = $obItemM->whereAll('ob_id', $o['id']); }
|
||||
|
||||
$soList = (new SalesOrder())->all();
|
||||
$soItemM = new SalesOrderItem();
|
||||
$progress = [];
|
||||
foreach ($soList as $so) {
|
||||
$items = $soItemM->whereAll('so_id', $so['id']);
|
||||
$ordered = array_sum(array_map(fn($i) => (int)($i['qty'] ?? 0), $items));
|
||||
$delivered = array_sum(array_map(fn($i) => (int)($i['delivered_qty'] ?? 0), $items));
|
||||
$amt = array_sum(array_map(fn($i) => (float)($i['amount'] ?? 0), $items));
|
||||
if ($ordered <= 0) continue;
|
||||
$progress[] = [
|
||||
'order_no' => $so['order_no'],
|
||||
'customer' => $so['customer'],
|
||||
'salesman' => $so['salesman'],
|
||||
'status' => $so['status'],
|
||||
'ordered' => $ordered,
|
||||
'delivered' => $delivered,
|
||||
'remain' => max(0, $ordered - $delivered),
|
||||
'amount' => $amt,
|
||||
];
|
||||
}
|
||||
return $this->renderSubsys('psi', 'psi/report_delivery', [
|
||||
'list' => $list, 'progress' => $progress,
|
||||
], \psi_nav(), 'reports');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\Sales;
|
||||
use App\Models\PSI\Product;
|
||||
use App\Models\PSI\Material;
|
||||
|
||||
/** 销售出库:内销 + 外贸出口。出库自动扣减库存并校验库存充足 */
|
||||
class SalesController extends Controller
|
||||
{
|
||||
use StockHelper;
|
||||
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$sales = (new Sales())->all();
|
||||
return $this->renderSubsys('psi', 'psi/sales', ['sales' => $sales], \psi_nav(), 'sales');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
$materials = (new \App\Models\PSI\Material())->all();
|
||||
$products = (new \App\Models\PSI\Product())->all();
|
||||
return $this->renderSubsys('psi', 'psi/sale_form', [
|
||||
'sale' => null, 'materials' => $materials, 'products' => $products,
|
||||
], \psi_nav(), 'sales');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if (!csrf_check()) return $this->redirect('PSI/sales');
|
||||
$itemType = $this->post('item_type') === 'material' ? 'material' : 'product';
|
||||
$itemId = (int)$this->post('item_id');
|
||||
$qty = (float)$this->post('qty');
|
||||
$price = (float)$this->post('price');
|
||||
if ($itemId <= 0 || $qty <= 0) { $this->flash('请选择有效的成品/物料并填写数量'); return $this->redirect('PSI/sales'); }
|
||||
$model = $itemType === 'product' ? new Product() : new Material();
|
||||
$item = $model->find($itemId);
|
||||
if (!$item || (float)($item['stock'] ?? 0) < $qty) {
|
||||
$this->flash('库存不足,无法出库(当前库存:' . (isset($item) ? (float)$item['stock'] : 0) . ')');
|
||||
return $this->redirect('PSI/sales');
|
||||
}
|
||||
$no = 'SO' . date('Ymd') . '-' . substr(uniqid(), -4);
|
||||
$saleId = (new Sales())->insert([
|
||||
'order_no' => $no,
|
||||
'customer' => trim($this->post('customer')),
|
||||
'channel' => $this->post('channel') === 'export' ? 'export' : 'domestic',
|
||||
'region' => trim($this->post('region')),
|
||||
'item_id' => $itemId,
|
||||
'item_type' => $itemType,
|
||||
'qty' => $qty,
|
||||
'price' => $price,
|
||||
'amount' => $qty * $price,
|
||||
'status' => 'shipped',
|
||||
'batch_no' => trim($this->post('batch_no')),
|
||||
'remark' => trim($this->post('remark')),
|
||||
'created_at' => date('Y-m-d'),
|
||||
]);
|
||||
$this->adjustStock($itemType, $itemId, $qty, 'out', $no, trim($this->post('batch_no')));
|
||||
$this->flash('销售出库成功,库存已扣减', 'ok');
|
||||
if ($this->post('auto_print', '1') !== '0') {
|
||||
return $this->redirect('PSI/sales/print/' . $saleId);
|
||||
}
|
||||
return $this->redirect('PSI/sales');
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$sale = (new Sales())->find($id);
|
||||
if (!$sale) { \Core\App::notFound('销售记录不存在'); return; }
|
||||
$itemInfo = $this->lookupItem($sale['item_id'] ?? 0, $sale['item_type'] ?? '');
|
||||
return $this->renderSubsys('psi', 'psi/sales_show', [
|
||||
's' => $sale,
|
||||
'item' => $itemInfo,
|
||||
], \psi_nav(), 'sales');
|
||||
}
|
||||
|
||||
public function printDoc($id)
|
||||
{
|
||||
$sale = (new Sales())->find($id);
|
||||
if (!$sale) { \Core\App::notFound('销售记录不存在'); return; }
|
||||
$itemInfo = $this->lookupItem($sale['item_id'] ?? 0, $sale['item_type'] ?? '');
|
||||
$body = \Core\View::buffer('psi/sales_print', ['s' => $sale, 'item' => $itemInfo]);
|
||||
echo \psi_print_shell('销售出库单 - ' . e($sale['order_no']), $body);
|
||||
}
|
||||
|
||||
/** 尝试从成品表或物料表查找物品信息 */
|
||||
private function lookupItem(int $itemId, string $itemType = ''): array
|
||||
{
|
||||
if ($itemId <= 0) return ['name' => '—', 'spec' => '', 'unit' => ''];
|
||||
if ($itemType === 'material') {
|
||||
$row = (new Material())->find($itemId);
|
||||
} else {
|
||||
$row = (new Product())->find($itemId) ?: (new Material())->find($itemId);
|
||||
}
|
||||
if ($row) return ['name' => $row['name'] ?? '—', 'spec' => $row['spec'] ?? '', 'unit' => $row['unit'] ?? ''];
|
||||
return ['name' => '—', 'spec' => '', 'unit' => ''];
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
(new Sales())->delete($id);
|
||||
return $this->redirect('PSI/sales');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\SalesOrder;
|
||||
use App\Models\PSI\SalesOrderItem;
|
||||
use App\Models\PSI\Product;
|
||||
|
||||
/**
|
||||
* 销售订单:录入(可打印)、关联出库。
|
||||
* 业务员默认取当前登录人;商品可关联 psi_products(用于后续出库扣减库存)。
|
||||
*/
|
||||
class SalesOrdersController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$orders = (new SalesOrder())->all(); // id 升序
|
||||
$orders = array_reverse($orders); // 最新在前
|
||||
$itemM = new SalesOrderItem();
|
||||
foreach ($orders as &$o) {
|
||||
$o['_items'] = $itemM->whereAll('so_id', $o['id']);
|
||||
}
|
||||
return $this->renderSubsys('psi', 'psi/sales_orders', ['orders' => $orders], \psi_nav(), 'sales_orders');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$products = (new Product())->all();
|
||||
return $this->renderSubsys('psi', 'psi/sales_order_form', [
|
||||
'o' => null, 'items' => [], 'products' => $products,
|
||||
], \psi_nav(), 'sales_orders');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !csrf_check()) return $this->redirect('PSI/sales_orders/create');
|
||||
$items = $this->parseItems();
|
||||
if (empty($items)) { $this->flash('请至少添加一条商品明细', 'err'); return $this->redirect('PSI/sales_orders/create'); }
|
||||
$id = (new SalesOrder())->insert([
|
||||
'order_no' => $oNo = \psi_gen_no('SO'),
|
||||
'customer' => $cust = trim($this->post('customer')),
|
||||
'salesman' => $sales = trim($this->post('salesman')) ?: ($_SESSION['admin_name'] ?? ''),
|
||||
'channel' => $this->post('channel') === 'export' ? 'export' : 'domestic',
|
||||
'region' => trim($this->post('region')),
|
||||
'delivery_date' => $this->post('delivery_date') ?: null,
|
||||
'remark' => trim($this->post('remark')),
|
||||
'status' => 'pending',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$itemM = new SalesOrderItem();
|
||||
foreach ($items as $it) { $it['so_id'] = $id; $itemM->insert($it); }
|
||||
\Core\Notify::newSalesOrder($oNo, $cust, $sales, $id);
|
||||
$this->flash('销售订单已创建', 'ok');
|
||||
if ($this->post('auto_print', '1') !== '0') {
|
||||
return $this->redirect('PSI/sales_orders/print/' . $id);
|
||||
}
|
||||
return $this->redirect('PSI/sales_orders/show/' . $id);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$o = (new SalesOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = (new SalesOrderItem())->whereAll('so_id', $id);
|
||||
return $this->renderSubsys('psi', 'psi/sales_order_show', [
|
||||
'o' => $o, 'items' => $items,
|
||||
], \psi_nav(), 'sales_orders');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$o = (new SalesOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = (new SalesOrderItem())->whereAll('so_id', $id);
|
||||
$products = (new Product())->all();
|
||||
return $this->renderSubsys('psi', 'psi/sales_order_form', [
|
||||
'o' => $o, 'items' => $items, 'products' => $products,
|
||||
], \psi_nav(), 'sales_orders');
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !csrf_check()) return $this->redirect('PSI/sales_orders/edit/' . $id);
|
||||
$o = (new SalesOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = $this->parseItems();
|
||||
if (empty($items)) { $this->flash('请至少添加一条商品明细', 'err'); return $this->redirect('PSI/sales_orders/edit/' . $id); }
|
||||
|
||||
(new SalesOrder())->update($id, [
|
||||
'customer' => trim($this->post('customer')),
|
||||
'salesman' => trim($this->post('salesman')) ?: ($_SESSION['admin_name'] ?? ''),
|
||||
'channel' => $this->post('channel') === 'export' ? 'export' : 'domestic',
|
||||
'region' => trim($this->post('region')),
|
||||
'delivery_date' => $this->post('delivery_date') ?: null,
|
||||
'remark' => trim($this->post('remark')),
|
||||
]);
|
||||
|
||||
// 保留已交付数量(delivered_qty)避免覆盖出库记录
|
||||
$old = (new SalesOrderItem())->whereAll('so_id', $id);
|
||||
$delivered = [];
|
||||
foreach ($old as $oi) { $delivered[($oi['product_id'] ?? 0) . '|' . $oi['name']] = (int)($oi['delivered_qty'] ?? 0); }
|
||||
(new SalesOrderItem())->deleteRaw('so_id', $id);
|
||||
foreach ($items as $it) {
|
||||
$key = ($it['product_id'] ?? 0) . '|' . $it['name'];
|
||||
$it['delivered_qty'] = $delivered[$key] ?? 0;
|
||||
$it['so_id'] = $id;
|
||||
(new SalesOrderItem())->insert($it);
|
||||
}
|
||||
\psi_recompute_so($id);
|
||||
$this->flash('销售订单已更新', 'ok');
|
||||
return $this->redirect('PSI/sales_orders/show/' . $id);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_check()) {
|
||||
(new SalesOrderItem())->deleteRaw('so_id', $id);
|
||||
(new SalesOrder())->delete($id);
|
||||
$this->flash('销售订单已删除', 'ok');
|
||||
}
|
||||
return $this->redirect('PSI/sales_orders');
|
||||
}
|
||||
|
||||
public function printDoc($id)
|
||||
{
|
||||
$o = (new SalesOrder())->find($id);
|
||||
if (!$o) { \Core\App::notFound(); return; }
|
||||
$items = (new SalesOrderItem())->whereAll('so_id', $id);
|
||||
$body = \App\Core\View::render('psi/sales_order_print', ['o' => $o, 'items' => $items]);
|
||||
echo \psi_print_shell('销售订单 ' . $o['order_no'], $body);
|
||||
}
|
||||
|
||||
/** 解析提交的商品明细行 */
|
||||
private function parseItems(): array
|
||||
{
|
||||
$raw = $_POST['items'] ?? [];
|
||||
$out = [];
|
||||
foreach ($raw as $row) {
|
||||
$name = trim((string)($row['name'] ?? ''));
|
||||
$qty = (int)($row['qty'] ?? 0);
|
||||
$price = (float)($row['price'] ?? 0);
|
||||
if ($name === '' || $qty <= 0) continue;
|
||||
$out[] = [
|
||||
'product_id' => (int)($row['product_id'] ?? 0),
|
||||
'name' => $name,
|
||||
'spec' => trim((string)($row['spec'] ?? '')),
|
||||
'unit' => trim((string)($row['unit'] ?? '')),
|
||||
'qty' => $qty,
|
||||
'price' => $price,
|
||||
'amount' => round($qty * $price, 2),
|
||||
'delivered_qty' => 0,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\StockMove;
|
||||
use App\Models\PSI\Material;
|
||||
use App\Models\PSI\Product;
|
||||
|
||||
/** 库存流水台账(所有出入库变动记录) */
|
||||
class StockController extends Controller
|
||||
{
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$moves = (new StockMove())->all();
|
||||
$moves = array_reverse($moves);
|
||||
$nameMap = [];
|
||||
foreach ((new Material())->all() as $m) { $nameMap['material:' . $m['id']] = $m['name']; }
|
||||
foreach ((new Product())->all() as $p) { $nameMap['product:' . $p['id']] = $p['name']; }
|
||||
return $this->renderSubsys('psi', 'psi/stock', ['moves' => $moves, 'nameMap' => $nameMap], \psi_nav(), 'stock');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Models\PSI\Material;
|
||||
use App\Models\PSI\Product;
|
||||
use App\Models\PSI\StockMove;
|
||||
|
||||
/**
|
||||
* 库存台账统一维护:任何出入库都经此更新,保证物料/成品库存与流水一致。
|
||||
* item_type: material | product
|
||||
*/
|
||||
trait StockHelper
|
||||
{
|
||||
private function adjustStock(string $itemType, int $itemId, float $qty, string $direction, string $refNo, string $batchNo = ''): void
|
||||
{
|
||||
$model = $itemType === 'product' ? new Product() : new Material();
|
||||
$item = $model->find($itemId);
|
||||
if (!$item) return;
|
||||
$cur = (float)($item['stock'] ?? 0);
|
||||
$new = $direction === 'in' ? $cur + $qty : max(0, $cur - $qty);
|
||||
$model->update($itemId, ['stock' => $new]);
|
||||
(new StockMove())->insert([
|
||||
'item_type' => $itemType,
|
||||
'item_id' => $itemId,
|
||||
'direction' => $direction,
|
||||
'qty' => $qty,
|
||||
'ref_no' => $refNo,
|
||||
'batch_no' => $batchNo,
|
||||
'remark' => '',
|
||||
'created_at' => date('Y-m-d'),
|
||||
]);
|
||||
|
||||
// 低库存预警:仅当出库且跌破阈值时触发(避免重复骚扰)
|
||||
$threshold = \Core\Notify::lowStockThreshold();
|
||||
if (\Core\Notify::lowStockEnabled()
|
||||
&& $direction === 'out'
|
||||
&& $new <= $threshold
|
||||
&& $cur > $threshold) {
|
||||
\Core\Notify::lowStockEvent($itemType, $item['name'] ?? '', $new, $threshold, $itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Models\PSI\Supplier;
|
||||
|
||||
/** 供应商管理(进出口/面辅料供应商) */
|
||||
class SuppliersController extends Controller
|
||||
{
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$suppliers = (new Supplier())->all();
|
||||
return $this->renderSubsys('psi', 'psi/suppliers', ['suppliers' => $suppliers], \psi_nav(), 'suppliers');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
return $this->renderSubsys('psi', 'psi/supplier_form', ['supplier' => null], \psi_nav(), 'suppliers');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if (!csrf_check()) return $this->redirect('PSI/suppliers');
|
||||
(new Supplier())->insert($this->collect());
|
||||
return $this->redirect('PSI/suppliers');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
$supplier = (new Supplier())->find($id);
|
||||
if (!$supplier) return $this->redirect('PSI/suppliers');
|
||||
return $this->renderSubsys('psi', 'psi/supplier_form', ['supplier' => $supplier], \psi_nav(), 'suppliers');
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
if (!csrf_check()) return $this->redirect('PSI/suppliers');
|
||||
$supplier = (new Supplier())->find($id);
|
||||
if (!$supplier) return $this->redirect('PSI/suppliers');
|
||||
(new Supplier())->update($id, $this->collect());
|
||||
return $this->redirect('PSI/suppliers');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
subsys_admin('psi') or \Core\App::forbidden('需要 PSI 管理员权限');
|
||||
(new Supplier())->delete($id);
|
||||
return $this->redirect('PSI/suppliers');
|
||||
}
|
||||
|
||||
private function collect(): array
|
||||
{
|
||||
return [
|
||||
'name' => trim($this->post('name')),
|
||||
'contact' => trim($this->post('contact')),
|
||||
'phone' => trim($this->post('phone')),
|
||||
'country' => trim($this->post('country')),
|
||||
'type' => trim($this->post('type')),
|
||||
'grade' => trim($this->post('grade')),
|
||||
'ontime_rate'=> (float)$this->post('ontime_rate'),
|
||||
'qc_rate' => (float)$this->post('qc_rate'),
|
||||
'remark' => trim($this->post('remark')),
|
||||
'created_at' => date('Y-m-d'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace App\Controllers\PSI;
|
||||
|
||||
use App\Controllers\Subsys\UsersController as BaseUsersController;
|
||||
|
||||
class UsersController extends BaseUsersController
|
||||
{
|
||||
protected function sys(): string
|
||||
{
|
||||
return 'psi';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user