2026-02-11 15:12:20 +08:00
|
|
|
|
# inventory-backend/app/services/inbound/service_service.py
|
|
|
|
|
|
from app.extensions import db
|
2026-02-09 11:29:37 +08:00
|
|
|
|
from app.models.inbound.service import StockService
|
2026-02-09 11:50:03 +08:00
|
|
|
|
from app.models.base import MaterialBase
|
2026-02-09 11:29:37 +08:00
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
import re
|
2026-02-11 14:55:28 +08:00
|
|
|
|
import traceback
|
2026-02-09 11:29:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ServiceService:
|
|
|
|
|
|
"""服务权益库存业务逻辑"""
|
|
|
|
|
|
|
|
|
|
|
|
SKU_PREFIX = 'SRV'
|
|
|
|
|
|
SKU_DATE_FORMAT = '%Y%m%d'
|
|
|
|
|
|
SKU_SUFFIX_LEN = 4
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _generate_sku(cls):
|
|
|
|
|
|
"""生成唯一SKU,格式 SRV-YYYYMMDD-XXXX"""
|
|
|
|
|
|
today_str = datetime.now().strftime(cls.SKU_DATE_FORMAT)
|
|
|
|
|
|
prefix = f'{cls.SKU_PREFIX}-{today_str}-'
|
2026-02-11 15:12:20 +08:00
|
|
|
|
|
2026-02-09 11:29:37 +08:00
|
|
|
|
# 查找今天已有的最大后缀
|
|
|
|
|
|
max_sku = db.session.query(db.func.max(StockService.sku)).filter(
|
|
|
|
|
|
StockService.sku.like(f'{prefix}%')
|
|
|
|
|
|
).scalar()
|
2026-02-11 15:12:20 +08:00
|
|
|
|
|
2026-02-09 11:29:37 +08:00
|
|
|
|
if not max_sku:
|
|
|
|
|
|
suffix_num = 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 提取后缀数字
|
|
|
|
|
|
suffix_part = max_sku.replace(prefix, '')
|
2026-02-11 15:12:20 +08:00
|
|
|
|
try:
|
|
|
|
|
|
match = re.search(r'(\d+)$', suffix_part)
|
|
|
|
|
|
suffix_num = int(match.group(1)) if match else 0
|
|
|
|
|
|
except:
|
|
|
|
|
|
suffix_num = 0
|
2026-02-09 11:29:37 +08:00
|
|
|
|
suffix_num += 1
|
2026-02-11 15:12:20 +08:00
|
|
|
|
|
2026-02-09 11:29:37 +08:00
|
|
|
|
# 格式化为4位数字,左侧补零
|
|
|
|
|
|
suffix = str(suffix_num).zfill(cls.SKU_SUFFIX_LEN)
|
|
|
|
|
|
return f'{prefix}{suffix}'
|
|
|
|
|
|
|
2026-02-09 12:11:01 +08:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def search_base_material(cls, keyword):
|
|
|
|
|
|
"""搜索基础物料,供前端远程选择"""
|
|
|
|
|
|
try:
|
2026-02-11 15:12:20 +08:00
|
|
|
|
# 只查询已启用的物料
|
2026-02-09 12:11:01 +08:00
|
|
|
|
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
2026-02-10 13:50:26 +08:00
|
|
|
|
|
2026-02-09 12:11:01 +08:00
|
|
|
|
if keyword:
|
|
|
|
|
|
query = query.filter(
|
|
|
|
|
|
db.or_(
|
|
|
|
|
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
|
|
|
|
|
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
2026-02-11 15:12:20 +08:00
|
|
|
|
|
2026-02-09 12:11:01 +08:00
|
|
|
|
results = []
|
|
|
|
|
|
for item in query.all():
|
|
|
|
|
|
results.append({
|
|
|
|
|
|
'id': item.id,
|
|
|
|
|
|
'name': item.name,
|
|
|
|
|
|
'spec': item.spec_model,
|
|
|
|
|
|
'category': item.category,
|
|
|
|
|
|
'unit': item.unit,
|
|
|
|
|
|
'type': item.material_type,
|
|
|
|
|
|
})
|
|
|
|
|
|
return results
|
2026-02-11 15:12:20 +08:00
|
|
|
|
except Exception:
|
2026-02-09 12:11:01 +08:00
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
2026-02-09 11:29:37 +08:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def create_service(cls, data):
|
|
|
|
|
|
"""创建服务权益记录"""
|
2026-02-11 15:12:20 +08:00
|
|
|
|
# 1. 检查基础物料
|
2026-02-10 13:50:26 +08:00
|
|
|
|
base_id = data.get('base_id')
|
|
|
|
|
|
base = MaterialBase.query.get(base_id)
|
2026-02-09 11:29:37 +08:00
|
|
|
|
if not base:
|
|
|
|
|
|
raise ValueError('基础物料不存在')
|
2026-02-10 13:50:26 +08:00
|
|
|
|
|
|
|
|
|
|
if not base.is_enabled:
|
|
|
|
|
|
raise ValueError(f"物料【{base.name}】已停用,无法创建新的服务权益。")
|
|
|
|
|
|
|
2026-02-11 15:12:20 +08:00
|
|
|
|
# 2. 生成SKU
|
2026-02-09 11:29:37 +08:00
|
|
|
|
sku = cls._generate_sku()
|
2026-02-11 15:12:20 +08:00
|
|
|
|
|
|
|
|
|
|
# 3. 创建对象 (不包含库存数量字段)
|
2026-02-09 11:29:37 +08:00
|
|
|
|
service = StockService(
|
|
|
|
|
|
base_id=data['base_id'],
|
|
|
|
|
|
sku=sku,
|
2026-02-11 15:12:20 +08:00
|
|
|
|
sale_price=data.get('sale_price', 0),
|
|
|
|
|
|
provider_name=data.get('provider_name', ''),
|
|
|
|
|
|
description=data.get('description', ''),
|
|
|
|
|
|
|
|
|
|
|
|
# 可选字段映射
|
|
|
|
|
|
service_category=data.get('service_category', ''),
|
|
|
|
|
|
contract_id=data.get('contract_id', ''),
|
|
|
|
|
|
contact_person=data.get('contact_person', ''),
|
|
|
|
|
|
valid_period=data.get('valid_period', ''),
|
|
|
|
|
|
cost_price=data.get('cost_price', 0)
|
2026-02-09 11:29:37 +08:00
|
|
|
|
)
|
2026-02-11 15:12:20 +08:00
|
|
|
|
|
2026-02-09 11:29:37 +08:00
|
|
|
|
db.session.add(service)
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
return service
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_service(cls, service_id):
|
2026-02-11 15:12:20 +08:00
|
|
|
|
"""获取单个详情"""
|
2026-02-09 11:29:37 +08:00
|
|
|
|
service = StockService.query.filter_by(id=service_id, is_deleted=False).first()
|
|
|
|
|
|
if not service:
|
|
|
|
|
|
raise ValueError('服务权益记录不存在')
|
|
|
|
|
|
return service
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def update_service(cls, service_id, data):
|
2026-02-11 15:12:20 +08:00
|
|
|
|
"""更新服务权益"""
|
2026-02-09 11:29:37 +08:00
|
|
|
|
service = cls.get_service(service_id)
|
2026-02-11 15:12:20 +08:00
|
|
|
|
|
|
|
|
|
|
# 允许更新的字段
|
2026-02-09 11:29:37 +08:00
|
|
|
|
if 'sale_price' in data:
|
|
|
|
|
|
service.sale_price = data['sale_price']
|
|
|
|
|
|
if 'provider_name' in data:
|
|
|
|
|
|
service.provider_name = data['provider_name']
|
|
|
|
|
|
if 'description' in data:
|
|
|
|
|
|
service.description = data.get('description', '')
|
2026-02-11 15:12:20 +08:00
|
|
|
|
if 'cost_price' in data:
|
|
|
|
|
|
service.cost_price = data.get('cost_price', 0)
|
|
|
|
|
|
if 'contract_id' in data:
|
|
|
|
|
|
service.contract_id = data.get('contract_id', '')
|
|
|
|
|
|
if 'contact_person' in data:
|
|
|
|
|
|
service.contact_person = data.get('contact_person', '')
|
|
|
|
|
|
if 'valid_period' in data:
|
|
|
|
|
|
service.valid_period = data.get('valid_period', '')
|
|
|
|
|
|
|
2026-02-09 11:29:37 +08:00
|
|
|
|
service.updated_at = datetime.now()
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
return service
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def delete_service(cls, service_id):
|
2026-02-11 15:12:20 +08:00
|
|
|
|
"""软删除"""
|
2026-02-09 11:29:37 +08:00
|
|
|
|
service = cls.get_service(service_id)
|
2026-03-20 15:47:13 +08:00
|
|
|
|
# 提前获取服务名称用于审计日志
|
|
|
|
|
|
service_name = service.service_name
|
2026-02-09 11:29:37 +08:00
|
|
|
|
service.is_deleted = True
|
|
|
|
|
|
service.updated_at = datetime.now()
|
|
|
|
|
|
db.session.commit()
|
2026-03-20 15:47:13 +08:00
|
|
|
|
return service_name
|
2026-02-09 11:29:37 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_service_list(cls, page=1, per_page=20, keyword=None,
|
|
|
|
|
|
start_date=None, end_date=None, provider_name=None):
|
2026-02-11 15:12:20 +08:00
|
|
|
|
"""分页查询列表"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
query = StockService.query.filter_by(is_deleted=False)
|
|
|
|
|
|
|
2026-07-15 11:11:40 +08:00
|
|
|
|
# 始终 join MaterialBase(base_id 为 NOT NULL,inner join 不会改变结果行数)
|
|
|
|
|
|
# 用于公司过滤和关键词搜索
|
|
|
|
|
|
query = query.join(StockService.base)
|
|
|
|
|
|
|
2026-02-11 15:12:20 +08:00
|
|
|
|
# 关键词联表搜索
|
|
|
|
|
|
if keyword:
|
2026-07-15 11:11:40 +08:00
|
|
|
|
query = query.filter(
|
2026-02-11 15:12:20 +08:00
|
|
|
|
db.or_(
|
|
|
|
|
|
StockService.sku.ilike(f'%{keyword}%'),
|
|
|
|
|
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
|
|
|
|
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
2026-02-11 14:55:28 +08:00
|
|
|
|
)
|
2026-02-09 11:29:37 +08:00
|
|
|
|
)
|
2026-02-11 15:12:20 +08:00
|
|
|
|
|
2026-07-15 11:11:40 +08:00
|
|
|
|
# 【行级数据隔离】基于 JWT 多租户公司过滤
|
|
|
|
|
|
from app.utils.decorators import get_current_company_filter
|
|
|
|
|
|
|
|
|
|
|
|
company_limit = get_current_company_filter()
|
|
|
|
|
|
if company_limit is not None:
|
|
|
|
|
|
query = query.filter(MaterialBase.company_name == company_limit)
|
|
|
|
|
|
|
2026-02-11 15:12:20 +08:00
|
|
|
|
# 日期过滤
|
|
|
|
|
|
if start_date:
|
|
|
|
|
|
try:
|
|
|
|
|
|
start = datetime.strptime(start_date, '%Y-%m-%d')
|
|
|
|
|
|
query = query.filter(StockService.created_at >= start)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if end_date:
|
|
|
|
|
|
try:
|
|
|
|
|
|
end = datetime.strptime(end_date, '%Y-%m-%d')
|
|
|
|
|
|
# 包含当天结束
|
|
|
|
|
|
end = end + timedelta(days=1) - timedelta(seconds=1)
|
|
|
|
|
|
query = query.filter(StockService.created_at <= end)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 服务商过滤
|
|
|
|
|
|
if provider_name:
|
|
|
|
|
|
query = query.filter(StockService.provider_name.ilike(f'%{provider_name}%'))
|
|
|
|
|
|
|
|
|
|
|
|
total = query.count()
|
|
|
|
|
|
|
|
|
|
|
|
items = query.order_by(StockService.created_at.desc()) \
|
|
|
|
|
|
.offset((page - 1) * per_page) \
|
|
|
|
|
|
.limit(per_page).all()
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
'items': [item.to_dict() for item in items],
|
|
|
|
|
|
'total': total,
|
|
|
|
|
|
'page': page,
|
|
|
|
|
|
'per_page': per_page
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
raise e
|
|
|
|
|
|
|
2026-02-10 13:51:19 +08:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_history_providers(cls, base_id):
|
2026-02-11 15:12:20 +08:00
|
|
|
|
"""获取历史供应商"""
|
2026-02-10 13:51:19 +08:00
|
|
|
|
try:
|
|
|
|
|
|
query = db.session.query(StockService.provider_name).filter(
|
|
|
|
|
|
StockService.base_id == base_id,
|
2026-02-11 15:12:20 +08:00
|
|
|
|
StockService.provider_name.isnot(None),
|
|
|
|
|
|
StockService.provider_name != ''
|
2026-02-10 13:51:19 +08:00
|
|
|
|
).distinct().order_by(StockService.provider_name)
|
2026-02-11 15:12:20 +08:00
|
|
|
|
return [row[0] for row in query.all()]
|
2026-02-10 13:51:19 +08:00
|
|
|
|
except Exception:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def search_system_users(cls, keyword):
|
2026-02-11 15:12:20 +08:00
|
|
|
|
"""搜索系统用户(占位)"""
|
|
|
|
|
|
return []
|
2026-02-11 14:42:16 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_filter_options(cls):
|
2026-02-11 15:12:20 +08:00
|
|
|
|
"""获取筛选下拉选项"""
|
2026-02-11 14:42:16 +08:00
|
|
|
|
try:
|
|
|
|
|
|
categories = db.session.query(MaterialBase.category) \
|
|
|
|
|
|
.filter(MaterialBase.category != None, MaterialBase.category != '') \
|
|
|
|
|
|
.distinct().all()
|
|
|
|
|
|
types = db.session.query(MaterialBase.material_type) \
|
|
|
|
|
|
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
|
|
|
|
|
|
.distinct().all()
|
|
|
|
|
|
return {
|
|
|
|
|
|
"categories": [r[0] for r in categories],
|
|
|
|
|
|
"types": [r[0] for r in types]
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception:
|
2026-02-11 15:12:20 +08:00
|
|
|
|
return {"categories": [], "types": []}
|