Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions data/samples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 样例导入表格

这里的表格演示从 ERP 导出数据、用平台导入写入 SRM 的格式。全部是标明「示例」的虚构数据,
不对应任何真实企业或物料清单(AGENTS.md 红线 5)。CI 用它们验证导入可重复执行(`test/material.test.ts`)。

## 物料 `srm_material.csv`

物料是 ERP 物料主数据的只读副本([02 §4.1](../../docs/design/02-domain-model.md#41-供应商主数据),
[ADR-0008](../../docs/adr/0008-order-to-pay-stays-in-erp.md))。ERP 是源,SRM 不新建物料。

| 列 | 字段 | 说明 |
|---|---|---|
| `erp_material_no` | ERP 物料号 | 必填,组织内唯一;导入按它匹配,已有则更新、没有则新建 |
| `material_code` | 物料编码 | 必填 |
| `name` | 物料名称 | 必填 |
| `specification` | 规格型号 | |
| `uom` | 计量单位 | 文本,如「个」「卷」 |
| `active` | 启用 | `true` / `false`;ERP 中停用的物料导入为 `false`,不删除 |

### 导入步骤

1. 从 ERP 导出物料主数据,把列名改成上表的字段名(或在导入界面逐列映射)。
2. 以 `srm_admin` 岗位登录,打开「基础数据 › 物料」,选择导入,写入方式选 **upsert**,
匹配字段选 **ERP 物料号**(`erp_material_no`)。
3. 先试运行(接口参数 `dryRun: true`)看预览,没有错误再正式导入。

接口方式等价于:

```http
POST /api/v1/data/srm_material/import
{ "format": "csv", "csv": "<表格内容>", "writeMode": "upsert", "matchFields": ["erp_material_no"] }
```

### 注意

- **日常重复导入不要带 `category` 列**。品类关联由采购员在 SRM 里维护,表格里带这一列会把它覆盖。
首次初始化时如果 ERP 的物料组能对上 SRM 品类,可以带上,值填品类名称。
- 同一张表格导入两遍,第二遍只更新、不新增,记录数与字段值不变。
- 未关联品类的物料在「物料 › 未关联品类」视图里,供采购员补齐。
9 changes: 9 additions & 0 deletions data/samples/srm_material.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
erp_material_no,material_code,name,specification,uom,active
EX-100001,RES-0603-10K,示例贴片电阻,0603 10kΩ ±1%,个,true
EX-100002,CAP-0805-100N,示例贴片电容,0805 100nF 50V X7R,个,true
EX-100003,IC-MCU-32F1,示例微控制器,"32 位, LQFP-48",个,true
EX-100004,CON-USB-C16,示例 USB-C 连接器,16 针 贴片,个,true
EX-200001,BOX-300-200,示例瓦楞纸箱,300×200×150 mm 五层,只,true
EX-200002,FILM-PE-50,示例 PE 缠绕膜,宽 500 mm 厚 50 μm,卷,true
EX-300001,BRG-6204,示例深沟球轴承,6204-2RS,套,true
EX-300002,GLV-NIT-L,示例丁腈手套,L 号 100 只/盒,盒,false
15 changes: 12 additions & 3 deletions docs/design/02-domain-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,19 @@ erDiagram
(lookup)、`status`(`trial` 试供 / `qualified` 合格 / `suspended` 暂停)、
`approved_date`、`source_application`(lookup 准入申请)。同一供应商同一品类唯一。

**`srm_material` 物料**(只读副本,OWD `public_read`;v0.2):`erp_material_no`
(唯一,导入匹配键)、`material_code`、`name`、`specification`、`uom`、`category`(lookup)、
**`srm_material` 物料**(只读副本,OWD `public_read`,暂用 `public_read_write`,见下方说明;v0.2):`erp_material_no`
(组织内唯一,导入匹配键)、`material_code`、`name`、`specification`、`uom`、`category`(lookup)、
`active`。用平台导入按 `erp_material_no` 从 ERP 导出表格导入,可重复执行;采购员只
维护 `category` 关联,其他字段对内部只读(FLS)。ERP 集成后改为 upsert 写入,结构不变。
维护 `category` 关联,其他字段对采购员只读(FLS),新建、删除只有 `srm_admin`。ERP 集成后改为
upsert 写入,结构不变。

> **临时做法**:目标 OWD 是 `public_read`,现阶段先用 `public_read_write`。物料归执行导入的管理员
> 所有,`public_read` 下采购员要改别人的记录只能授予 `modifyAllRecords`,而平台的
> `modifyAllRecords` 同时放开删除(平台需求 [objectstack#25470](https://github.com/objectstack-ai/objectstack/issues/25470))。
> 用 `public_read_write` 时,能写哪些字段由权限集的 `allowEdit` 与 FLS 决定,删除仍受 `allowDelete`
> 约束;代价是今后任何授予 `allowEdit` 的权限集都能改全部物料。平台支持「可改不可删」后改回
> `public_read`。日常重复导入的表格**不带** `category` 列,以免覆盖采购员维护的关联;导入说明见
> [`data/samples/README.md`](../../data/samples/README.md)。

**`srm_plant` 工厂 / 收货地点**(手工维护或导入):`erp_plant_id`(预留)、`code`、`name`、
`legal_entity`(法人名称或组织节点)、`address`。
Expand Down
4 changes: 2 additions & 2 deletions docs/design/04-security-and-portal.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ SRM 同时服务内部员工和互相竞争的供应商,权限设计的首要
| `srm_internal_base` | 内部员工的基础:读品类、物料、工厂、供应商主数据(不含银行账号)、价格库;**不是** `isDefault` |
| `srm_sourcing` | 寻源单、寻源明细的增改(状态只经 screen flow 推进);邀请、技术评分、定标只经 screen flow;读报价(受封标 RLS 约束);新建合同 |
| `srm_supplier_admin` | 供应商档案、联系人、证书、准入申请、审核、供货品类资格、处置单、合同的增改;开通门户 |
| `srm_buyer` | 供应商档案、合同、交付记录读;代供应商发起变更单(v0.6 前);ERP 集成后加订单、送货单的内部字段 |
| `srm_buyer` | 供应商档案、合同、交付记录读;物料只改 `category` 关联(其余字段 FLS 只读,不新建、不删除);代供应商发起变更单(v0.6 前);ERP 集成后加订单、送货单的内部字段 |
| `srm_quality` | 审核、交付记录(录入)、整改单的增改;证书只读,核验经 screen flow |
| `srm_iqc` | 交付记录的录入(没有质量系统的客户) |
| `srm_finance` | 银行账号可读,核验经 screen flow;银行账户变更的第 2 级审批;整改单索赔结论标记 |
| `srm_legal` | 合同读改(审批由 flow 授予) |
| `srm_executive` | 全部 SRM 对象只读 + 仪表盘;审批由 flow 授予,不需要额外写权限 |
| `srm_auditor` | 全部 SRM 对象只读(报价同样受封标约束),可读审批历史与字段历史,不可导出银行账号 |
| `srm_admin` | 品类、指标库、编码、通知模板的配置;不授予报价读权限 |
| `srm_admin` | 品类、指标库、编码、通知模板的配置;物料导入与维护;不授予报价读权限 |
| `srm_portal_supplier` | 供应商门户用户:见 §3.2 |
| `srm_portal_supplier_admin` | 在 `srm_portal_supplier` 之上:维护本公司联系人、银行账户,提交变更申请与绩效申诉;直接授予具体用户 |
| `srm_integration` | 预留(ERP 集成后):集成服务账号,物料、交付记录、订单镜像的写入;供应商仅可写 `erp_vendor_code`,价格记录只读;**不**授予报价单与银行账号 |
Expand Down
1 change: 1 addition & 0 deletions src/core/apps/workbench.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const Workbench = App.create({
icon: 'database',
children: [
{ id: 'nav_category', type: 'object', objectName: 'srm_category', label: '采购品类', icon: 'folder-tree' },
{ id: 'nav_material', type: 'object', objectName: 'srm_material', label: '物料', icon: 'package' },
{ id: 'nav_plant', type: 'object', objectName: 'srm_plant', label: '工厂', icon: 'warehouse' },
],
},
Expand Down
1 change: 1 addition & 0 deletions src/core/objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { SupplierContact } from './supplier-contact.object.js';
export { SupplierCertificate } from './supplier-certificate.object.js';
export { SupplierCategory } from './supplier-category.object.js';
export { SupplierApplication } from './supplier-application.object.js';
export { Material } from './material.object.js';
28 changes: 28 additions & 0 deletions src/core/objects/material.object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ObjectSchema, Field } from '@objectstack/spec/data';

// 物料(02 §4.1):ERP 物料主数据的只读副本,用平台导入按 erp_material_no 幂等写入(ADR-0008)。
// SRM 内只维护与品类的关联;其余字段对采购员只读,见权限集 srm_buyer。
export const Material = ObjectSchema.create({
name: 'srm_material',
label: '物料',
pluralLabel: '物料',
icon: 'package',
// 临时:目标是 public_read。平台的 modifyAllRecords 会连删除一起放开,采购员「可改不可删」
// 只能先放宽 OWD(objectstack#25470,02 §4.1)。
sharingModel: 'public_read_write',

fieldGroups: [
{ key: 'basic', label: '基本信息', icon: 'package' },
{ key: 'external', label: '外部系统', icon: 'link' },
],

fields: {
material_code: Field.text({ label: '物料编码', group: 'basic', required: true, searchable: true, maxLength: 60 }),
name: Field.text({ label: '物料名称', group: 'basic', required: true, searchable: true, maxLength: 200 }),
specification: Field.text({ label: '规格型号', group: 'basic', searchable: true, maxLength: 500 }),
uom: Field.text({ label: '计量单位', group: 'basic', maxLength: 20 }),
category: Field.lookup('srm_category', { label: '采购品类', group: 'basic' }),
active: Field.boolean({ label: '启用', group: 'basic', defaultValue: true }),
erp_material_no: Field.text({ label: 'ERP 物料号', group: 'external', required: true, unique: true, maxLength: 60 }),
},
});
24 changes: 23 additions & 1 deletion src/core/security/permission-sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const read = { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false, viewAllRecords: true, modifyAllRecords: false };
const write = { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false };
const flowOwned = { readable: true, editable: false };
const readOnly = { readable: true, editable: false };

// 内部员工的基础:读基础数据与供应商主数据
export const InternalBasePermissionSet = {
Expand All @@ -12,13 +13,31 @@ export const InternalBasePermissionSet = {
objects: {
srm_category: read,
srm_plant: read,
srm_material: read,
srm_supplier: read,
srm_supplier_contact: read,
srm_supplier_certificate: read,
srm_supplier_category: read,
},
};

// 采购员:物料是 ERP 的只读副本(ADR-0008),采购员只维护物料与品类的关联,不新建、不删除
export const BuyerPermissionSet = {
name: 'srm_buyer',
label: 'SRM 采购',
objects: {
srm_material: { ...read, allowEdit: true },
},
fields: {
'srm_material.erp_material_no': readOnly,
'srm_material.material_code': readOnly,
'srm_material.name': readOnly,
'srm_material.specification': readOnly,
'srm_material.uom': readOnly,
'srm_material.active': readOnly,
},
};

// 品类经理等:维护供应商档案与子信息,发起和推进准入申请
export const SupplierAdminPermissionSet = {
name: 'srm_supplier_admin',
Expand Down Expand Up @@ -62,6 +81,7 @@ export const ExecutivePermissionSet = {
objects: {
srm_category: read,
srm_plant: read,
srm_material: read,
srm_supplier: read,
srm_supplier_contact: read,
srm_supplier_certificate: read,
Expand All @@ -81,18 +101,20 @@ export const FinancePermissionSet = {
},
};

// 系统管理员:维护品类与工厂等配置
// 系统管理员:维护品类与工厂等配置,执行物料导入
export const AdminPermissionSet = {
name: 'srm_admin',
label: 'SRM 系统管理员',
objects: {
srm_category: { ...write, allowDelete: true },
srm_plant: { ...write, allowDelete: true },
srm_material: { ...write, allowDelete: true },
},
};

export const SrmPermissionSets = [
InternalBasePermissionSet,
BuyerPermissionSet,
SupplierAdminPermissionSet,
QualityPermissionSet,
ExecutivePermissionSet,
Expand Down
8 changes: 4 additions & 4 deletions src/core/security/positions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// 内部岗位(04 §2.1)的子集:只列已上线流程用到的岗位,其余随对应版本加入。
// 挂载的权限集同样是子集:`srm_buyer`、`srm_sourcing` 等权限集随其对象上线后补挂。
// 挂载的权限集同样是子集:`srm_sourcing` 等权限集随其对象上线后补挂。
export const SrmPositions = [
{ name: 'srm_procurement_director', label: '采购总监', permissionSets: ['srm_internal_base', 'srm_executive'] },
{ name: 'srm_procurement_director', label: '采购总监', permissionSets: ['srm_internal_base', 'srm_buyer', 'srm_executive'] },
{ name: 'srm_category_manager', label: '品类经理', permissionSets: ['srm_internal_base', 'srm_supplier_admin'] },
{ name: 'srm_buyer', label: '采购员', permissionSets: ['srm_internal_base'] },
{ name: 'srm_buyer_lead', label: '采购主管', permissionSets: ['srm_internal_base', 'srm_supplier_admin'] },
{ name: 'srm_buyer', label: '采购员', permissionSets: ['srm_internal_base', 'srm_buyer'] },
{ name: 'srm_buyer_lead', label: '采购主管', permissionSets: ['srm_internal_base', 'srm_buyer', 'srm_supplier_admin'] },
{ name: 'srm_sqe', label: 'SQE', permissionSets: ['srm_internal_base', 'srm_quality'] },
{ name: 'srm_ap_accountant', label: '应付会计', permissionSets: ['srm_internal_base', 'srm_finance'] },
{ name: 'srm_admin', label: 'SRM 系统管理员', permissionSets: ['srm_internal_base', 'srm_admin'] },
Expand Down
19 changes: 19 additions & 0 deletions src/core/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ export const en: TranslationData = {
},
_views: { default: { label: 'All Plants' } },
},
srm_material: {
label: 'Material',
pluralLabel: 'Materials',
_sections: { basic: { label: 'Basic Information' }, external: { label: 'External Systems' } },
fields: {
material_code: { label: 'Material Code' },
name: { label: 'Material Name' },
specification: { label: 'Specification' },
uom: { label: 'Unit of Measure' },
category: { label: 'Purchasing Category' },
active: { label: 'Active' },
erp_material_no: { label: 'ERP Material No.' },
},
_views: {
default: { label: 'All Materials' },
uncategorized: { label: 'Without Category' },
},
},
srm_supplier: {
label: 'Supplier',
pluralLabel: 'Suppliers',
Expand Down Expand Up @@ -217,6 +235,7 @@ export const en: TranslationData = {
nav_certificate: { label: 'Certificates' },
group_master_data: { label: 'Master Data' },
nav_category: { label: 'Purchasing Categories' },
nav_material: { label: 'Materials' },
nav_plant: { label: 'Plants' },
},
},
Expand Down
1 change: 1 addition & 0 deletions src/core/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { PlantViews } from './plant.view.js';
export { SupplierViews } from './supplier.view.js';
export { SupplierCertificateViews } from './supplier-certificate.view.js';
export { SupplierApplicationViews } from './supplier-application.view.js';
export { MaterialViews } from './material.view.js';
30 changes: 30 additions & 0 deletions src/core/views/material.view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineView } from '@objectstack/spec';

const data = { provider: 'object' as const, object: 'srm_material' };

export const MaterialViews = defineView({
list: {
label: '全部物料',
type: 'grid',
data,
columns: [
{ field: 'material_code' },
{ field: 'name' },
{ field: 'specification' },
{ field: 'uom' },
{ field: 'category' },
{ field: 'active' },
],
sort: [{ field: 'material_code', order: 'asc' }],
},

listViews: {
uncategorized: {
label: '未关联品类',
type: 'grid',
data,
columns: [{ field: 'material_code' }, { field: 'name' }, { field: 'specification' }, { field: 'uom' }],
filter: [{ field: 'category', operator: 'is_null' }],
},
},
});
Loading
Loading