ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

InvenTree ValidationMixin 插件开发指南:为数据库对象、部件编号与序列号构建自定义校验规则

InvenTree ValidationMixin 插件开发指南:为数据库对象、部件编号与序列号构建自定义校验规则 InvenTree ValidationMixin 插件开发指南为数据库对象、部件编号与序列号构建自定义校验规则【免费下载链接】InvenTreeOpen Source Inventory Management System项目地址: https://gitcode.com/GitHub_Trending/in/InvenTree本文是 InvenTree 开源库存管理系统中ValidationMixin 插件混入类的完整开发指南。ValidationMixin将数据库模型的保存、删除以及部件名称、IPN、参数值、批次代码与序列号等字段的校验逻辑全部暴露给插件系统本文讲解如何通过编写自定义插件实现企业级业务约束如命名规范、序列号方案、批次编码规则。读完本文你将掌握 ValidationMixin 的全部钩子方法签名、错误抛出规范、多插件协作语义并能基于仓库自带示例插件快速落地自己的校验插件。ValidationMixin 是什么ValidationMixin类允许插件对数据库中的对象执行自定义校验。在 InvenTree 中插件不仅用于扩展 UI、后台任务或外部接口还能深度介入核心数据层的完整性与业务规则约束。其完整定义位于 ValidationMixin.py并在 mixins 聚合导出 中以ValidationMixin名称统一导出因此插件中可直接使用from plugin import InvenTreePlugin from plugin.mixins import ValidationMixin该混入类支持以下六类校验/生成能力插件可根据需要覆写其中任意方法Part 名称validate_part_namePart IPN 内部物料号validate_part_ipnPart 参数值validate_parameter序列号validate_serial_number/convert_serial_to_int/increment_serial_number/get_latest_serial_number批次代码validate_batch_code/generate_batch_code通用模型实例校验与删除校验validate_model_instance/validate_model_deletion多插件协作语义系统允许同时加载多个支持校验方法的插件。以字段校验为例当某个插件返回空值None时系统会继续询问下一个可用插件这与事件、通知等其他混入的语义一致。具体规则在 ValidationMixin 类文档字符串 中有明确说明多个ValidationMixin插件可以同时生效基类提供的桩方法默认返回None空值对生成类方法第一个返回非空结果的插件胜出后续插件不再调用对校验类方法系统依次检查所有已加载插件直到某个插件抛出异常为止。对于校验方法插件有三种可接受的返回值约定方法判定值不合法抛出django.core.exceptions.ValidationError方法通过并返回None系统继续检查下一个插件方法通过并返回True跳过后续所有插件的检查。模型删除校验任何继承了PluginValidationMixin的模型在从数据库删除之前都会被先交给插件生态做是否真的允许删除的检查。该混入类定义于 InvenTree/models.py其delete()方法在调用super().delete()之前会遍历所有注册了VALIDATION混入的插件并调用validate_model_deletiondef delete(self, *args, **kwargs): if self.should_plugin_validate(): for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): try: plugin.validate_model_deletion(self) except ValidationError as e: # 插件可抛出 ValidationError 来阻止删除 raise e except Exception: log_error(validate_model_deletion, pluginplugin.slug) continue super().delete(*args, **kwargs)插件可覆写validate_model_deletion方法对即将删除的模型实例执行自定义校验。方法签名如下def validate_model_deletion(self, instance: Model) - None: Run custom validation when a model instance is being deleted. 参数: instance: 待删除的模型实例 返回: None 或 True语义见类文档 抛出: ValidationError: 若该实例不允许被删除 return None典型应用场景包括禁止删除仍被引用的供应商、禁止删除带有库存记录的部件、或者基于业务规则阻止删除特定类别的物料。需要特别注意的是异常处理逻辑表明插件抛出的ValidationError会直接向上传播从而阻断删除而其他类型的未预期异常则仅被记录到错误日志并继续后续插件。模型实例校验保存前校验任何继承了PluginValidationMixin的模型实例在保存到数据库之前无论是新建还是更新都会先经过插件生态的校验。实现机制位于 PluginValidationMixin模型基类重写了full_clean()与save()前者在 Django 内置校验之后追加插件校验后者在调用super().save()之前执行插件校验def full_clean(self, *args, **kwargs): super().full_clean(*args, **kwargs) self.run_plugin_validation() def save(self, *args, **kwargs): self.run_plugin_validation() super().save(*args, **kwargs)run_plugin_validation会通过DiffMixin的get_field_deltas()计算实例的字段变更差异deltas并依次调用各插件的validate_model_instancedef run_plugin_validation(self): deltas self.get_field_deltas() for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): try: if plugin.validate_model_instance(self, deltasdeltas) is True: return except ValidationError as exc: raise exc except Exception: InvenTree.exceptions.log_error(validate_model_instance, pluginplugin.slug) raise ValidationError(_(Error running plugin validation))其中deltas是形如{field_name: {old: 旧值, new: 新值}}的字典参见 DiffMixin.get_field_deltas当实例为新建数据库中无对应记录时为空字典{}。利用deltas插件可以精确判断某个字段是否被修改、改成了什么这是实现增量约束例如描述不允许被缩短的关键。插件侧只需覆写validate_model_instance方法def validate_model_instance( self, instance: Model, deltas: Optional[dict] None ) - Optional[bool]: 对数据库模型实例执行自定义校验。 参数: instance: 待校验的模型实例 deltas: 字段名与更新值映射仅当实例被更新时提供 返回: None 或 True语义见类文档 抛出: ValidationError: 若实例不合法 return None此外should_plugin_validate()方法models.py允许在导入/导出等只读管理命令执行期间跳过插件校验插件作者无需关心这一层但在排查为什么导入时没有触发校验问题时需要知晓这一行为。错误消息规范任何错误消息都必须以ValidationError抛出。ValidationMixin提供了raise_error便捷方法它是对ValidationError构造函数的简单封装def raise_error(self, message): Raise a ValidationError with the given message. raise ValidationError(message)根据错误作用范围ValidationError的正文body有两种形态实例级错误Instance Errors校验错误作用于整个模型实例错误正文应为一个字符串或字符串列表。例如self.raise_error(Part description cannot be shorter than the name)。字段级错误Field Errors校验错误仅作用于实例上的某个字段错误正文应为字典字典的键对应模型字段名。例如self.raise_error({ name: Part name and category name must start with the same letter })同一个字典中可以包含多个字段键即一条ValidationError可以同时指出多个字段的违规。字段级错误会在上层被包装并关联到表单/API 的对应字段上参见 Part.validate_name 中raise ValidationError({name: exc.message})的用法。示例插件原文档给出了一个同时演示实例级与字段级校验的完整插件from plugin import InvenTreePlugin from plugin.mixins import ValidationMixin import part.models class MyValidationMixin(ValidationMixin, InvenTreePlugin): Custom validation plugin. def validate_model_instance(self, instance, deltasNone): Custom model validation example. - A part name and category name must have the same starting letter - A PartCategory description field cannot be shortened after it has been created if isinstance(instance, part.models.Part): if category : instance.category: if category.name[0] ! part.name[0]: self.raise_error({ name: Part name and category name must start with the same letter }) if isinstance(instance, part.models.PartCategory): if deltas and description in deltas: d_new deltas[description][new] d_old deltas[description][old] if len(d_new) len(d_old): self.raise_error({ description: Description cannot be shortened })该示例展示了两类典型模式针对Part模型通过isinstance类型判断后校验跨字段一致性部件名与所属类目名的首字母必须一致此处以字段级错误定位到name字段针对PartCategory模型则利用deltas检测description字段是否被缩短实现只允许追加、不允许删减的增量约束。需要注意的是示例代码中第二处校验分支的part变量应为当前实例即instance完整可运行的参考实现请直接查看仓库自带的 validation_sample.py 示例插件。字段级校验除了通用的模型实例校验InvenTree 还针对以下高频业务字段开放了独立的校验钩子字段校验发生在对应模型自身的validate_*方法中且同样遵循依次调用插件、None继续、True短路、异常即失败的语义。Part 名称校验默认情况下部件名称不受任何命名规范约束但如果业务上需要例如强制统一命名前缀、禁止非法字符可以覆写validate_part_name方法。当插件判定名称不合规时抛出ValidationError上层调用方会捕获并处理。方法签名def validate_part_name(self, name: str, part: part.models.Part) - Optional[bool]: 对提议的部件名称执行校验。 参数: name: 提议的部件名称 part: 正在被校验的 Part 实例 返回: None 或 True语义见类文档 抛出: ValidationError: 若名称不合规 return None调用链位于 Part.validate_name依次调用各插件的validate_part_name(self.name, self)若插件抛出ValidationError且raise_errorTrue则包装为{name: exc.message}的字段级错误继续上抛。示例插件 validate_part_name 演示了两个刻意简单的规则名称长度不得长于描述字段名称不得包含ILLEGAL_PART_CHARS设置中定义的非法字符默认!#$%^*()~。Part IPN 校验Part 的 IPNInternal Part Number内部物料号字段同样暴露给插件。任何继承ValidationMixin的插件都可以实现validate_part_ipn方法当 IPN 不符合约定规范时抛出ValidationErrordef validate_part_ipn(self, ipn: str, part: part.models.Part) - Optional[bool]: 对提议的部件 IPN 执行校验。 return None调用链位于 Part.validate_ipn先依次询问各插件的validate_part_ipn(self.IPN, self)插件抛错则包装为{IPN: exc.message}如果所有插件都放行再回退到全局设置PART_IPN_REGEX的正则匹配作为内置兜底校验。这意味着插件校验是 IPN 规则的第一道自定义关卡而正则设置项是第二道内置防线。示例插件 validate_part_ipn 演示了通过设置项IPN_MUST_CONTAIN_Q强制 IPN 必须包含字符Q的用法。参数值校验Parameters部件参数也支持自定义校验规则。通过实现validate_parameter方法插件可以对参数值施加任意约定不符合时抛出带消息的ValidationError。方法签名如下def validate_parameter( self, parameter: common.models.Parameter, data: str ) - Optional[bool]: 校验参数值。 参数: parameter: 正在被校验的参数对象 data: 提议的参数值 返回: None 或 True语义见类文档 抛出: ValidationError: 若参数值不合规 return None示例插件 validate_parameter 演示了根据参数模板名parameter.template.name实施差异化规则当参数名为length或width时数值必须小于 100。这展示了校验规则可以完全由业务模板驱动。批次代码批次代码既可以被插件校验也可以被插件生成。校验批次代码validate_batch_code方法允许插件在用户输入的批次代码不符合特定模式时抛出错误。方法签名def validate_batch_code( self, batch_code: str, item: stock.models.StockItem ) - Optional[bool]: 校验提供的批次代码。 return None调用链位于 StockItem.validate_batch_code遍历校验插件插件抛出的ValidationError会被包装为{batch: exc.message}字段级错误。示例插件 validate_batch_code 通过设置项BATCH_CODE_PREFIX强制批次代码必须以指定前缀默认B开头。生成批次代码generate_batch_code方法用于根据一组上下文信息生成新的批次代码。方法签名def generate_batch_code(self, **kwargs) - Optional[str]: 生成新的批次代码。 kwargs: 基于调用方上下文传入的任意关键字参数 返回: 新的批次代码字符串或 None return None调用链位于 stock/generators.py 的 generate_batch_code系统会构造包含date、year、month、day、hour、minute、week以及调用方附加上下文如part、build_order的 context 字典逐个询问插件第一个返回非空结果的插件胜出若所有插件均未生成则回退到全局设置STOCK_BATCH_CODE_TEMPLATE的 Django 模板渲染作为内置兜底。从源码看兼容旧版插件签名不接受**kwargs的调用会被自动识别通过inspect.signature判断因此旧插件无需改写即可继续工作。示例插件 generate_batch_code 展示了完整的上下文利用方式生成SAMPLE-BATCH-年:月:日格式的代码并在传入part时前缀部件名、传入build_order时前缀工单参考号由此可以拼出携带业务上下文的批次码。序列号不同应用对序列号的需求差异极大。与其试图提供一刀切的序列号实现InvenTree 允许通过插件实现完全自定义的序列号方案。InvenTree 内置的序列号系统使用简单算法来校验与递增序列号更复杂的行为十六进制序列、回文序列、字母数字混合序列等则通过ValidationMixin的以下方法实现。序列号校验通过validate_serial_number方法实现自定义序列号校验。一个提议的序列号会被传入该方法插件可以抛出ValidationError表明该序列号无效。方法签名def validate_serial_number( self, serial: str, part: part.models.Part, stock_item: Optional[stock.models.StockItem] None, ) - Optional[bool]: 校验提供的序列号。 return None关于stock_item参数调用链 Part.validate_serial_number 明确说明如果提供了stock_item参数说明该库存条目已经被分配了此序列号在后续的唯一性检查中应将该条目排除该参数是可选的在无库存条目上下文中可能为None。此外调用方会通过inspect.signature自动检测插件方法是否接受stock_item参数旧签名插件只接收serial, part无需修改也能运行。内置校验在插件全部放行后还会执行默认的唯一性检查根据全局设置SERIAL_NUMBER_GLOBALLY_UNIQUE决定序列号是在整个部件树内唯一还是跨所有部件全局唯一。一个要求所有序列号必须是合法十六进制值的插件可这样实现def validate_serial_number(self, serial: str, part: Part, stock_item: StockItem None): Validate the supplied serial number Arguments: serial: The proposed serial number (string) part: The Part instance for which this serial number is being validated stock_item: The StockItem instance for which this serial number is being validated try: # Attempt integer conversion int(serial, 16) except ValueError: raise ValidationError(Serial number must be a valid hex value)序列号排序虽然 InvenTree 的序列号字段支持任意文本值但在内部它会尝试将这些值强制转换为整数表示以便进行更高效的排序。插件可以实现convert_serial_to_int方法决定特定序列号如何转换为整数表示def convert_serial_to_int(self, serial: str) - Optional[int]: 将序列号字符串转换为整数表示用于基于序列号的高效排序。 插件可以实现以下任一行为 - 按某种算法基于序列号字符串返回整数 - 返回固定值使排序回退到字符串表示 - 返回 None让其他插件继续执行转换 return None如果该方法未实现或序列号无法转换为整数排序算法将回退到文本字符串值。从源码实现 StockItem.convert_serial_to_int 可以看到转换结果会被取绝对值并裁剪到0x7FFFFFFF数据库 schema 允许的范围内确保极端情况下不会溢出。此外该整数值还会被写入StockItem.serial_int字段见 update_serial_number用于下一个/上一个序列化库存条目等高效查询serial_int索引排序。注意文档同时说明返回的整数不要求唯一因为排序场景并不依赖唯一性。序列号递增序列号系统的核心能力之一是递增序列号即确定序列中的下一个值。对于自定义序列号方案必须提供给定当前值生成下一个序列号的方法。插件可以实现increment_serial_number方法def increment_serial_number( self, serial: str, part: Optional[part.models.Part] None, **kwargs ) - Optional[str]: 基于提供的值返回序列中的下一个序列号。 参数: serial: 当前序列号值字符串 part: 正在被递增的 Part 实例 返回: 序列中的下一个序列号字符串或 None return None如果提供的值无法递增或发生错误方法应返回None此时系统回退到内置的简单递增算法。内置入口位于 helpers.py 的 increment_serial_number同样通过inspect.signature兼容新旧签名part参数于 2024-08-21 加入插件返回的第一个非空结果即被采用。承接前面的十六进制示例递增方法可这样实现def increment_serial_number(self, serial: str): Provide the next hexadecimal number in sequence try: val int(serial, 16) 1 val hex(val).upper()[2:] except ValueError: val None return val值得关注的是序列号的完整生成链路在 stock/generators.py 的 generate_serial_number 中串联起来先通过part.get_latest_serial_number()该方法本身也开放了get_latest_serial_number插件钩子见 Part.get_latest_serial_number取得当前最大值再循环调用increment_serial_number生成所需数量任何插件返回空值或重复值都会中断生成。也就是说一套完整的自定义序列号方案通常需要同时实现get_latest_serial_number、validate_serial_number、convert_serial_to_int与increment_serial_number四个钩子。仓库自带的完整示例插件InvenTree 源码中提供了一个实现多项自定义校验的完整示例插件validation_sample.py类名为SampleValidatorPlugin。它同时继承SettingsMixin与ValidationMixin并通过SETTINGS定义了一组可配置开关是把规则做成可配置项的最佳范本设置键含义默认值ILLEGAL_PART_CHARS部件名称中不允许出现的字符!#$%^*()~IPN_MUST_CONTAIN_QIPN 字段必须包含字符QFalseSERIAL_MUST_BE_PALINDROME序列号必须是回文FalseSERIAL_MUST_MATCH_PART序列号首字母必须与部件名首字母一致FalseBATCH_CODE_PREFIX批次代码必需的前缀BBOM_ITEM_INTEGERBom 条目数量必须是整数False该插件完整覆盖了本文所述的大部分钩子validate_model_instance校验 BomItem 数量为整数、Part 描述不可缩短validate_part_name校验名称长度与非法字符validate_part_ipn校验 IPN 包含Qvalidate_parameter对length/width参数限制取值上限validate_serial_number依次检查回文、与部件名匹配、以及序列号不能是 5 的倍数三条规则increment_serial_number在递增时自动跳过 5 的倍数validate_batch_code强制批次前缀generate_batch_code生成携带部件名与工单号的日期型批次码。其中跳过 5 的倍数与其序列号不能是 5 的倍数的校验规则形成闭环演示了校验与生成如何配合实现一致的序列号策略。在开发自己的校验插件时建议直接以该示例为起点继承ValidationMixin可按需叠加SettingsMixin使规则可配置、覆写所需钩子方法、始终通过self.raise_error(...)抛出ValidationError并严格遵循字段级错误用 dict、实例级错误用 str/list的约定。加载插件后校验逻辑会自动生效于所有继承PluginValidationMixin的模型InvenTree 绝大多数核心模型均继承自 InvenTreeModel无需修改任何核心代码。【免费下载链接】InvenTreeOpen Source Inventory Management System项目地址: https://gitcode.com/GitHub_Trending/in/InvenTree创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表