@shundoo-ai/dsh-cosmic 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +5 -5
- package/package.json +1 -1
- package/skills/cosmic-build/CHANGELOG.md +4 -0
- package/skills/cosmic-build/LOCAL-PATCHES.md +91 -40
- package/skills/cosmic-build/SKILL.md +46 -13
- package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +169 -0
- package/skills/cosmic-build/assets/platform-report-filters/platform.patch +98 -0
- package/skills/cosmic-build/assets/report-algo-benchmark/ReportAlgoBenchmark.java +48 -0
- package/skills/cosmic-build/assets/report-algo-benchmark/ReportBenchmarkLogFactory.java +7 -0
- package/skills/cosmic-build/examples/java-rules/CosmicEntryQuery.java +26 -0
- package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +35 -0
- package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +77 -0
- package/skills/cosmic-build/examples/java-rules/CosmicTimeAdapter.java +25 -0
- package/skills/cosmic-build/examples/java-rules/CosmicTimeAdapterCheck.java +26 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/api/algo/GroupbyDataSet.md +47 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/api/entity/report/FilterInfo.md +12 -12
- package/skills/cosmic-build/knowledge/cosmic/platform/api/entity/report/FilterItemInfo.md +2 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +2 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/java-type-contracts.md +11 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +18 -14
- package/skills/cosmic-build/knowledge/cosmic/platform/operation-plugin.md +4 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +17 -9
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/cross-entity-aggregate-writeback.md +2 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/pre-post-operation.md +15 -7
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +24 -68
- package/skills/cosmic-build/knowledge/cosmic/platform/workflow-plugin.md +2 -7
- package/skills/cosmic-build/references/application-context.md +35 -1
- package/skills/cosmic-build/references/common-reference.md +1 -1
- package/skills/cosmic-build/references/compile-guide.md +55 -68
- package/skills/cosmic-build/references/delivery.md +59 -7
- package/skills/cosmic-build/references/execution-contract.md +38 -0
- package/skills/cosmic-build/references/form-discovery.md +7 -1
- package/skills/cosmic-build/references/java-plugin-delivery.md +32 -162
- package/skills/cosmic-build/references/operation-dispatch.md +17 -0
- package/skills/cosmic-build/references/plugin-handoff-input.md +131 -0
- package/skills/cosmic-build/references/plugin-runtime-routing.md +1 -1
- package/skills/cosmic-build/references/report-filter-contract.md +78 -0
- package/skills/cosmic-build/references/sdk-inspection.md +23 -0
- package/skills/cosmic-build/schemas/data-model-authoring.schema.json +2 -3
- package/skills/cosmic-build/scripts/_http_client.py +55 -55
- package/skills/cosmic-build/scripts/appconfig_load.py +2 -2
- package/skills/cosmic-build/scripts/apply_report_filter_extension.py +39 -0
- package/skills/cosmic-build/scripts/bootstrap_cosmic_context.py +37 -2
- package/skills/cosmic-build/scripts/compile_plugin_source.py +279 -156
- package/skills/cosmic-build/scripts/execution_binding.py +40 -0
- package/skills/cosmic-build/scripts/inspect_platform_sdk.py +169 -0
- package/skills/cosmic-build/scripts/kd_api.py +23 -22
- package/skills/cosmic-build/scripts/kd_auth.py +1 -1
- package/skills/cosmic-build/scripts/kd_env_loader.py +19 -10
- package/skills/cosmic-build/scripts/kd_token_cache.py +2 -2
- package/skills/cosmic-build/scripts/kingscript_sdk_sync.py +350 -0
- package/skills/cosmic-build/scripts/kingscript_source_gate.py +11 -8
- package/skills/cosmic-build/scripts/platform_api_contract.py +246 -11
- package/skills/cosmic-build/scripts/plugin_corpus/build_index.py +403 -0
- package/skills/cosmic-build/scripts/plugin_corpus/jvm_classfile.py +281 -0
- package/skills/cosmic-build/scripts/plugin_corpus/measure_api_usage.py +446 -0
- package/skills/cosmic-build/scripts/plugin_source_gate.py +48 -7
- package/skills/cosmic-build/scripts/plugin_target_sdk.py +45 -0
- package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +493 -47
- package/skills/cosmic-build/scripts/preview_url.py +35 -6
- package/skills/cosmic-build/scripts/query_plugin_handoff.py +250 -0
- package/skills/cosmic-build/scripts/register_plugins.py +28 -2
- package/skills/cosmic-build/scripts/render_output_cards.py +42 -9
- package/skills/cosmic-build/scripts/report_algo_benchmark.py +46 -0
- package/skills/cosmic-build/scripts/run.py +2 -2
- package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +43 -0
- package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +64 -0
- package/skills/cosmic-build/scripts/test/fixtures/java_context_booking_native.json +541 -0
- package/skills/cosmic-build/scripts/test/test_add_rule_business_contract.py +85 -14
- package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +12 -3
- package/skills/cosmic-build/scripts/test/test_build_meta_order.py +0 -16
- package/skills/cosmic-build/scripts/test/test_cli_continuation.py +87 -0
- package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +224 -3
- package/skills/cosmic-build/scripts/test/test_control_authoring.py +123 -4
- package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +141 -0
- package/skills/cosmic-build/scripts/test/test_data_model_contract.py +30 -5
- package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +2 -10
- package/skills/cosmic-build/scripts/test/test_dym_sync_background.py +11 -0
- package/skills/cosmic-build/scripts/test/test_dynamic_form_metadata_only_contract.py +59 -1
- package/skills/cosmic-build/scripts/test/test_guide_skeleton_contract.py +87 -43
- package/skills/cosmic-build/scripts/test/test_inspect_platform_sdk.py +71 -0
- package/skills/cosmic-build/scripts/test/test_kd_api.py +67 -19
- package/skills/cosmic-build/scripts/test/test_kingscript_sdk_sync.py +221 -0
- package/skills/cosmic-build/scripts/test/test_kingscript_source_gate.py +10 -0
- package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +18 -0
- package/skills/cosmic-build/scripts/test/test_long_session_regressions.py +116 -0
- package/skills/cosmic-build/scripts/test/test_mcp_cli.py +524 -0
- package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +141 -0
- package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +36 -2
- package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +161 -1
- package/skills/cosmic-build/scripts/test/test_plugin_business_contract.py +91 -0
- package/skills/cosmic-build/scripts/test/test_plugin_corpus.py +195 -0
- package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +346 -1
- package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +70 -0
- package/skills/cosmic-build/scripts/test/test_preview_url.py +44 -1
- package/skills/cosmic-build/scripts/test/test_property_batch.py +22 -0
- package/skills/cosmic-build/scripts/test/test_property_input_discovery.py +13 -11
- package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +160 -0
- package/skills/cosmic-build/scripts/test/test_register_plugins.py +31 -1
- package/skills/cosmic-build/scripts/test/test_render_output_cards.py +50 -4
- package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -0
- package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +5 -4
- package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +36 -0
- package/skills/cosmic-build/scripts/test/test_rule_batch_evidence.py +1 -1
- package/skills/cosmic-build/scripts/test/test_rule_domain_contract.py +20 -0
- package/skills/cosmic-build/scripts/test/test_rule_function_formula.py +16 -6
- package/skills/cosmic-build/scripts/test/test_skill_entry_contract.py +1 -0
- package/skills/cosmic-build/scripts/test/test_test_environment.py +45 -0
- package/skills/cosmic-build/scripts/test/test_verify_plugin_loading.py +79 -0
- package/skills/cosmic-build/scripts/test_environment.py +49 -0
- package/skills/cosmic-build/scripts/verify_plugin_loading.py +71 -0
- package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +86 -162
- package/skills/cosmic-build/skills/cosmic-coding-script/ATTRIBUTION.md +1 -1
- package/skills/cosmic-build/skills/cosmic-coding-script/SKILL.md +45 -14
- package/skills/cosmic-build/skills/cosmic-coding-script/references/kwc-controller-authoring.md +6 -0
- package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/README.md +29 -19
- package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/docs//350/204/232/346/234/254SDK.md +3 -4
- package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/indexes/package-index.md +1 -1
- package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/strategy.md +27 -18
- package/skills/cosmic-build/skills/cosmic-meta-api/SKILL.md +11 -17
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/app_menu.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/meta_log.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/control-authoring.json +45 -4
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/form-control-traits.json +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/layout-ui.json +18 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/menu-publish.json +8 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/property-batch.json +7 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/property-input-index.json +3 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +1 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +37 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-event-contract.json +24 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/run-build-meta.json +61 -12
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/dropdown-menu.schema.json +69 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +2 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/rule-set.schema.json +1 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/rule-write.schema.json +14 -7
- package/skills/cosmic-build/skills/cosmic-meta-api/docs/api-summary.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/app-action/SKILL.md +6 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/develop/SKILL.md +1 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/export/SKILL.md +5 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/init/SKILL.md +13 -36
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/SKILL.md +8 -9
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/config.json.example +5 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/reset/SKILL.md +6 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/usage/SKILL.md +20 -148
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/build_receipts.py +171 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +25 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_controls.py +55 -21
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_fields.py +10 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +63 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_forms.py +59 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +8 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +36 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_plugins.py +115 -90
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +666 -4
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +1415 -87
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/commands.py +6 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +28 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +40 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +32 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_policy_contract.py +6 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +113 -17
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/reset.py +3 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/rule_contract.py +107 -26
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/build_meta_order.py +50 -49
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +856 -150
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +47 -14
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +374 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_cli_failure_contract.py +24 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_creation_identity_boundary.py +4 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_dropdown_menu_contract.py +382 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_existing_form_ui.py +0 -25
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_field_decorator.py +50 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_functional_group_resolution.py +83 -16
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_identity_template_resolution.py +103 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +28 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +22 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +65 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +1041 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +65 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +17 -0
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/api-param-checklist.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/rule-modeling-guide.md +7 -4
- package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +61 -13
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/app-menu/SKILL.md +8 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/formtemplate/SKILL.md +5 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/formtemplate/dynamic-form-artifact-schema.md +5 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +21 -9
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-entity-graphs.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-storage-constraints.md +5 -3
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-ui-composition.md +6 -3
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/creation-recovery.md +13 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/parse-core.md +3 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +6 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +135 -87
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/field_type_registry.py +11 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/isv_resolver.py +1 -3
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +29 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +4 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/context/SKILL.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/execute/SKILL.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/SKILL.md +5 -4
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/operation-write.md +6 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/validation.md +2 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/SKILL.md +20 -5
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/complex-apis.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/policy-grammar.md +4 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +9 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +61 -4
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/raw-rule-authoring.md +3 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/rule-actions.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/rule-reference.md +6 -6
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/state.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/unit/SKILL.md +6 -3
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +4 -4
- package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +4 -1
- package/skills/cosmic-build/skills/cosmic-meta-ui/SKILL.md +9 -1
- package/skills/cosmic-build/skills/cosmic-meta-ui/modules/button/SKILL.md +3 -0
- package/skills/cosmic-build/skills/mcp-server/SKILL.md +210 -0
- package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +431 -0
- package/skills/cosmic-build/skills/openapi-gen/SKILL.md +3 -3
- package/skills/cosmic-build/skills/openapi-gen/scripts/api/gen_runner.py +2 -2
- package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +19 -9
- package/skills/cosmic-dev-best-practices/references/cosmic-meta-api.md +14 -11
- package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +1 -1
- package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/AssistantField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/BasedataField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/BasedataPropField.md +3 -3
- package/skills/cosmic-dev-best-practices/references/elements/CreaterField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/CurrencyField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/FlexField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/MaterielField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/OrgField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/UnitField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/UserField.md +3 -3
- package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +16 -11
- package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +5 -4
- package/skills/erp-analysis/references/delivery-contract.md +4 -2
- package/skills/erp-analysis/templates/AGENTS.template.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/.dsh/cosmic/.kd/config.json +0 -21
|
@@ -32,6 +32,9 @@ from .mutation_contract import mutation_contract, combine_mutation_evidence, cla
|
|
|
32
32
|
from .rule_contract import (
|
|
33
33
|
FORM_RULE_DEFAULT_RET,
|
|
34
34
|
normalize_rule_config,
|
|
35
|
+
merge_action_events,
|
|
36
|
+
select_action_events,
|
|
37
|
+
runtime_action_warnings,
|
|
35
38
|
trigger_field_selectors,
|
|
36
39
|
)
|
|
37
40
|
|
|
@@ -665,8 +668,10 @@ def _validate_rule_write_input(rule_config, *, command):
|
|
|
665
668
|
if rule_type not in {"formRule", "bizRule"}:
|
|
666
669
|
raise ValueError("RULE_TYPE_INVALID: ruleType must be formRule or bizRule")
|
|
667
670
|
if command == "addRule":
|
|
668
|
-
missing = [field for field in ("description", "preDescription", "preCondition"
|
|
671
|
+
missing = [field for field in ("description", "preDescription", "preCondition")
|
|
669
672
|
if rule_config.get(field) in (None, "", [])]
|
|
673
|
+
if not (rule_config.get("trueActions") or rule_config.get("falseActions")):
|
|
674
|
+
missing.append("trueActions or falseActions")
|
|
670
675
|
if missing:
|
|
671
676
|
raise ValueError(
|
|
672
677
|
f"RULE_INPUT_REQUIRED: {', '.join('addRule.' + field for field in missing)}; "
|
|
@@ -1078,6 +1083,61 @@ def _validate_clear_value_action_scope(cfg, token, form_number, rule_config):
|
|
|
1078
1083
|
return rule_config
|
|
1079
1084
|
|
|
1080
1085
|
|
|
1086
|
+
def _resolve_call_field_change_targets(cfg, token, form_number, rule_config):
|
|
1087
|
+
if rule_config.get("ruleType") != "bizRule":
|
|
1088
|
+
return rule_config
|
|
1089
|
+
normalized = normalize_rule_config(rule_config)
|
|
1090
|
+
if not any(
|
|
1091
|
+
action.get("ActionType") == "CallFieldChangeAction"
|
|
1092
|
+
for branch in ("trueActions", "falseActions")
|
|
1093
|
+
for action in (normalized.get(branch) or [])
|
|
1094
|
+
):
|
|
1095
|
+
return rule_config
|
|
1096
|
+
listed = do_list_elements(cfg, token, form_number=form_number, category="field")
|
|
1097
|
+
rows = listed.get("data") if isinstance(listed, dict) else None
|
|
1098
|
+
if not _is_api_success(listed) or not isinstance(rows, list):
|
|
1099
|
+
raise RuntimeError(
|
|
1100
|
+
"RULE_CALL_FIELD_INVENTORY_UNAVAILABLE: listElements returned no field list"
|
|
1101
|
+
)
|
|
1102
|
+
mounted = str(rule_config.get("mountPoint") or "").strip()
|
|
1103
|
+
resolved = copy.deepcopy(rule_config)
|
|
1104
|
+
for branch in ("trueActions", "falseActions"):
|
|
1105
|
+
for action in resolved.get(branch) or []:
|
|
1106
|
+
kind = action.get("actionType") or action.get("ActionType")
|
|
1107
|
+
if kind != "CallFieldChangeAction":
|
|
1108
|
+
continue
|
|
1109
|
+
slot = "expression" if "expression" in action else "Expression"
|
|
1110
|
+
wanted = str(action.get(slot) or "").strip()
|
|
1111
|
+
candidates = [
|
|
1112
|
+
row for row in rows
|
|
1113
|
+
if isinstance(row, dict)
|
|
1114
|
+
and wanted in {
|
|
1115
|
+
str(row.get("key") or "").strip(),
|
|
1116
|
+
str(row.get("name") or "").strip(),
|
|
1117
|
+
str(row.get("id") or "").strip(),
|
|
1118
|
+
}
|
|
1119
|
+
]
|
|
1120
|
+
exact = [row for row in candidates if row.get("key") == wanted]
|
|
1121
|
+
if exact:
|
|
1122
|
+
candidates = exact
|
|
1123
|
+
if mounted:
|
|
1124
|
+
candidates = [
|
|
1125
|
+
row for row in candidates
|
|
1126
|
+
if not str(row.get("parentKey") or "").strip()
|
|
1127
|
+
or str(row.get("parentKey") or "").strip() == mounted
|
|
1128
|
+
]
|
|
1129
|
+
if len(candidates) != 1:
|
|
1130
|
+
code = "UNRESOLVED" if not candidates else "AMBIGUOUS"
|
|
1131
|
+
raise ValueError(
|
|
1132
|
+
f"RULE_CALL_FIELD_TARGET_{code}: {wanted!r}; "
|
|
1133
|
+
"provide an exact field key and entity when names repeat"
|
|
1134
|
+
)
|
|
1135
|
+
for alias in ("expression", "Expression"):
|
|
1136
|
+
if alias in action:
|
|
1137
|
+
action[alias] = str(candidates[0].get("key") or "").strip()
|
|
1138
|
+
return resolved
|
|
1139
|
+
|
|
1140
|
+
|
|
1081
1141
|
def _validate_set_equation_action_scope(cfg, token, form_number, rule_config):
|
|
1082
1142
|
if rule_config.get("ruleType") != "bizRule":
|
|
1083
1143
|
return rule_config
|
|
@@ -1103,24 +1163,24 @@ def _validate_set_equation_action_scope(cfg, token, form_number, rule_config):
|
|
|
1103
1163
|
for row in rows if isinstance(row, dict) and str(row.get("key") or "").strip()
|
|
1104
1164
|
}
|
|
1105
1165
|
for action in actions:
|
|
1106
|
-
mapping
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1166
|
+
for mapping in action.get("Expression") or []:
|
|
1167
|
+
for slot in ("TargetField", "SrcField"):
|
|
1168
|
+
field_key = str(mapping.get(slot) or "").strip()
|
|
1169
|
+
if "." in field_key:
|
|
1170
|
+
raise ValueError(
|
|
1171
|
+
f"RULE_SET_EQUATION_CROSS_ENTITY_UNSUPPORTED: {slot}={field_key!r}"
|
|
1172
|
+
)
|
|
1173
|
+
row = by_key.get(field_key)
|
|
1174
|
+
if row is None:
|
|
1175
|
+
raise ValueError(
|
|
1176
|
+
f"RULE_SET_EQUATION_FIELD_UNRESOLVED: {field_key!r}"
|
|
1177
|
+
)
|
|
1178
|
+
owner = str(row.get("parentKey") or "").strip()
|
|
1179
|
+
if mounted and owner and owner != mounted:
|
|
1180
|
+
raise ValueError(
|
|
1181
|
+
"RULE_SET_EQUATION_ENTITY_MISMATCH: "
|
|
1182
|
+
f"field {field_key!r} belongs to {owner!r}, rule mount is {mounted!r}"
|
|
1183
|
+
)
|
|
1124
1184
|
return rule_config
|
|
1125
1185
|
|
|
1126
1186
|
|
|
@@ -1716,6 +1776,11 @@ def _attach_rule_readback(cfg, token, form_number, rule_config, result, preserve
|
|
|
1716
1776
|
metadata = (read_back or {}).get("data")
|
|
1717
1777
|
observed = _find_rule_projection(metadata, rule_id)
|
|
1718
1778
|
expected = normalize_rule_config(rule_config)
|
|
1779
|
+
if rule_config.get("ruleId"):
|
|
1780
|
+
for branch in ("trueActions", "falseActions"):
|
|
1781
|
+
for action in expected.get(branch) or []:
|
|
1782
|
+
if isinstance(action, dict):
|
|
1783
|
+
action.pop("Id", None)
|
|
1719
1784
|
mismatches = rule_readback_mismatches(expected, observed, metadata)
|
|
1720
1785
|
preserved_mismatches = []
|
|
1721
1786
|
if preserved_rule is not None and isinstance(observed, dict):
|
|
@@ -1838,6 +1903,9 @@ def do_add_rule_verified(
|
|
|
1838
1903
|
resolved = _validate_clear_value_action_scope(
|
|
1839
1904
|
cfg, token, form_number, resolved
|
|
1840
1905
|
)
|
|
1906
|
+
resolved = _resolve_call_field_change_targets(
|
|
1907
|
+
cfg, token, form_number, resolved
|
|
1908
|
+
)
|
|
1841
1909
|
resolved = _validate_set_equation_action_scope(
|
|
1842
1910
|
cfg, token, form_number, resolved
|
|
1843
1911
|
)
|
|
@@ -1855,12 +1923,15 @@ def do_add_rule_verified(
|
|
|
1855
1923
|
result = do_add_rule(cfg, token, form_number, resolved)
|
|
1856
1924
|
if _is_api_success(result):
|
|
1857
1925
|
result = _attach_rule_readback(cfg, token, form_number, resolved, result)
|
|
1926
|
+
warnings = runtime_action_warnings(resolved)
|
|
1927
|
+
if _is_api_success(result) and warnings:
|
|
1928
|
+
result["runtimeVerification"] = {"verified": False, "warnings": warnings}
|
|
1858
1929
|
if isinstance(result, dict) and trigger_results:
|
|
1859
1930
|
result["triggerPropertyResults"] = trigger_results
|
|
1860
1931
|
return result
|
|
1861
1932
|
|
|
1862
1933
|
|
|
1863
|
-
def
|
|
1934
|
+
def _resolve_same_entity_field(rows, entity_key, selector, error_prefix):
|
|
1864
1935
|
wanted = str(selector or "").strip()
|
|
1865
1936
|
matches = [
|
|
1866
1937
|
row for row in rows
|
|
@@ -1881,15 +1952,37 @@ def _resolve_calculation_field(rows, entity_key, selector):
|
|
|
1881
1952
|
if len(matches) != 1:
|
|
1882
1953
|
error = "UNRESOLVED" if not matches else "AMBIGUOUS"
|
|
1883
1954
|
raise ValueError(
|
|
1884
|
-
f"
|
|
1955
|
+
f"{error_prefix}_FIELD_{error}: {wanted!r} in entity {entity_key!r}"
|
|
1885
1956
|
)
|
|
1886
|
-
|
|
1957
|
+
return matches[0]
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
def _resolve_calculation_field(rows, entity_key, selector):
|
|
1961
|
+
row = _resolve_same_entity_field(
|
|
1962
|
+
rows, entity_key, selector, "FIELD_CALCULATION",
|
|
1963
|
+
)
|
|
1964
|
+
wanted = str(selector or "").strip()
|
|
1965
|
+
field_type = str(row.get("type") or "").strip()
|
|
1887
1966
|
if field_type not in _CALCULATION_NUMERIC_FIELD_TYPES:
|
|
1888
1967
|
raise ValueError(
|
|
1889
1968
|
"FIELD_CALCULATION_NUMERIC_FIELD_REQUIRED: "
|
|
1890
1969
|
f"{wanted!r} is {field_type or 'unknown'}"
|
|
1891
1970
|
)
|
|
1892
|
-
return
|
|
1971
|
+
return row
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
def _resolve_time_difference_operand(rows, entity_key, selector):
|
|
1975
|
+
row = _resolve_same_entity_field(
|
|
1976
|
+
rows, entity_key, selector, "TIME_DIFFERENCE",
|
|
1977
|
+
)
|
|
1978
|
+
wanted = str(selector or "").strip()
|
|
1979
|
+
field_type = str(row.get("type") or "").strip()
|
|
1980
|
+
if field_type != "TimeField":
|
|
1981
|
+
raise ValueError(
|
|
1982
|
+
"TIME_DIFFERENCE_TIME_FIELD_REQUIRED: "
|
|
1983
|
+
f"{wanted!r} is {field_type or 'unknown'}"
|
|
1984
|
+
)
|
|
1985
|
+
return row
|
|
1893
1986
|
|
|
1894
1987
|
|
|
1895
1988
|
def _front_value_action_matches(expected, actual):
|
|
@@ -2051,6 +2144,145 @@ def do_ensure_field_calculation(
|
|
|
2051
2144
|
}
|
|
2052
2145
|
|
|
2053
2146
|
|
|
2147
|
+
def do_ensure_time_difference_calculation(
|
|
2148
|
+
cfg, token, form_number, entity, target, start, end,
|
|
2149
|
+
):
|
|
2150
|
+
"""Ensure a same-entity TimeField difference expressed in decimal hours."""
|
|
2151
|
+
entity_row, rows = _resolve_rule_entity_context(
|
|
2152
|
+
cfg, token, form_number, entity, [target, start, end]
|
|
2153
|
+
)
|
|
2154
|
+
entity_key = str(entity_row.get("key") or "").strip()
|
|
2155
|
+
entity_id = str(entity_row.get("id") or "").strip()
|
|
2156
|
+
target_row = _resolve_calculation_field(rows, entity_key, target)
|
|
2157
|
+
start_row = _resolve_time_difference_operand(rows, entity_key, start)
|
|
2158
|
+
end_row = _resolve_time_difference_operand(rows, entity_key, end)
|
|
2159
|
+
target_key = str(target_row.get("key") or "").strip()
|
|
2160
|
+
start_key = str(start_row.get("key") or "").strip()
|
|
2161
|
+
end_key = str(end_row.get("key") or "").strip()
|
|
2162
|
+
if len({target_key, start_key, end_key}) != 3:
|
|
2163
|
+
raise ValueError(
|
|
2164
|
+
"TIME_DIFFERENCE_DISTINCT_FIELDS_REQUIRED: target, start and end "
|
|
2165
|
+
"must be different fields"
|
|
2166
|
+
)
|
|
2167
|
+
target_name = str(target_row.get("name") or target_key).strip()
|
|
2168
|
+
start_name = str(start_row.get("name") or start_key).strip()
|
|
2169
|
+
end_name = str(end_row.get("name") or end_key).strip()
|
|
2170
|
+
formula = f"{target_key} = ({end_key} - {start_key}) / 3600.0"
|
|
2171
|
+
condition = f"{start_key} >= 0 and {end_key} >= 0"
|
|
2172
|
+
trigger_field = f"{start_name}({start_key});{end_name}({end_key})"
|
|
2173
|
+
description = f"{start_name}、{end_name}非空时计算{target_name}"
|
|
2174
|
+
requested = {
|
|
2175
|
+
"ruleType": "bizRule",
|
|
2176
|
+
"itemId": entity_id,
|
|
2177
|
+
"mountPoint": entity_key,
|
|
2178
|
+
"description": description,
|
|
2179
|
+
"preDescription": description,
|
|
2180
|
+
"preCondition": condition,
|
|
2181
|
+
"triggerField": trigger_field,
|
|
2182
|
+
"enabled": True,
|
|
2183
|
+
"trueActions": [{
|
|
2184
|
+
"actionType": "FormulaAction",
|
|
2185
|
+
"description": f"{target_name} = ({end_name} - {start_name}) / 3600.0",
|
|
2186
|
+
"expression": formula,
|
|
2187
|
+
"ret": 1021,
|
|
2188
|
+
}],
|
|
2189
|
+
"falseActions": [{
|
|
2190
|
+
"actionType": "ClearValueAction",
|
|
2191
|
+
"description": f"时间不完整时清空{target_name}",
|
|
2192
|
+
"expression": target_key,
|
|
2193
|
+
"ret": 1021,
|
|
2194
|
+
}],
|
|
2195
|
+
}
|
|
2196
|
+
current = do_load_full_metadata(cfg, token, form_number)
|
|
2197
|
+
if not _is_api_success(current):
|
|
2198
|
+
raise RuntimeError(
|
|
2199
|
+
"TIME_DIFFERENCE_READ_FAILED: "
|
|
2200
|
+
+ (_extract_error_message(current) or "loadFullMetadata failed")
|
|
2201
|
+
)
|
|
2202
|
+
metadata = current.get("data")
|
|
2203
|
+
target_paths = {target_key, f"{entity_key}.{target_key}"}
|
|
2204
|
+
for row in rows:
|
|
2205
|
+
if str(row.get("parentKey") or "") != entity_key:
|
|
2206
|
+
continue
|
|
2207
|
+
for item in _metadata_items_by_key(metadata, str(row.get("key") or "")):
|
|
2208
|
+
actions = item.get("ExecuteActions") or {}
|
|
2209
|
+
if isinstance(actions, str):
|
|
2210
|
+
actions = json.loads(actions)
|
|
2211
|
+
if isinstance(actions, dict) and any(
|
|
2212
|
+
isinstance(action, dict)
|
|
2213
|
+
and action.get("action") == "updateField"
|
|
2214
|
+
and action.get("target") in target_paths
|
|
2215
|
+
for action in actions.get("valueChange") or []
|
|
2216
|
+
):
|
|
2217
|
+
raise ValueError(
|
|
2218
|
+
f"TIME_DIFFERENCE_TARGET_CONFLICT: {target_key!r} already has "
|
|
2219
|
+
"a field value-change calculation"
|
|
2220
|
+
)
|
|
2221
|
+
semantic = [
|
|
2222
|
+
rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
2223
|
+
if _rule_function_formula_matches(rule, entity_key, formula)
|
|
2224
|
+
]
|
|
2225
|
+
conflicts = [
|
|
2226
|
+
rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
2227
|
+
if _rule_function_formula_conflicts(rule, entity_key, target_key, formula)
|
|
2228
|
+
]
|
|
2229
|
+
if len(semantic) > 1:
|
|
2230
|
+
raise ValueError("TIME_DIFFERENCE_DUPLICATE_RULES")
|
|
2231
|
+
if conflicts:
|
|
2232
|
+
raise ValueError(
|
|
2233
|
+
"TIME_DIFFERENCE_TARGET_CONFLICT: "
|
|
2234
|
+
f"target={target_key!r}, candidateRuleIds="
|
|
2235
|
+
f"{[str(rule.get('Id') or '') for rule in conflicts]!r}"
|
|
2236
|
+
)
|
|
2237
|
+
changed = True
|
|
2238
|
+
if semantic:
|
|
2239
|
+
expected = normalize_rule_config(requested)
|
|
2240
|
+
mismatches = rule_readback_mismatches(expected, semantic[0], metadata)
|
|
2241
|
+
if not mismatches:
|
|
2242
|
+
return {
|
|
2243
|
+
"status": True,
|
|
2244
|
+
"errorCode": "0",
|
|
2245
|
+
"message": "Time difference calculation already matches",
|
|
2246
|
+
"writeAttempted": False,
|
|
2247
|
+
"data": {
|
|
2248
|
+
"changed": False,
|
|
2249
|
+
"ruleId": str(semantic[0].get("Id") or ""),
|
|
2250
|
+
"entityKey": entity_key,
|
|
2251
|
+
"targetField": target_key,
|
|
2252
|
+
"sourceFields": [start_key, end_key],
|
|
2253
|
+
"unit": "hours",
|
|
2254
|
+
"formula": formula,
|
|
2255
|
+
},
|
|
2256
|
+
"verification": {"verified": True, "mismatches": []},
|
|
2257
|
+
}
|
|
2258
|
+
result = do_update_rule_verified(cfg, token, form_number, {
|
|
2259
|
+
"ruleType": "bizRule",
|
|
2260
|
+
"ruleId": str(semantic[0].get("Id") or ""),
|
|
2261
|
+
"description": requested["description"],
|
|
2262
|
+
"preDescription": requested["preDescription"],
|
|
2263
|
+
"preCondition": requested["preCondition"],
|
|
2264
|
+
"triggerField": requested["triggerField"],
|
|
2265
|
+
"enabled": True,
|
|
2266
|
+
"trueActions": requested["trueActions"],
|
|
2267
|
+
"falseActions": requested["falseActions"],
|
|
2268
|
+
})
|
|
2269
|
+
else:
|
|
2270
|
+
result = do_add_rule_verified(cfg, token, form_number, requested)
|
|
2271
|
+
if isinstance(result, dict):
|
|
2272
|
+
result["writeAttempted"] = changed
|
|
2273
|
+
data = result.setdefault("data", {})
|
|
2274
|
+
if isinstance(data, dict):
|
|
2275
|
+
data.update({
|
|
2276
|
+
"changed": changed,
|
|
2277
|
+
"entityKey": entity_key,
|
|
2278
|
+
"targetField": target_key,
|
|
2279
|
+
"sourceFields": [start_key, end_key],
|
|
2280
|
+
"unit": "hours",
|
|
2281
|
+
"formula": formula,
|
|
2282
|
+
})
|
|
2283
|
+
return result
|
|
2284
|
+
|
|
2285
|
+
|
|
2054
2286
|
def _resolve_rule_function_field(rows, entity_key, selector):
|
|
2055
2287
|
wanted = str(selector or "").strip()
|
|
2056
2288
|
matches = [
|
|
@@ -2133,7 +2365,9 @@ def _rule_function_formula_conflicts(rule, entity_key, target_key, formula):
|
|
|
2133
2365
|
)
|
|
2134
2366
|
|
|
2135
2367
|
|
|
2136
|
-
def do_ensure_rule_function_formula(
|
|
2368
|
+
def do_ensure_rule_function_formula(
|
|
2369
|
+
cfg, token, form_number, entity, target, call, add_events=None, events=None,
|
|
2370
|
+
):
|
|
2137
2371
|
"""Compile one released FX call into a same-entity FormulaAction.
|
|
2138
2372
|
|
|
2139
2373
|
The call tree has no raw-expression escape hatch: function IDs resolve against
|
|
@@ -2200,15 +2434,19 @@ def do_ensure_rule_function_formula(cfg, token, form_number, entity, target, cal
|
|
|
2200
2434
|
"actionType": "FormulaAction",
|
|
2201
2435
|
"description": f"{target_key} = {expression}",
|
|
2202
2436
|
"expression": formula,
|
|
2203
|
-
"ret": 253,
|
|
2437
|
+
"ret": _requested_action_ret("FormulaAction", 253, events, add_events),
|
|
2204
2438
|
}],
|
|
2205
2439
|
"falseActions": ([{
|
|
2206
2440
|
"actionType": "ClearValueAction",
|
|
2207
2441
|
"description": f"clear {target_key} when an input is null",
|
|
2208
2442
|
"expression": target_key,
|
|
2209
|
-
"ret": 253,
|
|
2443
|
+
"ret": _requested_action_ret("ClearValueAction", 253, events, add_events),
|
|
2210
2444
|
}] if guarded_source_keys else []),
|
|
2211
2445
|
}
|
|
2446
|
+
legacy_description = description
|
|
2447
|
+
tagged_description = f"{requested['preDescription']} [RFF_{legacy_description.rsplit('_', 1)[1]}]"
|
|
2448
|
+
description = requested["preDescription"]
|
|
2449
|
+
requested["description"] = description
|
|
2212
2450
|
current = do_load_full_metadata(cfg, token, form_number)
|
|
2213
2451
|
if not _is_api_success(current):
|
|
2214
2452
|
raise RuntimeError(
|
|
@@ -2253,10 +2491,29 @@ def do_ensure_rule_function_formula(cfg, token, form_number, entity, target, cal
|
|
|
2253
2491
|
f"{[str(rule.get('Id') or '') for rule in conflicts]!r}"
|
|
2254
2492
|
)
|
|
2255
2493
|
if semantic:
|
|
2494
|
+
timing_result = _reconcile_existing_rule_events(
|
|
2495
|
+
cfg, token, form_number, semantic[0], "bizRule", events, add_events,
|
|
2496
|
+
)
|
|
2497
|
+
if timing_result is not None:
|
|
2498
|
+
return timing_result
|
|
2499
|
+
expected = normalize_rule_config(requested)
|
|
2500
|
+
if events is not None or add_events is not None:
|
|
2501
|
+
for branch, stored_branch in (("trueActions", "TrueActions"),
|
|
2502
|
+
("falseActions", "FalseActions")):
|
|
2503
|
+
for action, stored_action in zip(
|
|
2504
|
+
expected.get(branch) or [], semantic[0].get(stored_branch) or [],
|
|
2505
|
+
):
|
|
2506
|
+
action["RET"] = stored_action["RET"]
|
|
2256
2507
|
mismatches = rule_readback_mismatches(
|
|
2257
|
-
|
|
2508
|
+
expected, semantic[0], metadata
|
|
2258
2509
|
)
|
|
2259
2510
|
if mismatches:
|
|
2511
|
+
if (str(semantic[0].get("Description") or "") in {legacy_description, tagged_description}
|
|
2512
|
+
and all(item.get("path") == "description" for item in mismatches)):
|
|
2513
|
+
return _upgrade_generated_rule_description(
|
|
2514
|
+
cfg, token, form_number, semantic[0],
|
|
2515
|
+
(legacy_description, tagged_description), description,
|
|
2516
|
+
)
|
|
2260
2517
|
if str(semantic[0].get("Description") or "") != description:
|
|
2261
2518
|
raise ValueError(
|
|
2262
2519
|
"RULE_FUNCTION_FORMULA_CONFLICT: "
|
|
@@ -2344,6 +2601,47 @@ def _rule_projections_by_description(value, description):
|
|
|
2344
2601
|
return matches
|
|
2345
2602
|
|
|
2346
2603
|
|
|
2604
|
+
def _upgrade_generated_rule_description(
|
|
2605
|
+
cfg, token, form_number, rule, legacy_description, readable_description,
|
|
2606
|
+
):
|
|
2607
|
+
"""Rename one exact legacy generated rule without replacing its actions."""
|
|
2608
|
+
names = ({legacy_description} if isinstance(legacy_description, str)
|
|
2609
|
+
else set(legacy_description))
|
|
2610
|
+
if str(rule.get("Description") or "") not in names:
|
|
2611
|
+
return None
|
|
2612
|
+
rule_id = str(rule.get("Id") or "").strip()
|
|
2613
|
+
if not rule_id:
|
|
2614
|
+
raise ValueError("RULE_DESCRIPTION_LEGACY_ID_UNAVAILABLE")
|
|
2615
|
+
result = do_update_rule_verified(cfg, token, form_number, {
|
|
2616
|
+
"ruleType": "bizRule", "ruleId": rule_id,
|
|
2617
|
+
"description": readable_description,
|
|
2618
|
+
})
|
|
2619
|
+
if isinstance(result, dict):
|
|
2620
|
+
result["writeAttempted"] = True
|
|
2621
|
+
data = result.setdefault("data", {})
|
|
2622
|
+
if isinstance(data, dict):
|
|
2623
|
+
data.update({
|
|
2624
|
+
"changed": True if _is_api_success(result) else None,
|
|
2625
|
+
"ruleId": rule_id,
|
|
2626
|
+
"renamedGeneratedRule": True if _is_api_success(result) else None,
|
|
2627
|
+
})
|
|
2628
|
+
return result
|
|
2629
|
+
|
|
2630
|
+
|
|
2631
|
+
def _rule_matches_except_generated_action_ids(expected, observed):
|
|
2632
|
+
"""A rule update may regenerate action IDs while retaining its behavior."""
|
|
2633
|
+
if not isinstance(expected, dict) or not isinstance(observed, dict):
|
|
2634
|
+
return False
|
|
2635
|
+
def comparable(rule):
|
|
2636
|
+
result = copy.deepcopy(rule)
|
|
2637
|
+
for branch in ("TrueActions", "FalseActions"):
|
|
2638
|
+
for action in result.get(branch) or []:
|
|
2639
|
+
if isinstance(action, dict):
|
|
2640
|
+
action.pop("Id", None)
|
|
2641
|
+
return result
|
|
2642
|
+
return comparable(expected) == comparable(observed)
|
|
2643
|
+
|
|
2644
|
+
|
|
2347
2645
|
def _all_rule_projections(value, rule_type=None):
|
|
2348
2646
|
matches = []
|
|
2349
2647
|
stack = [value]
|
|
@@ -2461,11 +2759,18 @@ def _resolve_rule_entity_context(
|
|
|
2461
2759
|
str(value or "").strip()
|
|
2462
2760
|
for value in condition_selectors if value not in (None, "")
|
|
2463
2761
|
]
|
|
2762
|
+
action_selectors = [str(value or "").strip() for value in selectors if value not in (None, "")]
|
|
2763
|
+
owner_sets = [{str(row.get("parentKey") or "").strip()
|
|
2764
|
+
for row in _match_field_selector(scoped_field_rows, selector)} for selector in action_selectors]
|
|
2765
|
+
# A unique amount/quantity target can select the entry that owns a repeated price caption.
|
|
2766
|
+
# Infer only when all participating action fields prove the same single owner.
|
|
2767
|
+
common_owners = set.intersection(*owner_sets) if owner_sets and all(owner_sets) else set()
|
|
2768
|
+
if len(common_owners) == 1:
|
|
2769
|
+
scoped_field_rows = [row for row in scoped_field_rows
|
|
2770
|
+
if str(row.get("parentKey") or "").strip() in common_owners]
|
|
2464
2771
|
selected_rows = [
|
|
2465
2772
|
resolve_scoped(selector)
|
|
2466
|
-
for selector in
|
|
2467
|
-
str(value or "").strip() for value in selectors if value not in (None, "")
|
|
2468
|
-
]
|
|
2773
|
+
for selector in action_selectors
|
|
2469
2774
|
]
|
|
2470
2775
|
if not selected_rows:
|
|
2471
2776
|
# No action target names the mount, so the condition field selects it.
|
|
@@ -2683,11 +2988,13 @@ def _repair_legacy_boolean_rule(cfg, token, form_number, metadata, requested,
|
|
|
2683
2988
|
def _repair_first_option_rule(
|
|
2684
2989
|
cfg, token, form_number, metadata, requested, *, rule_type, family,
|
|
2685
2990
|
entity_id, trigger_field, markers, replacement_exists=False,
|
|
2991
|
+
family_marker=None,
|
|
2686
2992
|
):
|
|
2687
2993
|
"""Replace one legacy @first rule occupying the same semantic slot."""
|
|
2688
2994
|
matches = [
|
|
2689
2995
|
rule for rule in _all_rule_projections(metadata, rule_type)
|
|
2690
|
-
if str(rule.get("Description") or "").startswith(family)
|
|
2996
|
+
if (str(rule.get("Description") or "").startswith(family)
|
|
2997
|
+
or (family_marker and family_marker in str(rule.get("Description") or "")))
|
|
2691
2998
|
and "@first" in str(rule.get("PreDescription") or "")
|
|
2692
2999
|
and (
|
|
2693
3000
|
not str(rule.get("ItemId") or "").strip()
|
|
@@ -2741,10 +3048,78 @@ def _repair_first_option_rule(
|
|
|
2741
3048
|
return result
|
|
2742
3049
|
|
|
2743
3050
|
|
|
3051
|
+
def _requested_action_ret(action_type, default_ret, events=None, add_events=None):
|
|
3052
|
+
if events is not None and add_events is not None:
|
|
3053
|
+
raise ValueError("RULE_EVENTS_CONFLICT: choose events or addEvents")
|
|
3054
|
+
if events is not None:
|
|
3055
|
+
return select_action_events(action_type, events)
|
|
3056
|
+
if add_events is not None:
|
|
3057
|
+
return merge_action_events(action_type, default_ret, add_events)
|
|
3058
|
+
return default_ret
|
|
3059
|
+
|
|
3060
|
+
|
|
3061
|
+
def _reconcile_existing_rule_events(
|
|
3062
|
+
cfg, token, form_number, rule, rule_type, events=None, add_events=None,
|
|
3063
|
+
):
|
|
3064
|
+
"""Apply a timing-only update without reconstructing any service or rule metadata."""
|
|
3065
|
+
if events is None and add_events is None:
|
|
3066
|
+
return None
|
|
3067
|
+
if events is not None and add_events is not None:
|
|
3068
|
+
raise ValueError("RULE_EVENTS_CONFLICT: choose events or addEvents")
|
|
3069
|
+
stored = copy.deepcopy(rule)
|
|
3070
|
+
rule_id = str(stored.get("Id") or "").strip()
|
|
3071
|
+
if not rule_id:
|
|
3072
|
+
raise ValueError("RULE_EVENT_RULE_ID_MISSING")
|
|
3073
|
+
for branch in ("TrueActions", "FalseActions"):
|
|
3074
|
+
for action in stored.get(branch) or []:
|
|
3075
|
+
current = action.get("RET")
|
|
3076
|
+
kind = action.get("ActionType")
|
|
3077
|
+
if events is not None:
|
|
3078
|
+
requested = select_action_events(kind, events)
|
|
3079
|
+
if current != requested:
|
|
3080
|
+
return {
|
|
3081
|
+
"status": False, "errorCode": "RULE_EVENT_REPLACE_REQUIRES_RULE_ID",
|
|
3082
|
+
"message": "Replacing timings on an existing rule requires an exact ruleId",
|
|
3083
|
+
"writeAttempted": False,
|
|
3084
|
+
"data": {"ruleId": rule_id, "actionType": kind,
|
|
3085
|
+
"currentRET": current, "requestedRET": requested},
|
|
3086
|
+
}
|
|
3087
|
+
if add_events is not None:
|
|
3088
|
+
action["RET"] = merge_action_events(kind, current, add_events)
|
|
3089
|
+
if _rule_matches_except_generated_action_ids(rule, stored):
|
|
3090
|
+
return None
|
|
3091
|
+
patch = {"ruleType": rule_type, "ruleId": rule_id,
|
|
3092
|
+
"trueActions": stored.get("TrueActions") or [],
|
|
3093
|
+
"falseActions": stored.get("FalseActions") or []}
|
|
3094
|
+
result = do_update_rule_verified(cfg, token, form_number, patch)
|
|
3095
|
+
if not _is_api_success(result):
|
|
3096
|
+
return result
|
|
3097
|
+
try:
|
|
3098
|
+
readback = do_load_full_metadata(cfg, token, form_number)
|
|
3099
|
+
except Exception as exc:
|
|
3100
|
+
readback = {"status": False, "message": str(exc)}
|
|
3101
|
+
observed = (_find_rule_projection(readback.get("data"), rule_id)
|
|
3102
|
+
if _is_api_success(readback) else None)
|
|
3103
|
+
if not _rule_matches_except_generated_action_ids(stored, observed):
|
|
3104
|
+
return {
|
|
3105
|
+
"status": False, "errorCode": "RULE_EVENT_READBACK_MISMATCH",
|
|
3106
|
+
"message": "Timing update could not verify preservation of the complete rule",
|
|
3107
|
+
"writeAttempted": True, "retryable": False,
|
|
3108
|
+
"data": {"changed": None, "ruleId": rule_id},
|
|
3109
|
+
"verification": {"verified": False, "preservationVerified": False},
|
|
3110
|
+
}
|
|
3111
|
+
result.setdefault("data", {}).update({"changed": True, "ruleId": rule_id})
|
|
3112
|
+
result["writeAttempted"] = True
|
|
3113
|
+
result.setdefault("verification", {}).update({
|
|
3114
|
+
"verified": True, "preservationVerified": True,
|
|
3115
|
+
})
|
|
3116
|
+
return result
|
|
3117
|
+
|
|
3118
|
+
|
|
2744
3119
|
@mutation_contract
|
|
2745
3120
|
def do_ensure_conditional_field_state(
|
|
2746
3121
|
cfg, token, form_number, entity, condition_field, operator, value,
|
|
2747
|
-
target, behavior,
|
|
3122
|
+
target, behavior, add_events=None, events=None,
|
|
2748
3123
|
):
|
|
2749
3124
|
"""Ensure one reversible conditional rule for one or many same-entity fields."""
|
|
2750
3125
|
action_pair = _CONDITIONAL_STATE_ACTIONS.get(
|
|
@@ -2754,6 +3129,8 @@ def do_ensure_conditional_field_state(
|
|
|
2754
3129
|
raise ValueError(
|
|
2755
3130
|
"CONDITIONAL_FIELD_STATE_BEHAVIOR_UNSUPPORTED: use show, hide, lock, or unlock"
|
|
2756
3131
|
)
|
|
3132
|
+
if events is not None and add_events is not None:
|
|
3133
|
+
raise ValueError("RULE_EVENTS_CONFLICT: choose exact events or add-events")
|
|
2757
3134
|
targets = [target] if isinstance(target, str) else list(target or [])
|
|
2758
3135
|
if not targets or any(not str(item or "").strip() for item in targets):
|
|
2759
3136
|
raise ValueError("CONDITIONAL_FIELD_STATE_TARGET_REQUIRED")
|
|
@@ -2784,6 +3161,12 @@ def do_ensure_conditional_field_state(
|
|
|
2784
3161
|
))
|
|
2785
3162
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
2786
3163
|
true_action, false_action = action_pair
|
|
3164
|
+
true_ret = (select_action_events(true_action, events) if events is not None
|
|
3165
|
+
else merge_action_events(true_action, 251, add_events)
|
|
3166
|
+
if add_events is not None else 251)
|
|
3167
|
+
false_ret = (select_action_events(false_action, events) if events is not None
|
|
3168
|
+
else merge_action_events(false_action, 251, add_events)
|
|
3169
|
+
if add_events is not None else 251)
|
|
2787
3170
|
condition_phrase = (
|
|
2788
3171
|
f"{condition_name} {'为空' if compiled_operator == 'empty' else '不为空'}"
|
|
2789
3172
|
if compiled_operator in {"empty", "not-empty"}
|
|
@@ -2811,11 +3194,13 @@ def do_ensure_conditional_field_state(
|
|
|
2811
3194
|
"actionType": true_action,
|
|
2812
3195
|
"description": f"{behavior_label}{target_phrase}",
|
|
2813
3196
|
"fields": target_keys,
|
|
3197
|
+
"ret": true_ret,
|
|
2814
3198
|
}],
|
|
2815
3199
|
"falseActions": [{
|
|
2816
3200
|
"actionType": false_action,
|
|
2817
3201
|
"description": f"{inverse_label}{target_phrase}",
|
|
2818
3202
|
"fields": target_keys,
|
|
3203
|
+
"ret": false_ret,
|
|
2819
3204
|
}],
|
|
2820
3205
|
}
|
|
2821
3206
|
|
|
@@ -2834,6 +3219,12 @@ def do_ensure_conditional_field_state(
|
|
|
2834
3219
|
)
|
|
2835
3220
|
expected = normalize_rule_config(requested)
|
|
2836
3221
|
if matches:
|
|
3222
|
+
if add_events is not None:
|
|
3223
|
+
for branch, stored_branch in (("trueActions", "TrueActions"),
|
|
3224
|
+
("falseActions", "FalseActions")):
|
|
3225
|
+
actions = matches[0].get(stored_branch) or []
|
|
3226
|
+
if len(actions) == 1 and "RET" in actions[0]:
|
|
3227
|
+
expected[branch][0]["RET"] = actions[0]["RET"]
|
|
2837
3228
|
mismatches = rule_readback_mismatches(expected, matches[0], metadata)
|
|
2838
3229
|
if mismatches:
|
|
2839
3230
|
raise ValueError(
|
|
@@ -2841,6 +3232,47 @@ def do_ensure_conditional_field_state(
|
|
|
2841
3232
|
f"already exists with different metadata; mismatches="
|
|
2842
3233
|
f"{json.dumps(mismatches, ensure_ascii=False)}"
|
|
2843
3234
|
)
|
|
3235
|
+
if add_events is not None:
|
|
3236
|
+
stored = copy.deepcopy(matches[0])
|
|
3237
|
+
rule_id = str(stored.get("Id") or "").strip()
|
|
3238
|
+
if not rule_id:
|
|
3239
|
+
raise ValueError("RULE_EVENT_RULE_ID_MISSING")
|
|
3240
|
+
for branch in ("TrueActions", "FalseActions"):
|
|
3241
|
+
action = stored[branch][0]
|
|
3242
|
+
action["RET"] = merge_action_events(
|
|
3243
|
+
action["ActionType"], action["RET"], add_events,
|
|
3244
|
+
)
|
|
3245
|
+
if _rule_matches_except_generated_action_ids(matches[0], stored):
|
|
3246
|
+
return {"status": True, "writeAttempted": False,
|
|
3247
|
+
"data": {"changed": False, "ruleId": rule_id},
|
|
3248
|
+
"verification": {"verified": True, "preservationVerified": True}}
|
|
3249
|
+
result = do_update_rule_verified(cfg, token, form_number, {
|
|
3250
|
+
"ruleType": "formRule", "ruleId": rule_id,
|
|
3251
|
+
"trueActions": stored["TrueActions"],
|
|
3252
|
+
"falseActions": stored["FalseActions"],
|
|
3253
|
+
})
|
|
3254
|
+
if not _is_api_success(result):
|
|
3255
|
+
return result
|
|
3256
|
+
try:
|
|
3257
|
+
readback = do_load_full_metadata(cfg, token, form_number)
|
|
3258
|
+
except Exception as exc:
|
|
3259
|
+
readback = {"status": False, "message": str(exc)}
|
|
3260
|
+
observed = (_find_rule_projection(readback.get("data"), rule_id)
|
|
3261
|
+
if _is_api_success(readback) else None)
|
|
3262
|
+
if not _rule_matches_except_generated_action_ids(stored, observed):
|
|
3263
|
+
return {"status": False, "errorCode": "RULE_EVENT_READBACK_MISMATCH",
|
|
3264
|
+
"message": "Event update could not verify preservation of the complete rule",
|
|
3265
|
+
"writeAttempted": True, "retryable": False,
|
|
3266
|
+
"data": {"changed": None, "ruleId": rule_id},
|
|
3267
|
+
"verification": {"verified": False, "preservationVerified": False}}
|
|
3268
|
+
result.setdefault("data", {}).update({"changed": True, "ruleId": rule_id})
|
|
3269
|
+
result["writeAttempted"] = True
|
|
3270
|
+
result.setdefault("verification", {}).update({
|
|
3271
|
+
"verified": True, "preservationVerified": True,
|
|
3272
|
+
"trueRET": stored["TrueActions"][0]["RET"],
|
|
3273
|
+
"falseRET": stored["FalseActions"][0]["RET"],
|
|
3274
|
+
})
|
|
3275
|
+
return result
|
|
2844
3276
|
cleaned = _repair_first_option_rule(
|
|
2845
3277
|
cfg, token, form_number, metadata, requested,
|
|
2846
3278
|
rule_type="FormRule", family="ConditionalFieldState_",
|
|
@@ -2966,7 +3398,7 @@ def do_ensure_conditional_field_state(
|
|
|
2966
3398
|
|
|
2967
3399
|
def do_ensure_conditional_control_state(
|
|
2968
3400
|
cfg, token, form_number, entity, condition_field, operator, value,
|
|
2969
|
-
target_control, behavior,
|
|
3401
|
+
target_control, behavior, add_events=None, events=None,
|
|
2970
3402
|
):
|
|
2971
3403
|
"""Ensure one reversible conditional control hide/lock rule."""
|
|
2972
3404
|
action_pair = _CONDITIONAL_CONTROL_ACTIONS.get(
|
|
@@ -3003,6 +3435,8 @@ def do_ensure_conditional_control_state(
|
|
|
3003
3435
|
identity.encode("utf-8")
|
|
3004
3436
|
).hexdigest()[:12]
|
|
3005
3437
|
true_action, false_action = action_pair
|
|
3438
|
+
true_ret = _requested_action_ret(true_action, 251, events, add_events)
|
|
3439
|
+
false_ret = _requested_action_ret(false_action, 251, events, add_events)
|
|
3006
3440
|
requested = {
|
|
3007
3441
|
"ruleType": "formRule",
|
|
3008
3442
|
"itemId": context["entityId"],
|
|
@@ -3019,11 +3453,13 @@ def do_ensure_conditional_control_state(
|
|
|
3019
3453
|
"actionType": true_action,
|
|
3020
3454
|
"description": f"{str(behavior).strip().casefold()} {control_name}",
|
|
3021
3455
|
"fields": [control_key],
|
|
3456
|
+
"ret": true_ret,
|
|
3022
3457
|
}],
|
|
3023
3458
|
"falseActions": [{
|
|
3024
3459
|
"actionType": false_action,
|
|
3025
3460
|
"description": f"restore {control_name}",
|
|
3026
3461
|
"fields": [control_key],
|
|
3462
|
+
"ret": false_ret,
|
|
3027
3463
|
}],
|
|
3028
3464
|
}
|
|
3029
3465
|
current = do_load_full_metadata(cfg, token, form_number)
|
|
@@ -3041,6 +3477,15 @@ def do_ensure_conditional_control_state(
|
|
|
3041
3477
|
)
|
|
3042
3478
|
expected = normalize_rule_config(requested)
|
|
3043
3479
|
if matches:
|
|
3480
|
+
timing_result = _reconcile_existing_rule_events(
|
|
3481
|
+
cfg, token, form_number, matches[0], "formRule", events, add_events,
|
|
3482
|
+
)
|
|
3483
|
+
if timing_result is not None:
|
|
3484
|
+
return timing_result
|
|
3485
|
+
if events is not None or add_events is not None:
|
|
3486
|
+
for branch, stored_branch in (("trueActions", "TrueActions"),
|
|
3487
|
+
("falseActions", "FalseActions")):
|
|
3488
|
+
expected[branch][0]["RET"] = matches[0][stored_branch][0]["RET"]
|
|
3044
3489
|
mismatches = rule_readback_mismatches(expected, matches[0], metadata)
|
|
3045
3490
|
if mismatches:
|
|
3046
3491
|
raise ValueError(
|
|
@@ -3312,6 +3757,7 @@ def do_ensure_conditional_client_appearance(cfg, token, form_number, request):
|
|
|
3312
3757
|
raise ValueError("CONDITIONAL_CLIENT_APPEARANCE_INPUT_INVALID: input must be an object")
|
|
3313
3758
|
allowed = {
|
|
3314
3759
|
"clientMount", "entity", "conditionField", "operator", "value", "effects",
|
|
3760
|
+
"events", "addEvents",
|
|
3315
3761
|
}
|
|
3316
3762
|
unknown = sorted(set(request) - allowed)
|
|
3317
3763
|
if unknown:
|
|
@@ -3352,6 +3798,10 @@ def do_ensure_conditional_client_appearance(cfg, token, form_number, request):
|
|
|
3352
3798
|
description = "ConditionalClientAppearance_" + hashlib.sha1(
|
|
3353
3799
|
identity.encode("utf-8")
|
|
3354
3800
|
).hexdigest()[:12]
|
|
3801
|
+
for action in actions:
|
|
3802
|
+
action["ret"] = _requested_action_ret(
|
|
3803
|
+
action["actionType"], 251, request.get("events"), request.get("addEvents"),
|
|
3804
|
+
)
|
|
3355
3805
|
requested = {
|
|
3356
3806
|
"ruleType": "formRule",
|
|
3357
3807
|
"clientMount": client_mount,
|
|
@@ -3382,9 +3832,19 @@ def do_ensure_conditional_client_appearance(cfg, token, form_number, request):
|
|
|
3382
3832
|
cfg, token, form_number, requested
|
|
3383
3833
|
)
|
|
3384
3834
|
if matches:
|
|
3385
|
-
|
|
3386
|
-
|
|
3835
|
+
timing_result = _reconcile_existing_rule_events(
|
|
3836
|
+
cfg, token, form_number, matches[0], "formRule",
|
|
3837
|
+
request.get("events"), request.get("addEvents"),
|
|
3387
3838
|
)
|
|
3839
|
+
if timing_result is not None:
|
|
3840
|
+
return timing_result
|
|
3841
|
+
expected = normalize_rule_config(resolved_requested)
|
|
3842
|
+
if request.get("events") is not None or request.get("addEvents") is not None:
|
|
3843
|
+
for action, stored_action in zip(
|
|
3844
|
+
expected.get("trueActions") or [], matches[0].get("TrueActions") or [],
|
|
3845
|
+
):
|
|
3846
|
+
action["RET"] = stored_action["RET"]
|
|
3847
|
+
mismatches = rule_readback_mismatches(expected, matches[0], metadata)
|
|
3388
3848
|
if not mismatches:
|
|
3389
3849
|
return {
|
|
3390
3850
|
"status": True, "errorCode": "0",
|
|
@@ -3416,11 +3876,159 @@ def do_ensure_conditional_client_appearance(cfg, token, form_number, request):
|
|
|
3416
3876
|
return result
|
|
3417
3877
|
|
|
3418
3878
|
|
|
3879
|
+
def do_ensure_conditional_field_change(
|
|
3880
|
+
cfg, token, form_number, entity, condition_field, operator, value,
|
|
3881
|
+
target, add_events=None, events=None,
|
|
3882
|
+
):
|
|
3883
|
+
"""Ensure one conditional field-change service action with typed selectors."""
|
|
3884
|
+
if events is not None and add_events is not None:
|
|
3885
|
+
raise ValueError("RULE_EVENTS_CONFLICT: choose exact events or add-events")
|
|
3886
|
+
event_ret = (select_action_events("CallFieldChangeAction", events)
|
|
3887
|
+
if events is not None else
|
|
3888
|
+
merge_action_events("CallFieldChangeAction", 773, add_events)
|
|
3889
|
+
if add_events is not None else 773)
|
|
3890
|
+
context = _resolve_conditional_context(
|
|
3891
|
+
cfg, token, form_number, entity, condition_field, operator, value,
|
|
3892
|
+
target=target,
|
|
3893
|
+
)
|
|
3894
|
+
target_key = context["targetKey"]
|
|
3895
|
+
target_name = context["targetName"]
|
|
3896
|
+
condition_key = context["conditionKey"]
|
|
3897
|
+
condition_name = context["conditionName"]
|
|
3898
|
+
condition_phrase = (
|
|
3899
|
+
f"{condition_name}{'为空' if context['operator'] == 'empty' else '不为空'}"
|
|
3900
|
+
if context["operator"] in {"empty", "not-empty"}
|
|
3901
|
+
else f"{condition_name} {context['operator']} {context['requestedValue']}"
|
|
3902
|
+
)
|
|
3903
|
+
pre_description = f"{condition_phrase}时调用{target_name}字段值更新服务"
|
|
3904
|
+
identity = ":".join((form_number, context["entityKey"], context["expression"], target_key))
|
|
3905
|
+
tagged_description = pre_description + " [CFC_" + hashlib.sha1(
|
|
3906
|
+
identity.encode("utf-8")
|
|
3907
|
+
).hexdigest()[:12] + "]"
|
|
3908
|
+
description = pre_description
|
|
3909
|
+
requested = {
|
|
3910
|
+
"ruleType": "bizRule", "itemId": context["entityId"],
|
|
3911
|
+
"mountPoint": context["entityKey"], "description": description,
|
|
3912
|
+
"preDescription": pre_description,
|
|
3913
|
+
"preCondition": context["expression"],
|
|
3914
|
+
"triggerField": f"{condition_name}({condition_key})",
|
|
3915
|
+
"enabled": True,
|
|
3916
|
+
"trueActions": [{
|
|
3917
|
+
"actionType": "CallFieldChangeAction", "expression": target_key,
|
|
3918
|
+
"description": f"调用{target_name}字段值更新服务", "ret": event_ret,
|
|
3919
|
+
}], "falseActions": [],
|
|
3920
|
+
}
|
|
3921
|
+
current = do_load_full_metadata(cfg, token, form_number)
|
|
3922
|
+
if not _is_api_success(current):
|
|
3923
|
+
raise RuntimeError("CONDITIONAL_FIELD_CHANGE_READ_FAILED: "
|
|
3924
|
+
+ (_extract_error_message(current) or "loadFullMetadata failed"))
|
|
3925
|
+
metadata = current.get("data")
|
|
3926
|
+
related = [
|
|
3927
|
+
rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
3928
|
+
if str(rule.get("TriggerField") or "") == requested["triggerField"]
|
|
3929
|
+
and str(rule.get("ItemId") or context["entityId"]) == context["entityId"]
|
|
3930
|
+
and not (rule.get("FalseActions") or [])
|
|
3931
|
+
and len(rule.get("TrueActions") or []) == 1
|
|
3932
|
+
and (rule.get("TrueActions") or [{}])[0].get("ActionType") == "CallFieldChangeAction"
|
|
3933
|
+
and str((rule.get("TrueActions") or [{}])[0].get("Expression") or "") == target_key
|
|
3934
|
+
]
|
|
3935
|
+
matches = [rule for rule in related
|
|
3936
|
+
if str(rule.get("PreCondition") or "") == requested["preCondition"]]
|
|
3937
|
+
if len(matches) > 1:
|
|
3938
|
+
raise ValueError("CONDITIONAL_FIELD_CHANGE_DUPLICATE_RULES: select the intended ruleId")
|
|
3939
|
+
if not matches and related:
|
|
3940
|
+
return {
|
|
3941
|
+
"status": False, "errorCode": "RULE_CONDITION_UPDATE_REQUIRES_RULE_ID",
|
|
3942
|
+
"message": "A rule already calls the same field from this trigger; select the existing ruleId before changing its condition",
|
|
3943
|
+
"writeAttempted": False,
|
|
3944
|
+
"data": {"candidates": [{"ruleId": rule.get("Id"),
|
|
3945
|
+
"preCondition": rule.get("PreCondition")}
|
|
3946
|
+
for rule in related]},
|
|
3947
|
+
}
|
|
3948
|
+
if matches:
|
|
3949
|
+
stored = copy.deepcopy(matches[0])
|
|
3950
|
+
rule_id = str(stored.get("Id") or "").strip()
|
|
3951
|
+
if not rule_id:
|
|
3952
|
+
raise ValueError("CONDITIONAL_FIELD_CHANGE_RULE_ID_MISSING")
|
|
3953
|
+
generated_name = stored.get("Description") == tagged_description
|
|
3954
|
+
if generated_name:
|
|
3955
|
+
stored["Description"] = description
|
|
3956
|
+
if events is not None and stored["TrueActions"][0].get("RET") != event_ret:
|
|
3957
|
+
return {"status": False, "errorCode": "RULE_EVENT_REPLACE_REQUIRES_RULE_ID",
|
|
3958
|
+
"message": "Replacing timings on an existing rule requires an exact ruleId",
|
|
3959
|
+
"writeAttempted": False,
|
|
3960
|
+
"data": {"ruleId": rule_id, "currentRET": stored["TrueActions"][0].get("RET"),
|
|
3961
|
+
"requestedRET": event_ret}}
|
|
3962
|
+
if add_events is not None:
|
|
3963
|
+
action = stored["TrueActions"][0]
|
|
3964
|
+
action["RET"] = merge_action_events(
|
|
3965
|
+
"CallFieldChangeAction", action["RET"], add_events,
|
|
3966
|
+
)
|
|
3967
|
+
if not _rule_matches_except_generated_action_ids(matches[0], stored):
|
|
3968
|
+
patch = {"ruleType": "bizRule", "ruleId": rule_id}
|
|
3969
|
+
if generated_name:
|
|
3970
|
+
patch["description"] = description
|
|
3971
|
+
if stored["TrueActions"] != matches[0].get("TrueActions"):
|
|
3972
|
+
patch["trueActions"] = stored["TrueActions"]
|
|
3973
|
+
result = do_update_rule_verified(cfg, token, form_number, patch)
|
|
3974
|
+
if not _is_api_success(result):
|
|
3975
|
+
return result
|
|
3976
|
+
try:
|
|
3977
|
+
readback = do_load_full_metadata(cfg, token, form_number)
|
|
3978
|
+
except Exception as exc:
|
|
3979
|
+
readback = {"status": False, "message": str(exc)}
|
|
3980
|
+
observed = (_find_rule_projection(readback.get("data"), rule_id)
|
|
3981
|
+
if _is_api_success(readback) else None)
|
|
3982
|
+
if not _rule_matches_except_generated_action_ids(stored, observed):
|
|
3983
|
+
return {"status": False, "errorCode": "RULE_EVENT_READBACK_MISMATCH",
|
|
3984
|
+
"message": "Field-change event update could not verify the complete rule",
|
|
3985
|
+
"writeAttempted": True, "retryable": False,
|
|
3986
|
+
"data": {"changed": None, "ruleId": rule_id},
|
|
3987
|
+
"verification": {"verified": False, "preservationVerified": False}}
|
|
3988
|
+
result.setdefault("data", {}).update({
|
|
3989
|
+
"changed": True, "ruleId": rule_id,
|
|
3990
|
+
"targetField": target_key, "condition": context["expression"],
|
|
3991
|
+
})
|
|
3992
|
+
result["writeAttempted"] = True
|
|
3993
|
+
result.setdefault("verification", {}).update({
|
|
3994
|
+
"verified": True, "preservationVerified": True,
|
|
3995
|
+
"ret": stored["TrueActions"][0]["RET"],
|
|
3996
|
+
})
|
|
3997
|
+
return result
|
|
3998
|
+
return {
|
|
3999
|
+
"status": True, "errorCode": "0", "writeAttempted": False,
|
|
4000
|
+
"data": {"changed": False, "ruleId": rule_id,
|
|
4001
|
+
"targetField": target_key, "condition": context["expression"]},
|
|
4002
|
+
"verification": {"scope": "design-metadata", "verified": True,
|
|
4003
|
+
"preservationVerified": True},
|
|
4004
|
+
"runtimeVerification": {"verified": False,
|
|
4005
|
+
"warnings": runtime_action_warnings(requested)},
|
|
4006
|
+
}
|
|
4007
|
+
result = do_add_rule_verified(cfg, token, form_number, requested)
|
|
4008
|
+
if isinstance(result, dict):
|
|
4009
|
+
result["writeAttempted"] = True
|
|
4010
|
+
data = result.setdefault("data", {})
|
|
4011
|
+
if isinstance(data, dict):
|
|
4012
|
+
data.update({"changed": True if _is_api_success(result) else None,
|
|
4013
|
+
"entityKey": context["entityKey"],
|
|
4014
|
+
"condition": context["expression"],
|
|
4015
|
+
"targetField": target_key})
|
|
4016
|
+
return result
|
|
4017
|
+
|
|
4018
|
+
|
|
3419
4019
|
def do_ensure_conditional_clear_fields(
|
|
3420
4020
|
cfg, token, form_number, entity, condition_field, operator, value,
|
|
3421
|
-
targets,
|
|
4021
|
+
targets, add_events=None, events=None,
|
|
3422
4022
|
):
|
|
3423
4023
|
"""Ensure one entity-scoped conditional ClearValueAction rule."""
|
|
4024
|
+
if events is not None and add_events is not None:
|
|
4025
|
+
raise ValueError("RULE_EVENTS_CONFLICT: choose exact events or add-events")
|
|
4026
|
+
event_ret = (
|
|
4027
|
+
select_action_events("ClearValueAction", events)
|
|
4028
|
+
if events is not None else
|
|
4029
|
+
merge_action_events("ClearValueAction", 253, add_events)
|
|
4030
|
+
if add_events is not None else 253
|
|
4031
|
+
)
|
|
3424
4032
|
selectors = [str(item or "").strip() for item in (targets or [])]
|
|
3425
4033
|
selectors = [item for item in selectors if item]
|
|
3426
4034
|
if not selectors:
|
|
@@ -3445,18 +4053,20 @@ def do_ensure_conditional_clear_fields(
|
|
|
3445
4053
|
identity = ":".join((
|
|
3446
4054
|
form_number, context["entityKey"], context["expression"], expression,
|
|
3447
4055
|
))
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
4056
|
+
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
4057
|
+
legacy_description = "ConditionalClearFields_" + digest
|
|
4058
|
+
pre_description = (
|
|
4059
|
+
f"{context['conditionName']} {context['operator']} "
|
|
4060
|
+
f"{context['requestedValue']} 时清空{'、'.join(target_names)}"
|
|
4061
|
+
)
|
|
4062
|
+
tagged_description = f"{pre_description} [CCF_{digest}]"
|
|
4063
|
+
description = pre_description
|
|
3451
4064
|
requested = {
|
|
3452
4065
|
"ruleType": "bizRule",
|
|
3453
4066
|
"itemId": context["entityId"],
|
|
3454
4067
|
"mountPoint": context["entityKey"],
|
|
3455
4068
|
"description": description,
|
|
3456
|
-
"preDescription":
|
|
3457
|
-
f"{context['conditionName']} {context['operator']} "
|
|
3458
|
-
f"{context['requestedValue']} 时清空{'、'.join(target_names)}"
|
|
3459
|
-
),
|
|
4069
|
+
"preDescription": pre_description,
|
|
3460
4070
|
"preCondition": context["expression"],
|
|
3461
4071
|
"triggerField": f"{context['conditionName']}({context['conditionKey']})",
|
|
3462
4072
|
"enabled": True,
|
|
@@ -3464,7 +4074,7 @@ def do_ensure_conditional_clear_fields(
|
|
|
3464
4074
|
"actionType": "ClearValueAction",
|
|
3465
4075
|
"description": "清除指定字段值",
|
|
3466
4076
|
"expression": expression,
|
|
3467
|
-
"ret":
|
|
4077
|
+
"ret": event_ret,
|
|
3468
4078
|
}],
|
|
3469
4079
|
"falseActions": [],
|
|
3470
4080
|
}
|
|
@@ -3473,7 +4083,8 @@ def do_ensure_conditional_clear_fields(
|
|
|
3473
4083
|
requested_targets = set(target_keys)
|
|
3474
4084
|
predecessors = []
|
|
3475
4085
|
for rule in _all_rule_projections(metadata, "BizRule"):
|
|
3476
|
-
|
|
4086
|
+
text = str(rule.get("Description") or "")
|
|
4087
|
+
if not (text.startswith("ConditionalClearFields_") or " [CCF_" in text):
|
|
3477
4088
|
continue
|
|
3478
4089
|
if str(rule.get("PreCondition") or "") != requested["preCondition"]:
|
|
3479
4090
|
continue
|
|
@@ -3506,7 +4117,16 @@ def do_ensure_conditional_clear_fields(
|
|
|
3506
4117
|
+ (_extract_error_message(current) or "loadFullMetadata failed")
|
|
3507
4118
|
)
|
|
3508
4119
|
metadata = current.get("data")
|
|
3509
|
-
matches =
|
|
4120
|
+
matches = [
|
|
4121
|
+
rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
4122
|
+
if str(rule.get("PreCondition") or "") == requested["preCondition"]
|
|
4123
|
+
and str(rule.get("TriggerField") or "") == requested["triggerField"]
|
|
4124
|
+
and str(rule.get("ItemId") or context["entityId"]) == context["entityId"]
|
|
4125
|
+
and not (rule.get("FalseActions") or [])
|
|
4126
|
+
and len(rule.get("TrueActions") or []) == 1
|
|
4127
|
+
and (rule.get("TrueActions") or [{}])[0].get("ActionType") == "ClearValueAction"
|
|
4128
|
+
and str((rule.get("TrueActions") or [{}])[0].get("Expression") or "") == expression
|
|
4129
|
+
]
|
|
3510
4130
|
if len(matches) > 1:
|
|
3511
4131
|
raise ValueError(
|
|
3512
4132
|
"CONDITIONAL_CLEAR_FIELDS_DUPLICATE_RULES: "
|
|
@@ -3514,6 +4134,22 @@ def do_ensure_conditional_clear_fields(
|
|
|
3514
4134
|
)
|
|
3515
4135
|
expected = normalize_rule_config(requested)
|
|
3516
4136
|
if matches:
|
|
4137
|
+
current_ret = (matches[0].get("TrueActions") or [{}])[0].get("RET")
|
|
4138
|
+
if events is not None and current_ret != event_ret:
|
|
4139
|
+
return {
|
|
4140
|
+
"status": False, "errorCode": "RULE_EVENT_REPLACE_REQUIRES_RULE_ID",
|
|
4141
|
+
"message": "Replacing timings on an existing rule requires an exact ruleId",
|
|
4142
|
+
"writeAttempted": False,
|
|
4143
|
+
"data": {"ruleId": matches[0].get("Id"),
|
|
4144
|
+
"currentRET": current_ret, "requestedRET": event_ret},
|
|
4145
|
+
}
|
|
4146
|
+
expected["description"] = matches[0].get("Description")
|
|
4147
|
+
expected["preDescription"] = matches[0].get("PreDescription")
|
|
4148
|
+
current_actions = matches[0].get("TrueActions") or []
|
|
4149
|
+
if len(current_actions) == 1 and "RET" in current_actions[0]:
|
|
4150
|
+
expected["trueActions"][0]["RET"] = current_actions[0]["RET"]
|
|
4151
|
+
expected["trueActions"][0]["Description"] = current_actions[0].get("Description")
|
|
4152
|
+
expected["trueActions"][0]["Seq"] = current_actions[0].get("Seq", 1)
|
|
3517
4153
|
mismatches = rule_readback_mismatches(expected, matches[0], metadata)
|
|
3518
4154
|
if mismatches:
|
|
3519
4155
|
raise ValueError(
|
|
@@ -3521,11 +4157,64 @@ def do_ensure_conditional_clear_fields(
|
|
|
3521
4157
|
f"already exists with different metadata; mismatches="
|
|
3522
4158
|
f"{json.dumps(mismatches, ensure_ascii=False)}"
|
|
3523
4159
|
)
|
|
4160
|
+
generated_name = matches[0].get("Description") in {
|
|
4161
|
+
legacy_description, tagged_description,
|
|
4162
|
+
}
|
|
4163
|
+
if add_events is not None or generated_name:
|
|
4164
|
+
stored = copy.deepcopy(matches[0])
|
|
4165
|
+
rule_id = str(stored.get("Id") or "").strip()
|
|
4166
|
+
if not rule_id:
|
|
4167
|
+
raise ValueError("RULE_EVENT_RULE_ID_MISSING")
|
|
4168
|
+
if generated_name:
|
|
4169
|
+
stored["Description"] = description
|
|
4170
|
+
if add_events is not None:
|
|
4171
|
+
action = stored["TrueActions"][0]
|
|
4172
|
+
action["RET"] = merge_action_events(
|
|
4173
|
+
"ClearValueAction", action["RET"], add_events,
|
|
4174
|
+
)
|
|
4175
|
+
if stored == matches[0]:
|
|
4176
|
+
return {
|
|
4177
|
+
"status": True, "writeAttempted": False,
|
|
4178
|
+
"data": {"changed": False, "ruleId": rule_id},
|
|
4179
|
+
"verification": {"verified": True, "preservationVerified": True},
|
|
4180
|
+
}
|
|
4181
|
+
patch = {"ruleType": "bizRule", "ruleId": rule_id}
|
|
4182
|
+
if generated_name:
|
|
4183
|
+
patch["description"] = description
|
|
4184
|
+
if stored["TrueActions"] != matches[0].get("TrueActions"):
|
|
4185
|
+
patch["trueActions"] = stored["TrueActions"]
|
|
4186
|
+
result = do_update_rule_verified(cfg, token, form_number, patch)
|
|
4187
|
+
if not _is_api_success(result):
|
|
4188
|
+
return result
|
|
4189
|
+
try:
|
|
4190
|
+
readback = do_load_full_metadata(cfg, token, form_number)
|
|
4191
|
+
except Exception as exc:
|
|
4192
|
+
readback = {"status": False, "message": str(exc)}
|
|
4193
|
+
observed = (
|
|
4194
|
+
_find_rule_projection(readback.get("data"), rule_id)
|
|
4195
|
+
if _is_api_success(readback) else None
|
|
4196
|
+
)
|
|
4197
|
+
if not _rule_matches_except_generated_action_ids(stored, observed):
|
|
4198
|
+
return {
|
|
4199
|
+
"status": False, "errorCode": "RULE_EVENT_READBACK_MISMATCH",
|
|
4200
|
+
"message": "Event update could not verify preservation of the complete rule",
|
|
4201
|
+
"writeAttempted": True, "retryable": False,
|
|
4202
|
+
"data": {"changed": None, "ruleId": rule_id},
|
|
4203
|
+
"verification": {"verified": False, "preservationVerified": False},
|
|
4204
|
+
}
|
|
4205
|
+
result.setdefault("data", {}).update({"changed": True, "ruleId": rule_id})
|
|
4206
|
+
result["writeAttempted"] = True
|
|
4207
|
+
result.setdefault("verification", {}).update({
|
|
4208
|
+
"verified": True, "preservationVerified": True,
|
|
4209
|
+
"ret": stored["TrueActions"][0]["RET"],
|
|
4210
|
+
})
|
|
4211
|
+
return result
|
|
3524
4212
|
cleaned = _repair_first_option_rule(
|
|
3525
4213
|
cfg, token, form_number, metadata, requested,
|
|
3526
4214
|
rule_type="BizRule", family="ConditionalClearFields_",
|
|
3527
4215
|
entity_id=context["entityId"], trigger_field=requested["triggerField"],
|
|
3528
4216
|
markers=["清空", *target_names], replacement_exists=True,
|
|
4217
|
+
family_marker=" [CCF_",
|
|
3529
4218
|
)
|
|
3530
4219
|
if cleaned is not None:
|
|
3531
4220
|
return cleaned
|
|
@@ -3560,7 +4249,7 @@ def do_ensure_conditional_clear_fields(
|
|
|
3560
4249
|
cfg, token, form_number, metadata, requested,
|
|
3561
4250
|
rule_type="BizRule", family="ConditionalClearFields_",
|
|
3562
4251
|
entity_id=context["entityId"], trigger_field=requested["triggerField"],
|
|
3563
|
-
markers=["清空", *target_names],
|
|
4252
|
+
markers=["清空", *target_names], family_marker=" [CCF_",
|
|
3564
4253
|
)
|
|
3565
4254
|
if migrated is not None:
|
|
3566
4255
|
return migrated
|
|
@@ -3594,7 +4283,7 @@ def do_ensure_conditional_clear_fields(
|
|
|
3594
4283
|
|
|
3595
4284
|
def do_ensure_conditional_calculation(
|
|
3596
4285
|
cfg, token, form_number, entity, condition_field, operator, value,
|
|
3597
|
-
target, left, calculation, right,
|
|
4286
|
+
target, left, calculation, right, add_events=None, events=None,
|
|
3598
4287
|
):
|
|
3599
4288
|
"""Ensure one conditional same-entity binary FormulaAction."""
|
|
3600
4289
|
symbol = _CALCULATION_OPERATORS.get(
|
|
@@ -3664,10 +4353,14 @@ def do_ensure_conditional_calculation(
|
|
|
3664
4353
|
"actionType": "FormulaAction",
|
|
3665
4354
|
"description": f"{target_name} = {left_name} {symbol} {right_name}",
|
|
3666
4355
|
"expression": formula,
|
|
3667
|
-
"ret": 253,
|
|
4356
|
+
"ret": _requested_action_ret("FormulaAction", 253, events, add_events),
|
|
3668
4357
|
}],
|
|
3669
4358
|
"falseActions": [],
|
|
3670
4359
|
}
|
|
4360
|
+
legacy_description = description
|
|
4361
|
+
tagged_description = f"{requested['preDescription']} [CALC_{legacy_description.rsplit('_', 1)[1]}]"
|
|
4362
|
+
description = requested["preDescription"]
|
|
4363
|
+
requested["description"] = description
|
|
3671
4364
|
current = do_load_full_metadata(cfg, token, form_number)
|
|
3672
4365
|
if not _is_api_success(current):
|
|
3673
4366
|
raise RuntimeError(
|
|
@@ -3675,17 +4368,41 @@ def do_ensure_conditional_calculation(
|
|
|
3675
4368
|
+ (_extract_error_message(current) or "loadFullMetadata failed")
|
|
3676
4369
|
)
|
|
3677
4370
|
metadata = current.get("data")
|
|
3678
|
-
matches =
|
|
4371
|
+
matches = [
|
|
4372
|
+
rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
4373
|
+
if str(rule.get("PreCondition") or "") == requested["preCondition"]
|
|
4374
|
+
and str(rule.get("TriggerField") or "") == requested["triggerField"]
|
|
4375
|
+
and str(rule.get("ItemId") or context["entityId"]) == context["entityId"]
|
|
4376
|
+
and not (rule.get("FalseActions") or [])
|
|
4377
|
+
and len(rule.get("TrueActions") or []) == 1
|
|
4378
|
+
and (rule.get("TrueActions") or [{}])[0].get("ActionType") == "FormulaAction"
|
|
4379
|
+
and str((rule.get("TrueActions") or [{}])[0].get("Expression") or "") == formula
|
|
4380
|
+
]
|
|
3679
4381
|
if len(matches) > 1:
|
|
3680
4382
|
raise ValueError("CONDITIONAL_CALCULATION_DUPLICATE_RULES")
|
|
3681
4383
|
expected = normalize_rule_config(requested)
|
|
3682
4384
|
if matches:
|
|
4385
|
+
timing_result = _reconcile_existing_rule_events(
|
|
4386
|
+
cfg, token, form_number, matches[0], "bizRule", events, add_events,
|
|
4387
|
+
)
|
|
4388
|
+
if timing_result is not None:
|
|
4389
|
+
return timing_result
|
|
4390
|
+
if events is not None or add_events is not None:
|
|
4391
|
+
expected["trueActions"][0]["RET"] = matches[0]["TrueActions"][0]["RET"]
|
|
4392
|
+
expected["description"] = matches[0].get("Description")
|
|
4393
|
+
expected["preDescription"] = matches[0].get("PreDescription")
|
|
3683
4394
|
mismatches = rule_readback_mismatches(expected, matches[0], metadata)
|
|
3684
4395
|
if mismatches:
|
|
3685
4396
|
raise ValueError(
|
|
3686
4397
|
"CONDITIONAL_CALCULATION_CONFLICT: "
|
|
3687
4398
|
+ json.dumps(mismatches, ensure_ascii=False)
|
|
3688
4399
|
)
|
|
4400
|
+
renamed = _upgrade_generated_rule_description(
|
|
4401
|
+
cfg, token, form_number, matches[0],
|
|
4402
|
+
(legacy_description, tagged_description), description,
|
|
4403
|
+
)
|
|
4404
|
+
if renamed is not None:
|
|
4405
|
+
return renamed
|
|
3689
4406
|
return {
|
|
3690
4407
|
"status": True, "errorCode": "0",
|
|
3691
4408
|
"message": "Conditional calculation already matches",
|
|
@@ -3777,6 +4494,7 @@ def _quantity_conversion_rule_matches(rule, source_key, target_keys):
|
|
|
3777
4494
|
|
|
3778
4495
|
def do_ensure_quantity_conversion(
|
|
3779
4496
|
cfg, token, form_number, entity, source_qty, target_qty,
|
|
4497
|
+
add_events=None, events=None,
|
|
3780
4498
|
):
|
|
3781
4499
|
"""Ensure one FillQtyAction from a source QtyField to one or more targets."""
|
|
3782
4500
|
raw_targets = [target_qty] if isinstance(target_qty, str) else (target_qty or [])
|
|
@@ -3852,11 +4570,18 @@ def do_ensure_quantity_conversion(
|
|
|
3852
4570
|
"actionType": "FillQtyAction",
|
|
3853
4571
|
"description": "根据数量关联的单位自动计算目标数量",
|
|
3854
4572
|
"expression": expression,
|
|
3855
|
-
"ret": 249,
|
|
4573
|
+
"ret": _requested_action_ret("FillQtyAction", 249, events, add_events),
|
|
3856
4574
|
}],
|
|
3857
4575
|
"falseActions": [],
|
|
3858
4576
|
}
|
|
3859
|
-
|
|
4577
|
+
legacy_description = description
|
|
4578
|
+
tagged_description = f"{requested['preDescription']} [QCV_{legacy_description.rsplit('_', 1)[1]}]"
|
|
4579
|
+
description = requested["preDescription"]
|
|
4580
|
+
requested["description"] = description
|
|
4581
|
+
descriptions = [
|
|
4582
|
+
*_rule_projections_by_description(metadata, legacy_description),
|
|
4583
|
+
*_rule_projections_by_description(metadata, tagged_description),
|
|
4584
|
+
]
|
|
3860
4585
|
semantic = [
|
|
3861
4586
|
rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
3862
4587
|
if _quantity_conversion_rule_matches(rule, source_key, target_keys)
|
|
@@ -3866,6 +4591,17 @@ def do_ensure_quantity_conversion(
|
|
|
3866
4591
|
if descriptions and not semantic:
|
|
3867
4592
|
raise ValueError("QUANTITY_CONVERSION_CONFLICT")
|
|
3868
4593
|
if semantic:
|
|
4594
|
+
timing_result = _reconcile_existing_rule_events(
|
|
4595
|
+
cfg, token, form_number, semantic[0], "bizRule", events, add_events,
|
|
4596
|
+
)
|
|
4597
|
+
if timing_result is not None:
|
|
4598
|
+
return timing_result
|
|
4599
|
+
renamed = _upgrade_generated_rule_description(
|
|
4600
|
+
cfg, token, form_number, semantic[0],
|
|
4601
|
+
(legacy_description, tagged_description), description,
|
|
4602
|
+
)
|
|
4603
|
+
if renamed is not None:
|
|
4604
|
+
return renamed
|
|
3869
4605
|
return {
|
|
3870
4606
|
"status": True, "errorCode": "0",
|
|
3871
4607
|
"message": "Quantity conversion already matches",
|
|
@@ -3939,25 +4675,32 @@ def _reference_entity_field_candidates(cfg, token, base_entity_id):
|
|
|
3939
4675
|
data = inventory.get("data") or {}
|
|
3940
4676
|
header = data.get("headerFields") or {}
|
|
3941
4677
|
entries = data.get("entryFields") or {}
|
|
3942
|
-
groups = [header, *(entries.values() if isinstance(entries, dict) else [])]
|
|
4678
|
+
groups = [(header, False), *((group, True) for group in (entries.values() if isinstance(entries, dict) else []))]
|
|
3943
4679
|
return [
|
|
3944
|
-
field
|
|
4680
|
+
{**field, "referenceCollection": collection,
|
|
4681
|
+
"referenceEntityKey": group.get("entryKey") or group.get("headerKey")}
|
|
4682
|
+
for group, collection in groups if isinstance(group, dict)
|
|
3945
4683
|
for field in (group.get("fields") or []) if isinstance(field, dict)
|
|
3946
4684
|
]
|
|
3947
4685
|
|
|
3948
4686
|
|
|
3949
4687
|
def _ensure_reference_property_for_carry(
|
|
3950
|
-
cfg, token, form_number, source_key, requested_property,
|
|
4688
|
+
cfg, token, form_number, source_key, requested_property, write=True,
|
|
4689
|
+
target_row=None, source_row=None, inventory_cache=None,
|
|
3951
4690
|
):
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
4691
|
+
cache = inventory_cache if inventory_cache is not None else {}
|
|
4692
|
+
state_key = ("properties", source_key)
|
|
4693
|
+
if state_key not in cache:
|
|
4694
|
+
cache[state_key] = do_get_properties(
|
|
4695
|
+
cfg, token, form_number, source_key, ["RefProps", "BaseEntityId"]
|
|
4696
|
+
)
|
|
4697
|
+
state = cache[state_key]
|
|
3955
4698
|
ref_props = _parse_ref_props_value(state)
|
|
3956
4699
|
existing = [
|
|
3957
4700
|
name for name in ref_props
|
|
3958
4701
|
if name.casefold() == requested_property.casefold()
|
|
3959
4702
|
]
|
|
3960
|
-
if len(existing) == 1:
|
|
4703
|
+
if len(existing) == 1 and target_row is None:
|
|
3961
4704
|
return existing[0], None
|
|
3962
4705
|
data = state.get("data") or {}
|
|
3963
4706
|
base_entity_id = str(
|
|
@@ -3969,12 +4712,13 @@ def _ensure_reference_property_for_carry(
|
|
|
3969
4712
|
f"requested={requested_property!r}, available={ref_props!r}; the "
|
|
3970
4713
|
"source reference does not expose one BaseEntityId for exact discovery"
|
|
3971
4714
|
)
|
|
3972
|
-
|
|
3973
|
-
cfg, token, base_entity_id
|
|
3974
|
-
|
|
4715
|
+
if base_entity_id not in cache:
|
|
4716
|
+
cache[base_entity_id] = _reference_entity_field_candidates(cfg, token, base_entity_id)
|
|
4717
|
+
candidates = cache[base_entity_id]
|
|
4718
|
+
property_selector = existing[0] if len(existing) == 1 else requested_property
|
|
3975
4719
|
exact_key = [
|
|
3976
4720
|
row for row in candidates
|
|
3977
|
-
if str(row.get("key") or "").strip() ==
|
|
4721
|
+
if str(row.get("key") or "").strip() == property_selector
|
|
3978
4722
|
]
|
|
3979
4723
|
matches = exact_key or [
|
|
3980
4724
|
row for row in candidates
|
|
@@ -3987,7 +4731,23 @@ def _ensure_reference_property_for_carry(
|
|
|
3987
4731
|
f"{json.dumps([{'key': row.get('key'), 'name': row.get('name'), 'type': row.get('type')} for row in candidates], ensure_ascii=False)}"
|
|
3988
4732
|
)
|
|
3989
4733
|
property_key = str(matches[0].get("key") or "").strip()
|
|
4734
|
+
if target_row is not None:
|
|
4735
|
+
collection = (matches[0].get("referenceCollection")
|
|
4736
|
+
or str(matches[0].get("type") or "").startswith("Mul")
|
|
4737
|
+
or str((source_row or {}).get("type") or "").startswith("Mul"))
|
|
4738
|
+
if collection:
|
|
4739
|
+
raise ValueError(
|
|
4740
|
+
"CARRY_REFERENCE_COLLECTION_UNSUPPORTED: "
|
|
4741
|
+
f"source={source_key}.{property_key}, sourceEntity={matches[0].get('referenceEntityKey')!r}, "
|
|
4742
|
+
f"target={target_row.get('key')!r}, targetType={target_row.get('type')!r}; "
|
|
4743
|
+
"the source projects a collection, not one scalar value. No RefProps or rule was written. "
|
|
4744
|
+
"Use a plugin with an explicit business selection/aggregation policy; do not take the first row implicitly."
|
|
4745
|
+
)
|
|
4746
|
+
if property_key in ref_props:
|
|
4747
|
+
return property_key, None
|
|
3990
4748
|
updated = [{"name": name} for name in [*ref_props, property_key]]
|
|
4749
|
+
if not write:
|
|
4750
|
+
return property_key, updated
|
|
3991
4751
|
written = do_set_ref_props(
|
|
3992
4752
|
cfg, token, form_number, source_key, updated
|
|
3993
4753
|
)
|
|
@@ -4036,6 +4796,7 @@ def _take_base_rule_matches(
|
|
|
4036
4796
|
def do_ensure_conditional_carry_reference_property(
|
|
4037
4797
|
cfg, token, form_number, entity, condition_field, operator, value,
|
|
4038
4798
|
source_reference, source_property, target, only_empty_target=False,
|
|
4799
|
+
add_events=None, events=None,
|
|
4039
4800
|
):
|
|
4040
4801
|
"""Ensure one conditional TakeBaseProAction from an existing RefProps declaration."""
|
|
4041
4802
|
if condition_field in (None, ""):
|
|
@@ -4060,12 +4821,13 @@ def do_ensure_conditional_carry_reference_property(
|
|
|
4060
4821
|
requested_property = str(source_property or "").strip()
|
|
4061
4822
|
if not requested_property:
|
|
4062
4823
|
raise ValueError("CONDITIONAL_CARRY_SOURCE_PROPERTY_REQUIRED")
|
|
4063
|
-
property_key, ref_props_write = _ensure_reference_property_for_carry(
|
|
4064
|
-
cfg, token, form_number, source_key, requested_property
|
|
4065
|
-
)
|
|
4066
4824
|
by_key = {
|
|
4067
4825
|
str(row.get("key") or "").strip(): row for row in context["fieldRows"]
|
|
4068
4826
|
}
|
|
4827
|
+
property_key, ref_props_write = _ensure_reference_property_for_carry(
|
|
4828
|
+
cfg, token, form_number, source_key, requested_property,
|
|
4829
|
+
target_row=by_key[target_key], source_row=by_key[source_key],
|
|
4830
|
+
)
|
|
4069
4831
|
source_name = str((by_key.get(source_key) or {}).get("name") or source_key).strip()
|
|
4070
4832
|
target_name = str((by_key.get(target_key) or {}).get("name") or target_key).strip()
|
|
4071
4833
|
source_path = f"{source_key}.{property_key}"
|
|
@@ -4102,10 +4864,14 @@ def do_ensure_conditional_carry_reference_property(
|
|
|
4102
4864
|
"description": "携带基础资料属性到目标字段",
|
|
4103
4865
|
"expression": [physical_mapping],
|
|
4104
4866
|
"fieldName": [display_mapping],
|
|
4105
|
-
"ret": 253,
|
|
4867
|
+
"ret": _requested_action_ret("TakeBaseProAction", 253, events, add_events),
|
|
4106
4868
|
}],
|
|
4107
4869
|
"falseActions": [],
|
|
4108
4870
|
}
|
|
4871
|
+
legacy_description = description
|
|
4872
|
+
tagged_description = f"{requested['preDescription']} [CARRY_{legacy_description.rsplit('_', 1)[1]}]"
|
|
4873
|
+
description = requested["preDescription"]
|
|
4874
|
+
requested["description"] = description
|
|
4109
4875
|
current = do_load_full_metadata(cfg, token, form_number)
|
|
4110
4876
|
if not _is_api_success(current):
|
|
4111
4877
|
raise RuntimeError(
|
|
@@ -4113,7 +4879,10 @@ def do_ensure_conditional_carry_reference_property(
|
|
|
4113
4879
|
+ (_extract_error_message(current) or "loadFullMetadata failed")
|
|
4114
4880
|
)
|
|
4115
4881
|
metadata = current.get("data")
|
|
4116
|
-
descriptions =
|
|
4882
|
+
descriptions = [
|
|
4883
|
+
*_rule_projections_by_description(metadata, legacy_description),
|
|
4884
|
+
*_rule_projections_by_description(metadata, tagged_description),
|
|
4885
|
+
]
|
|
4117
4886
|
semantic = [
|
|
4118
4887
|
rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
4119
4888
|
if _take_base_rule_matches(
|
|
@@ -4125,6 +4894,21 @@ def do_ensure_conditional_carry_reference_property(
|
|
|
4125
4894
|
if descriptions and not semantic:
|
|
4126
4895
|
raise ValueError("CONDITIONAL_CARRY_CONFLICT")
|
|
4127
4896
|
if semantic:
|
|
4897
|
+
timing_result = _reconcile_existing_rule_events(
|
|
4898
|
+
cfg, token, form_number, semantic[0], "bizRule", events, add_events,
|
|
4899
|
+
)
|
|
4900
|
+
if timing_result is not None:
|
|
4901
|
+
if ref_props_write is not None and isinstance(timing_result, dict):
|
|
4902
|
+
timing_result["refPropsResult"] = ref_props_write
|
|
4903
|
+
return timing_result
|
|
4904
|
+
renamed = _upgrade_generated_rule_description(
|
|
4905
|
+
cfg, token, form_number, semantic[0],
|
|
4906
|
+
(legacy_description, tagged_description), description,
|
|
4907
|
+
)
|
|
4908
|
+
if renamed is not None:
|
|
4909
|
+
if ref_props_write is not None and isinstance(renamed, dict):
|
|
4910
|
+
renamed["refPropsResult"] = ref_props_write
|
|
4911
|
+
return renamed
|
|
4128
4912
|
result = {
|
|
4129
4913
|
"status": True, "errorCode": "0",
|
|
4130
4914
|
"message": "Conditional carry reference property already matches",
|
|
@@ -4173,9 +4957,235 @@ def do_ensure_conditional_carry_reference_property(
|
|
|
4173
4957
|
return result
|
|
4174
4958
|
|
|
4175
4959
|
|
|
4960
|
+
@mutation_contract
|
|
4961
|
+
def do_ensure_carry_reference_mappings(cfg, token, form_number, request):
|
|
4962
|
+
"""Write one TakeBaseProAction whose mapping table has several rows."""
|
|
4963
|
+
if not isinstance(request, dict):
|
|
4964
|
+
raise ValueError("CARRY_MAPPINGS_INPUT_INVALID: expected an object")
|
|
4965
|
+
allowed = {"entity", "conditionField", "operator", "value", "mappings", "branch",
|
|
4966
|
+
"events", "addEvents", "ruleId"}
|
|
4967
|
+
unknown = sorted(set(request) - allowed)
|
|
4968
|
+
mappings = request.get("mappings")
|
|
4969
|
+
if unknown or not isinstance(mappings, list) or not 1 <= len(mappings) <= 50:
|
|
4970
|
+
raise ValueError(
|
|
4971
|
+
f"CARRY_MAPPINGS_INPUT_INVALID: unknown={unknown}; mappings need 1..50 rows. "
|
|
4972
|
+
'Use {"mappings":[{"sourceReference":"<reference>","sourceProperty":"<property>","target":"<target>"}]}; '
|
|
4973
|
+
"all three selectors belong inside each mappings row. For one mapping with source-not-empty default, use --source-reference/--source-property/--target without --input."
|
|
4974
|
+
)
|
|
4975
|
+
branch = str(request.get("branch") or "true").strip().casefold()
|
|
4976
|
+
if branch not in {"true", "false"}:
|
|
4977
|
+
raise ValueError("CARRY_MAPPINGS_BRANCH_INVALID: use true or false")
|
|
4978
|
+
selected_branch = "TrueActions" if branch == "true" else "FalseActions"
|
|
4979
|
+
opposite_branch = "FalseActions" if branch == "true" else "TrueActions"
|
|
4980
|
+
selected_input = "trueActions" if branch == "true" else "falseActions"
|
|
4981
|
+
required = {"sourceReference", "sourceProperty", "target"}
|
|
4982
|
+
for index, mapping in enumerate(mappings):
|
|
4983
|
+
if not isinstance(mapping, dict) or required - set(mapping) or set(mapping) - (required | {"onlyEmptyTarget"}):
|
|
4984
|
+
raise ValueError(f"CARRY_MAPPINGS_ROW_INVALID: mappings[{index}]")
|
|
4985
|
+
if any(not str(mapping[key] or "").strip() for key in required):
|
|
4986
|
+
raise ValueError(f"CARRY_MAPPINGS_ROW_INVALID: mappings[{index}] needs non-empty selectors")
|
|
4987
|
+
if "onlyEmptyTarget" in mapping and not isinstance(mapping["onlyEmptyTarget"], bool):
|
|
4988
|
+
raise ValueError(f"CARRY_MAPPINGS_ROW_INVALID: mappings[{index}].onlyEmptyTarget must be boolean")
|
|
4989
|
+
condition_field = request.get("conditionField")
|
|
4990
|
+
if condition_field in (None, "") and (request.get("operator") not in (None, "") or request.get("value") is not None):
|
|
4991
|
+
raise ValueError("CARRY_MAPPINGS_CONDITION_INVALID: operator/value need conditionField")
|
|
4992
|
+
if condition_field not in (None, "") and not str(request.get("operator") or "").strip():
|
|
4993
|
+
raise ValueError("CARRY_MAPPINGS_CONDITION_INVALID: conditionField needs operator")
|
|
4994
|
+
action_ret = _requested_action_ret(
|
|
4995
|
+
"TakeBaseProAction", 253, request.get("events"), request.get("addEvents"),
|
|
4996
|
+
)
|
|
4997
|
+
selectors = [selector for mapping in mappings
|
|
4998
|
+
for selector in (mapping["sourceReference"], mapping["target"])]
|
|
4999
|
+
entity_row, rows = _resolve_rule_entity_context(
|
|
5000
|
+
cfg, token, form_number, request.get("entity"), selectors,
|
|
5001
|
+
condition_selectors=[condition_field] if condition_field else [],
|
|
5002
|
+
)
|
|
5003
|
+
entity_key = str(entity_row.get("key") or "").strip()
|
|
5004
|
+
by_key = {str(row.get("key") or "").strip(): row for row in rows if isinstance(row, dict)}
|
|
5005
|
+
resolved = []
|
|
5006
|
+
seen_targets = set()
|
|
5007
|
+
for index, mapping in enumerate(mappings):
|
|
5008
|
+
source_key = resolve_element_rows(rows, mapping["sourceReference"], form_number)
|
|
5009
|
+
target_key = resolve_element_rows(rows, mapping["target"], form_number)
|
|
5010
|
+
if source_key == target_key or target_key in seen_targets:
|
|
5011
|
+
raise ValueError(f"CARRY_MAPPINGS_TARGET_CONFLICT: mappings[{index}] target={target_key!r}")
|
|
5012
|
+
seen_targets.add(target_key)
|
|
5013
|
+
resolved.append((source_key, target_key, str(mapping["sourceProperty"]).strip(),
|
|
5014
|
+
bool(mapping.get("onlyEmptyTarget"))))
|
|
5015
|
+
if condition_field:
|
|
5016
|
+
context = _resolve_conditional_context(
|
|
5017
|
+
cfg, token, form_number, request.get("entity"), condition_field,
|
|
5018
|
+
request["operator"], request.get("value"), participants=selectors,
|
|
5019
|
+
)
|
|
5020
|
+
condition = context["expression"]
|
|
5021
|
+
condition_key = context["conditionKey"]
|
|
5022
|
+
else:
|
|
5023
|
+
condition = "1 = 1"
|
|
5024
|
+
condition_key = None
|
|
5025
|
+
physical = []
|
|
5026
|
+
display = []
|
|
5027
|
+
ref_props_results = []
|
|
5028
|
+
pending_ref_props = {}
|
|
5029
|
+
inventory_cache = {}
|
|
5030
|
+
for source_key, target_key, source_property, only_empty in resolved:
|
|
5031
|
+
property_key, pending = _ensure_reference_property_for_carry(
|
|
5032
|
+
cfg, token, form_number, source_key, source_property, write=False,
|
|
5033
|
+
target_row=by_key[target_key], source_row=by_key[source_key],
|
|
5034
|
+
inventory_cache=inventory_cache,
|
|
5035
|
+
)
|
|
5036
|
+
if pending is not None:
|
|
5037
|
+
existing_names = pending_ref_props.get(source_key)
|
|
5038
|
+
if existing_names is None:
|
|
5039
|
+
existing_names = [item["name"] for item in pending]
|
|
5040
|
+
pending_ref_props[source_key] = list(dict.fromkeys([
|
|
5041
|
+
*existing_names, property_key,
|
|
5042
|
+
]))
|
|
5043
|
+
source_name = str((by_key.get(source_key) or {}).get("name") or source_key).strip()
|
|
5044
|
+
target_name = str((by_key.get(target_key) or {}).get("name") or target_key).strip()
|
|
5045
|
+
physical.append({"TargetField": target_key, "SrcField": f"{source_key}.{property_key}",
|
|
5046
|
+
**({"OnlyEmptyRow": True} if only_empty else {})})
|
|
5047
|
+
display.append({"TargetFieldName": f"{target_name}({target_key})",
|
|
5048
|
+
"SrcFieldName": f"{source_name}.{property_key}({source_key}.{property_key})",
|
|
5049
|
+
**({"OnlyEmptyRow": True} if only_empty else {})})
|
|
5050
|
+
trigger_keys = list(dict.fromkeys([
|
|
5051
|
+
*([condition_key] if condition_key else []),
|
|
5052
|
+
*(source_key for source_key, _, _, _ in resolved),
|
|
5053
|
+
]))
|
|
5054
|
+
trigger_field = ";".join(
|
|
5055
|
+
f"{str((by_key.get(key) or {}).get('name') or key).strip()}({key})"
|
|
5056
|
+
for key in trigger_keys
|
|
5057
|
+
)
|
|
5058
|
+
description = "带出" + "、".join(
|
|
5059
|
+
str((by_key.get(target_key) or {}).get("name") or target_key).strip()
|
|
5060
|
+
for _, target_key, _, _ in resolved
|
|
5061
|
+
)
|
|
5062
|
+
requested = {
|
|
5063
|
+
"ruleType": "bizRule", "itemId": str(entity_row.get("id") or "").strip(),
|
|
5064
|
+
"mountPoint": entity_key, "description": description,
|
|
5065
|
+
"preDescription": description, "preCondition": condition,
|
|
5066
|
+
"triggerField": trigger_field, "enabled": True,
|
|
5067
|
+
"trueActions": [], "falseActions": [],
|
|
5068
|
+
}
|
|
5069
|
+
requested[selected_input] = [{"actionType": "TakeBaseProAction",
|
|
5070
|
+
"description": "携带基础资料属性到目标字段",
|
|
5071
|
+
"expression": physical, "fieldName": display,
|
|
5072
|
+
"ret": action_ret}]
|
|
5073
|
+
|
|
5074
|
+
def write_ref_props():
|
|
5075
|
+
for source_key, names in pending_ref_props.items():
|
|
5076
|
+
written = do_set_ref_props(
|
|
5077
|
+
cfg, token, form_number, source_key,
|
|
5078
|
+
[{"name": name} for name in names],
|
|
5079
|
+
)
|
|
5080
|
+
if not _is_api_success(written) or not bool(
|
|
5081
|
+
(written.get("verification") or {}).get("verified")
|
|
5082
|
+
):
|
|
5083
|
+
raise RuntimeError("CARRY_MAPPINGS_REFPROPS_WRITE_FAILED: " +
|
|
5084
|
+
(_extract_error_message(written) or str(written)))
|
|
5085
|
+
ref_props_results.append(written)
|
|
5086
|
+
current = do_load_full_metadata(cfg, token, form_number)
|
|
5087
|
+
if not _is_api_success(current):
|
|
5088
|
+
raise RuntimeError("CARRY_MAPPINGS_READ_FAILED: " + (_extract_error_message(current) or "loadFullMetadata failed"))
|
|
5089
|
+
metadata = current.get("data")
|
|
5090
|
+
selected_id = str(request.get("ruleId") or "").strip()
|
|
5091
|
+
candidates = [rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
5092
|
+
if str(rule.get("PreCondition") or "") == condition
|
|
5093
|
+
and str(rule.get("ItemId") or str(entity_row.get("id") or "")) == str(entity_row.get("id") or "")
|
|
5094
|
+
and not (rule.get(opposite_branch) or [])
|
|
5095
|
+
and (rule.get(selected_branch) or [])
|
|
5096
|
+
and all(isinstance(action, dict) and action.get("ActionType") == "TakeBaseProAction"
|
|
5097
|
+
for action in rule[selected_branch])]
|
|
5098
|
+
requested_pairs = [(item["TargetField"], item["SrcField"], bool(item.get("OnlyEmptyRow")))
|
|
5099
|
+
for item in physical]
|
|
5100
|
+
matching = [rule for rule in candidates
|
|
5101
|
+
if sorted((str(item.get("TargetField") or ""), str(item.get("SrcField") or ""),
|
|
5102
|
+
bool(item.get("OnlyEmptyRow")))
|
|
5103
|
+
for action in rule[selected_branch]
|
|
5104
|
+
for item in (action.get("Expression") or [])) == sorted(requested_pairs)
|
|
5105
|
+
and all(len(action.get("Expression") or []) == len(action.get("FieldName") or [])
|
|
5106
|
+
for action in rule[selected_branch])]
|
|
5107
|
+
overlap = [rule for rule in candidates if rule not in matching and seen_targets.intersection({
|
|
5108
|
+
str(item.get("TargetField") or "")
|
|
5109
|
+
for action in rule[selected_branch]
|
|
5110
|
+
for item in (action.get("Expression") or [])
|
|
5111
|
+
})]
|
|
5112
|
+
if overlap and not selected_id:
|
|
5113
|
+
return {"status": False, "writeAttempted": False,
|
|
5114
|
+
"errorCode": "CARRY_MAPPINGS_TARGET_OVERLAP",
|
|
5115
|
+
"message": "Existing rules already carry part of these targets; inspect the candidate and update its complete mapping list explicitly",
|
|
5116
|
+
"data": {"candidateRuleIds": [rule.get("Id") for rule in overlap]}}
|
|
5117
|
+
if matching and not selected_id and any(
|
|
5118
|
+
str(rule.get("TriggerField") or "") != trigger_field for rule in matching
|
|
5119
|
+
):
|
|
5120
|
+
return {"status": False, "writeAttempted": bool(ref_props_results),
|
|
5121
|
+
"errorCode": "CARRY_MAPPINGS_EXISTING_RULE_REQUIRES_ID",
|
|
5122
|
+
"message": "Matching mappings already exist with different trigger metadata; select the exact ruleId",
|
|
5123
|
+
"data": {"candidateRuleIds": [rule.get("Id") for rule in matching]}}
|
|
5124
|
+
if selected_id:
|
|
5125
|
+
matching = [rule for rule in matching if str(rule.get("Id") or "") == selected_id]
|
|
5126
|
+
if not matching:
|
|
5127
|
+
raise ValueError("CARRY_MAPPINGS_RULE_ID_MISMATCH: selected rule does not match all mappings")
|
|
5128
|
+
if len(matching) > 1:
|
|
5129
|
+
raise ValueError("CARRY_MAPPINGS_AMBIGUOUS: select one exact ruleId")
|
|
5130
|
+
if matching:
|
|
5131
|
+
rule = matching[0]
|
|
5132
|
+
actions = rule.get(selected_branch) or []
|
|
5133
|
+
if len(actions) == 1:
|
|
5134
|
+
if len(actions[0].get("Expression") or []) == len(physical):
|
|
5135
|
+
if not selected_id and request.get("events") is not None and actions[0].get("RET") != action_ret:
|
|
5136
|
+
return {"status": False, "writeAttempted": False,
|
|
5137
|
+
"errorCode": "RULE_EVENT_REPLACE_REQUIRES_RULE_ID",
|
|
5138
|
+
"message": "Replacing existing timings requires the exact ruleId",
|
|
5139
|
+
"data": {"candidateRuleId": rule.get("Id")}}
|
|
5140
|
+
write_ref_props()
|
|
5141
|
+
if selected_id and request.get("events") is not None:
|
|
5142
|
+
patch = {"ruleType": "bizRule", "ruleId": selected_id,
|
|
5143
|
+
selected_input: [{**actions[0], "RET": action_ret}]}
|
|
5144
|
+
if actions[0].get("RET") != action_ret:
|
|
5145
|
+
return do_update_rule_verified(cfg, token, form_number, patch)
|
|
5146
|
+
timing_result = _reconcile_existing_rule_events(
|
|
5147
|
+
cfg, token, form_number, rule, "bizRule",
|
|
5148
|
+
None if selected_id else request.get("events"),
|
|
5149
|
+
request.get("addEvents"),
|
|
5150
|
+
)
|
|
5151
|
+
if timing_result is not None:
|
|
5152
|
+
return timing_result
|
|
5153
|
+
return {"status": True, "writeAttempted": bool(ref_props_results),
|
|
5154
|
+
"data": {"changed": bool(ref_props_results), "ruleId": rule.get("Id"),
|
|
5155
|
+
"mappingCount": len(physical)},
|
|
5156
|
+
"verification": {"verified": True, "preservationVerified": True}}
|
|
5157
|
+
if not selected_id:
|
|
5158
|
+
return {"status": False, "writeAttempted": bool(ref_props_results),
|
|
5159
|
+
"errorCode": "CARRY_MAPPINGS_CONSOLIDATION_REQUIRES_RULE_ID",
|
|
5160
|
+
"message": "Matching mappings exist as separate services; select their ruleId before consolidation",
|
|
5161
|
+
"data": {"candidateRuleId": rule.get("Id")}}
|
|
5162
|
+
if len({action.get("RET") for action in actions}) != 1:
|
|
5163
|
+
raise ValueError("CARRY_MAPPINGS_TIMING_CONFLICT: existing services have different timings")
|
|
5164
|
+
requested[selected_input][0]["ret"] = _requested_action_ret(
|
|
5165
|
+
"TakeBaseProAction", actions[0]["RET"],
|
|
5166
|
+
request.get("events"), request.get("addEvents"),
|
|
5167
|
+
)
|
|
5168
|
+
patch = {"ruleType": "bizRule", "ruleId": selected_id,
|
|
5169
|
+
selected_input: requested[selected_input]}
|
|
5170
|
+
write_ref_props()
|
|
5171
|
+
result = do_update_rule_verified(cfg, token, form_number, patch)
|
|
5172
|
+
else:
|
|
5173
|
+
if selected_id:
|
|
5174
|
+
raise ValueError("CARRY_MAPPINGS_RULE_ID_NOT_FOUND")
|
|
5175
|
+
write_ref_props()
|
|
5176
|
+
result = do_add_rule_verified(cfg, token, form_number, requested)
|
|
5177
|
+
if isinstance(result, dict):
|
|
5178
|
+
result["writeAttempted"] = True
|
|
5179
|
+
result.setdefault("data", {}).update({"changed": _is_api_success(result),
|
|
5180
|
+
"mappingCount": len(physical)})
|
|
5181
|
+
if ref_props_results:
|
|
5182
|
+
result["refPropsResults"] = ref_props_results
|
|
5183
|
+
return result
|
|
5184
|
+
|
|
5185
|
+
|
|
4176
5186
|
def do_ensure_conditional_copy_field_value(
|
|
4177
5187
|
cfg, token, form_number, entity, condition_field, operator, value,
|
|
4178
|
-
source, target, only_empty_target=False,
|
|
5188
|
+
source, target, only_empty_target=False, add_events=None, events=None,
|
|
4179
5189
|
):
|
|
4180
5190
|
"""Ensure one conditional same-entity SetEqutionAction mapping."""
|
|
4181
5191
|
context = _resolve_conditional_context(
|
|
@@ -4226,10 +5236,14 @@ def do_ensure_conditional_copy_field_value(
|
|
|
4226
5236
|
"description": "携带字段值到目标字段",
|
|
4227
5237
|
"expression": [physical_mapping],
|
|
4228
5238
|
"fieldName": [display_mapping],
|
|
4229
|
-
"ret": 1021,
|
|
5239
|
+
"ret": _requested_action_ret("SetEqutionAction", 1021, events, add_events),
|
|
4230
5240
|
}],
|
|
4231
5241
|
"falseActions": [],
|
|
4232
5242
|
}
|
|
5243
|
+
legacy_description = description
|
|
5244
|
+
tagged_description = f"{requested['preDescription']} [COPY_{legacy_description.rsplit('_', 1)[1]}]"
|
|
5245
|
+
description = requested["preDescription"]
|
|
5246
|
+
requested["description"] = description
|
|
4233
5247
|
current = do_load_full_metadata(cfg, token, form_number)
|
|
4234
5248
|
if not _is_api_success(current):
|
|
4235
5249
|
raise RuntimeError(
|
|
@@ -4237,7 +5251,10 @@ def do_ensure_conditional_copy_field_value(
|
|
|
4237
5251
|
+ (_extract_error_message(current) or "loadFullMetadata failed")
|
|
4238
5252
|
)
|
|
4239
5253
|
metadata = current.get("data")
|
|
4240
|
-
descriptions =
|
|
5254
|
+
descriptions = [
|
|
5255
|
+
*_rule_projections_by_description(metadata, legacy_description),
|
|
5256
|
+
*_rule_projections_by_description(metadata, tagged_description),
|
|
5257
|
+
]
|
|
4241
5258
|
semantic = [
|
|
4242
5259
|
rule for rule in _all_rule_projections(metadata, "BizRule")
|
|
4243
5260
|
if _set_equation_rule_matches(
|
|
@@ -4249,6 +5266,17 @@ def do_ensure_conditional_copy_field_value(
|
|
|
4249
5266
|
if descriptions and not semantic:
|
|
4250
5267
|
raise ValueError("CONDITIONAL_COPY_FIELD_CONFLICT")
|
|
4251
5268
|
if semantic:
|
|
5269
|
+
timing_result = _reconcile_existing_rule_events(
|
|
5270
|
+
cfg, token, form_number, semantic[0], "bizRule", events, add_events,
|
|
5271
|
+
)
|
|
5272
|
+
if timing_result is not None:
|
|
5273
|
+
return timing_result
|
|
5274
|
+
renamed = _upgrade_generated_rule_description(
|
|
5275
|
+
cfg, token, form_number, semantic[0],
|
|
5276
|
+
(legacy_description, tagged_description), description,
|
|
5277
|
+
)
|
|
5278
|
+
if renamed is not None:
|
|
5279
|
+
return renamed
|
|
4252
5280
|
return {
|
|
4253
5281
|
"status": True, "errorCode": "0",
|
|
4254
5282
|
"message": "Conditional copy field value already matches",
|
|
@@ -4274,12 +5302,219 @@ def do_ensure_conditional_copy_field_value(
|
|
|
4274
5302
|
return result
|
|
4275
5303
|
|
|
4276
5304
|
|
|
5305
|
+
@mutation_contract
|
|
5306
|
+
def do_ensure_copy_field_mappings(cfg, token, form_number, request):
|
|
5307
|
+
"""Write one SetEqutionAction with parallel source/target mapping rows."""
|
|
5308
|
+
if not isinstance(request, dict):
|
|
5309
|
+
raise ValueError("COPY_MAPPINGS_INPUT_INVALID: expected an object")
|
|
5310
|
+
allowed = {"entity", "conditionField", "operator", "value", "mappings", "branch",
|
|
5311
|
+
"events", "addEvents", "ruleId"}
|
|
5312
|
+
mappings = request.get("mappings")
|
|
5313
|
+
if set(request) - allowed or not isinstance(mappings, list) or not 1 <= len(mappings) <= 50:
|
|
5314
|
+
raise ValueError("COPY_MAPPINGS_INPUT_INVALID: expected 1..50 mapping rows")
|
|
5315
|
+
branch = str(request.get("branch") or "true").strip().casefold()
|
|
5316
|
+
if branch not in {"true", "false"}:
|
|
5317
|
+
raise ValueError("COPY_MAPPINGS_BRANCH_INVALID: use true or false")
|
|
5318
|
+
selected_branch = "TrueActions" if branch == "true" else "FalseActions"
|
|
5319
|
+
opposite_branch = "FalseActions" if branch == "true" else "TrueActions"
|
|
5320
|
+
selected_input = "trueActions" if branch == "true" else "falseActions"
|
|
5321
|
+
for index, mapping in enumerate(mappings):
|
|
5322
|
+
if not isinstance(mapping, dict) or {"source", "target"} - set(mapping) or set(mapping) - {"source", "target", "onlyEmptyTarget"}:
|
|
5323
|
+
raise ValueError(f"COPY_MAPPINGS_ROW_INVALID: mappings[{index}]")
|
|
5324
|
+
if not str(mapping["source"] or "").strip() or not str(mapping["target"] or "").strip():
|
|
5325
|
+
raise ValueError(f"COPY_MAPPINGS_ROW_INVALID: mappings[{index}] needs source and target")
|
|
5326
|
+
if "onlyEmptyTarget" in mapping and not isinstance(mapping["onlyEmptyTarget"], bool):
|
|
5327
|
+
raise ValueError(f"COPY_MAPPINGS_ROW_INVALID: mappings[{index}].onlyEmptyTarget must be boolean")
|
|
5328
|
+
condition_field = request.get("conditionField")
|
|
5329
|
+
if condition_field in (None, "") and (request.get("operator") not in (None, "") or request.get("value") is not None):
|
|
5330
|
+
raise ValueError("COPY_MAPPINGS_CONDITION_INVALID: operator/value need conditionField")
|
|
5331
|
+
if condition_field not in (None, "") and not str(request.get("operator") or "").strip():
|
|
5332
|
+
raise ValueError("COPY_MAPPINGS_CONDITION_INVALID: conditionField needs operator")
|
|
5333
|
+
selectors = [selector for mapping in mappings for selector in (mapping["source"], mapping["target"])]
|
|
5334
|
+
entity_row, rows = _resolve_rule_entity_context(
|
|
5335
|
+
cfg, token, form_number, request.get("entity"), selectors,
|
|
5336
|
+
condition_selectors=[condition_field] if condition_field else [],
|
|
5337
|
+
)
|
|
5338
|
+
by_key = {str(row.get("key") or "").strip(): row for row in rows if isinstance(row, dict)}
|
|
5339
|
+
physical = []
|
|
5340
|
+
display = []
|
|
5341
|
+
source_keys = []
|
|
5342
|
+
targets = set()
|
|
5343
|
+
for index, mapping in enumerate(mappings):
|
|
5344
|
+
source_key = resolve_element_rows(rows, mapping["source"], form_number)
|
|
5345
|
+
target_key = resolve_element_rows(rows, mapping["target"], form_number)
|
|
5346
|
+
if source_key == target_key or target_key in targets:
|
|
5347
|
+
raise ValueError(f"COPY_MAPPINGS_TARGET_CONFLICT: mappings[{index}] target={target_key!r}")
|
|
5348
|
+
targets.add(target_key)
|
|
5349
|
+
source_keys.append(source_key)
|
|
5350
|
+
only_empty = bool(mapping.get("onlyEmptyTarget"))
|
|
5351
|
+
physical.append({"TargetField": target_key, "SrcField": source_key,
|
|
5352
|
+
**({"OnlyEmptyRow": True} if only_empty else {})})
|
|
5353
|
+
display.append({"TargetFieldName": f"{str((by_key.get(target_key) or {}).get('name') or target_key).strip()}({target_key})",
|
|
5354
|
+
"SrcFieldName": f"{str((by_key.get(source_key) or {}).get('name') or source_key).strip()}({source_key})",
|
|
5355
|
+
**({"OnlyEmptyRow": True} if only_empty else {})})
|
|
5356
|
+
if condition_field:
|
|
5357
|
+
context = _resolve_conditional_context(
|
|
5358
|
+
cfg, token, form_number, request.get("entity"), condition_field,
|
|
5359
|
+
request["operator"], request.get("value"), participants=selectors,
|
|
5360
|
+
)
|
|
5361
|
+
condition = context["expression"]
|
|
5362
|
+
trigger_keys = [context["conditionKey"], *source_keys]
|
|
5363
|
+
else:
|
|
5364
|
+
condition = "1 = 1"
|
|
5365
|
+
trigger_keys = source_keys
|
|
5366
|
+
trigger_field = ";".join(
|
|
5367
|
+
f"{str((by_key.get(key) or {}).get('name') or key).strip()}({key})"
|
|
5368
|
+
for key in dict.fromkeys(trigger_keys)
|
|
5369
|
+
)
|
|
5370
|
+
description = "携带" + "、".join(
|
|
5371
|
+
str((by_key.get(item["TargetField"]) or {}).get("name") or item["TargetField"]).strip()
|
|
5372
|
+
for item in physical
|
|
5373
|
+
)
|
|
5374
|
+
requested = {
|
|
5375
|
+
"ruleType": "bizRule", "itemId": str(entity_row.get("id") or "").strip(),
|
|
5376
|
+
"mountPoint": str(entity_row.get("key") or "").strip(),
|
|
5377
|
+
"description": description, "preDescription": description,
|
|
5378
|
+
"preCondition": condition, "triggerField": trigger_field, "enabled": True,
|
|
5379
|
+
"trueActions": [], "falseActions": [],
|
|
5380
|
+
}
|
|
5381
|
+
requested[selected_input] = [{"actionType": "SetEqutionAction",
|
|
5382
|
+
"description": "携带字段值到目标字段",
|
|
5383
|
+
"expression": physical, "fieldName": display,
|
|
5384
|
+
"ret": _requested_action_ret(
|
|
5385
|
+
"SetEqutionAction", 1021,
|
|
5386
|
+
request.get("events"), request.get("addEvents"),
|
|
5387
|
+
)}]
|
|
5388
|
+
current = do_load_full_metadata(cfg, token, form_number)
|
|
5389
|
+
if not _is_api_success(current):
|
|
5390
|
+
raise RuntimeError("COPY_MAPPINGS_READ_FAILED: " + (_extract_error_message(current) or "loadFullMetadata failed"))
|
|
5391
|
+
requested_pairs = sorted((item["TargetField"], item["SrcField"], bool(item.get("OnlyEmptyRow")))
|
|
5392
|
+
for item in physical)
|
|
5393
|
+
matches = [rule for rule in _all_rule_projections(current.get("data"), "BizRule")
|
|
5394
|
+
if str(rule.get("PreCondition") or "") == condition
|
|
5395
|
+
and str(rule.get("ItemId") or requested["itemId"]) == requested["itemId"]
|
|
5396
|
+
and not (rule.get(opposite_branch) or [])
|
|
5397
|
+
and (rule.get(selected_branch) or [])
|
|
5398
|
+
and all(isinstance(action, dict) and action.get("ActionType") == "SetEqutionAction"
|
|
5399
|
+
for action in rule[selected_branch])
|
|
5400
|
+
and sorted((str(item.get("TargetField") or ""), str(item.get("SrcField") or ""),
|
|
5401
|
+
bool(item.get("OnlyEmptyRow")))
|
|
5402
|
+
for action in rule[selected_branch]
|
|
5403
|
+
for item in (action.get("Expression") or [])) == requested_pairs
|
|
5404
|
+
and all(len(action.get("Expression") or []) == len(action.get("FieldName") or [])
|
|
5405
|
+
for action in rule[selected_branch])]
|
|
5406
|
+
selected_id = str(request.get("ruleId") or "").strip()
|
|
5407
|
+
overlapping = [rule for rule in _all_rule_projections(current.get("data"), "BizRule")
|
|
5408
|
+
if rule not in matches
|
|
5409
|
+
and str(rule.get("PreCondition") or "") == condition
|
|
5410
|
+
and str(rule.get("ItemId") or requested["itemId"]) == requested["itemId"]
|
|
5411
|
+
and not (rule.get(opposite_branch) or [])
|
|
5412
|
+
and (rule.get(selected_branch) or [])
|
|
5413
|
+
and all(isinstance(action, dict) and action.get("ActionType") == "SetEqutionAction"
|
|
5414
|
+
for action in rule[selected_branch])
|
|
5415
|
+
and targets.intersection({
|
|
5416
|
+
str(item.get("TargetField") or "")
|
|
5417
|
+
for action in rule[selected_branch]
|
|
5418
|
+
for item in (action.get("Expression") or [])
|
|
5419
|
+
})]
|
|
5420
|
+
if overlapping and not selected_id:
|
|
5421
|
+
return {"status": False, "writeAttempted": False,
|
|
5422
|
+
"errorCode": "COPY_MAPPINGS_TARGET_OVERLAP",
|
|
5423
|
+
"message": "Existing rules already copy into part of these targets; inspect the candidate and update its complete mapping list explicitly",
|
|
5424
|
+
"data": {"candidateRuleIds": [rule.get("Id") for rule in overlapping]}}
|
|
5425
|
+
if selected_id:
|
|
5426
|
+
matches = [rule for rule in matches if str(rule.get("Id") or "") == selected_id]
|
|
5427
|
+
if not matches:
|
|
5428
|
+
raise ValueError("COPY_MAPPINGS_RULE_ID_MISMATCH: selected rule does not match all mappings")
|
|
5429
|
+
elif matches and any(str(rule.get("TriggerField") or "") != trigger_field for rule in matches):
|
|
5430
|
+
return {"status": False, "writeAttempted": False,
|
|
5431
|
+
"errorCode": "COPY_MAPPINGS_EXISTING_RULE_REQUIRES_ID",
|
|
5432
|
+
"message": "Matching mappings exist with different trigger metadata; select the exact ruleId",
|
|
5433
|
+
"data": {"candidateRuleIds": [rule.get("Id") for rule in matches]}}
|
|
5434
|
+
if len(matches) > 1:
|
|
5435
|
+
raise ValueError("COPY_MAPPINGS_AMBIGUOUS: select one exact ruleId")
|
|
5436
|
+
if matches:
|
|
5437
|
+
rule = matches[0]
|
|
5438
|
+
actions = rule[selected_branch]
|
|
5439
|
+
if len(actions) == 1 and len(actions[0].get("Expression") or []) == len(physical):
|
|
5440
|
+
if selected_id and request.get("events") is not None and actions[0].get("RET") != requested[selected_input][0]["ret"]:
|
|
5441
|
+
return do_update_rule_verified(cfg, token, form_number, {
|
|
5442
|
+
"ruleType": "bizRule", "ruleId": selected_id,
|
|
5443
|
+
selected_input: [{**actions[0], "RET": requested[selected_input][0]["ret"]}],
|
|
5444
|
+
})
|
|
5445
|
+
timing = _reconcile_existing_rule_events(
|
|
5446
|
+
cfg, token, form_number, rule, "bizRule",
|
|
5447
|
+
None if selected_id else request.get("events"), request.get("addEvents"),
|
|
5448
|
+
)
|
|
5449
|
+
if timing is not None:
|
|
5450
|
+
return timing
|
|
5451
|
+
return {"status": True, "writeAttempted": False,
|
|
5452
|
+
"data": {"changed": False, "ruleId": rule.get("Id"), "mappingCount": len(physical)},
|
|
5453
|
+
"verification": {"verified": True, "preservationVerified": True}}
|
|
5454
|
+
if not selected_id:
|
|
5455
|
+
return {"status": False, "writeAttempted": False,
|
|
5456
|
+
"errorCode": "COPY_MAPPINGS_CONSOLIDATION_REQUIRES_RULE_ID",
|
|
5457
|
+
"message": "Matching mappings exist as separate services; select their ruleId",
|
|
5458
|
+
"data": {"candidateRuleId": rule.get("Id")}}
|
|
5459
|
+
if len({action.get("RET") for action in actions}) != 1:
|
|
5460
|
+
raise ValueError("COPY_MAPPINGS_TIMING_CONFLICT: existing services have different timings")
|
|
5461
|
+
requested[selected_input][0]["ret"] = _requested_action_ret(
|
|
5462
|
+
"SetEqutionAction", actions[0]["RET"], request.get("events"), request.get("addEvents"),
|
|
5463
|
+
)
|
|
5464
|
+
result = do_update_rule_verified(cfg, token, form_number, {
|
|
5465
|
+
"ruleType": "bizRule", "ruleId": selected_id, selected_input: requested[selected_input],
|
|
5466
|
+
})
|
|
5467
|
+
else:
|
|
5468
|
+
if selected_id:
|
|
5469
|
+
raise ValueError("COPY_MAPPINGS_RULE_ID_NOT_FOUND")
|
|
5470
|
+
result = do_add_rule_verified(cfg, token, form_number, requested)
|
|
5471
|
+
if isinstance(result, dict):
|
|
5472
|
+
result["writeAttempted"] = True
|
|
5473
|
+
result.setdefault("data", {}).update({"changed": _is_api_success(result),
|
|
5474
|
+
"mappingCount": len(physical)})
|
|
5475
|
+
return result
|
|
5476
|
+
|
|
5477
|
+
|
|
5478
|
+
def _rule_set_action_types(command, args):
|
|
5479
|
+
if command == "ensureConditionalFieldState":
|
|
5480
|
+
pair = _CONDITIONAL_STATE_ACTIONS.get(str(args.get("behavior") or "").casefold())
|
|
5481
|
+
if pair is None:
|
|
5482
|
+
raise ValueError("RULE_SET_ARGS_INVALID: unsupported field-state behavior")
|
|
5483
|
+
return [(kind, 251) for kind in pair]
|
|
5484
|
+
if command == "ensureConditionalControlState":
|
|
5485
|
+
pair = _CONDITIONAL_CONTROL_ACTIONS.get(str(args.get("behavior") or "").casefold())
|
|
5486
|
+
if pair is None:
|
|
5487
|
+
raise ValueError("RULE_SET_ARGS_INVALID: unsupported control-state behavior")
|
|
5488
|
+
return [(kind, 251) for kind in pair]
|
|
5489
|
+
if command == "ensureRuleFunctionFormula":
|
|
5490
|
+
return [("FormulaAction", 253), ("ClearValueAction", 253)]
|
|
5491
|
+
if command == "ensureTimeDifferenceCalculation":
|
|
5492
|
+
return [("FormulaAction", 1021), ("ClearValueAction", 1021)]
|
|
5493
|
+
return [{
|
|
5494
|
+
"ensureConditionalClearFields": ("ClearValueAction", 253),
|
|
5495
|
+
"ensureConditionalFieldChange": ("CallFieldChangeAction", 773),
|
|
5496
|
+
"ensureConditionalCopyFieldValue": ("SetEqutionAction", 1021),
|
|
5497
|
+
"ensureConditionalCalculation": ("FormulaAction", 253),
|
|
5498
|
+
"ensureConditionalCarryReferenceProperty": ("TakeBaseProAction", 253),
|
|
5499
|
+
"ensureQuantityConversion": ("FillQtyAction", 249),
|
|
5500
|
+
}[command]]
|
|
5501
|
+
|
|
5502
|
+
|
|
4277
5503
|
_RULE_SET_COMMANDS = {
|
|
5504
|
+
"ensureConditionalClientAppearance": (
|
|
5505
|
+
{"request"}, set(),
|
|
5506
|
+
),
|
|
5507
|
+
"ensureRuleFunctionFormula": (
|
|
5508
|
+
{"target", "call"}, {"entity", "events", "addEvents"},
|
|
5509
|
+
),
|
|
4278
5510
|
"ensureFieldCalculation": (
|
|
4279
5511
|
{"target", "left", "operator", "right"}, {"entity"},
|
|
4280
5512
|
),
|
|
5513
|
+
"ensureTimeDifferenceCalculation": (
|
|
5514
|
+
{"target", "start", "end"}, {"entity"},
|
|
5515
|
+
),
|
|
4281
5516
|
"ensureConditionalFieldState": (
|
|
4282
|
-
{"conditionField", "operator", "target", "behavior"}, {"entity", "value"},
|
|
5517
|
+
{"conditionField", "operator", "target", "behavior"}, {"entity", "value", "addEvents", "events"},
|
|
4283
5518
|
),
|
|
4284
5519
|
"ensureConditionalEntryStyle": (
|
|
4285
5520
|
{"conditionField", "operator", "value", "scope"},
|
|
@@ -4287,25 +5522,30 @@ _RULE_SET_COMMANDS = {
|
|
|
4287
5522
|
),
|
|
4288
5523
|
"ensureConditionalControlState": (
|
|
4289
5524
|
{"conditionField", "operator", "value", "targetControl", "behavior"},
|
|
4290
|
-
{"entity"},
|
|
5525
|
+
{"entity", "events", "addEvents"},
|
|
4291
5526
|
),
|
|
4292
5527
|
"ensureConditionalClearFields": (
|
|
4293
|
-
{"conditionField", "operator", "value", "targets"}, {"entity"},
|
|
5528
|
+
{"conditionField", "operator", "value", "targets"}, {"entity", "addEvents", "events"},
|
|
5529
|
+
),
|
|
5530
|
+
"ensureConditionalFieldChange": (
|
|
5531
|
+
{"conditionField", "operator", "target"}, {"entity", "value", "addEvents", "events"},
|
|
4294
5532
|
),
|
|
4295
5533
|
"ensureConditionalCopyFieldValue": (
|
|
4296
|
-
|
|
4297
|
-
{"
|
|
5534
|
+
set(),
|
|
5535
|
+
{"conditionField", "operator", "value", "source", "target", "mappings", "ruleId",
|
|
5536
|
+
"entity", "onlyEmptyTarget", "events", "addEvents", "branch"},
|
|
4298
5537
|
),
|
|
4299
5538
|
"ensureConditionalCalculation": (
|
|
4300
5539
|
{"conditionField", "operator", "value", "target", "left", "calculation", "right"},
|
|
4301
|
-
{"entity"},
|
|
5540
|
+
{"entity", "events", "addEvents"},
|
|
4302
5541
|
),
|
|
4303
5542
|
"ensureConditionalCarryReferenceProperty": (
|
|
4304
|
-
|
|
4305
|
-
{"
|
|
5543
|
+
set(),
|
|
5544
|
+
{"sourceReference", "sourceProperty", "target", "mappings", "ruleId",
|
|
5545
|
+
"entity", "conditionField", "operator", "value", "onlyEmptyTarget", "events", "addEvents", "branch"},
|
|
4306
5546
|
),
|
|
4307
5547
|
"ensureQuantityConversion": (
|
|
4308
|
-
{"sourceQty", "targetQty"}, {"entity"},
|
|
5548
|
+
{"sourceQty", "targetQty"}, {"entity", "events", "addEvents"},
|
|
4309
5549
|
),
|
|
4310
5550
|
}
|
|
4311
5551
|
|
|
@@ -4346,7 +5586,42 @@ def _validate_rule_set_request(request):
|
|
|
4346
5586
|
)
|
|
4347
5587
|
if "onlyEmptyTarget" in args and not isinstance(args["onlyEmptyTarget"], bool):
|
|
4348
5588
|
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}].onlyEmptyTarget must be boolean")
|
|
4349
|
-
if command == "
|
|
5589
|
+
if command == "ensureConditionalCarryReferenceProperty":
|
|
5590
|
+
single = {"sourceReference", "sourceProperty", "target"}
|
|
5591
|
+
if "mappings" in args:
|
|
5592
|
+
if single.intersection(args) or "onlyEmptyTarget" in args:
|
|
5593
|
+
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}] mixes mappings with single-row arguments")
|
|
5594
|
+
if not isinstance(args["mappings"], list) or not args["mappings"]:
|
|
5595
|
+
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}].mappings requires rows")
|
|
5596
|
+
elif single - set(args) or "ruleId" in args or "branch" in args:
|
|
5597
|
+
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}] needs one sourceReference/sourceProperty/target")
|
|
5598
|
+
if command == "ensureConditionalCopyFieldValue":
|
|
5599
|
+
single = {"conditionField", "operator", "value", "source", "target"}
|
|
5600
|
+
if "mappings" in args:
|
|
5601
|
+
if {"source", "target", "onlyEmptyTarget"}.intersection(args):
|
|
5602
|
+
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}] mixes mappings with single-row arguments")
|
|
5603
|
+
if not isinstance(args["mappings"], list) or not args["mappings"]:
|
|
5604
|
+
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}].mappings requires rows")
|
|
5605
|
+
elif single - set(args) or "ruleId" in args or "branch" in args:
|
|
5606
|
+
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}] needs condition/source/target")
|
|
5607
|
+
if command == "ensureConditionalClientAppearance":
|
|
5608
|
+
if not isinstance(args["request"], dict):
|
|
5609
|
+
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}].request must be an object")
|
|
5610
|
+
allowed_request = {"clientMount", "entity", "conditionField", "operator",
|
|
5611
|
+
"value", "effects", "events", "addEvents"}
|
|
5612
|
+
missing_request = {"clientMount", "conditionField", "operator", "effects"} - set(args["request"])
|
|
5613
|
+
unknown_request = set(args["request"]) - allowed_request
|
|
5614
|
+
if missing_request or unknown_request:
|
|
5615
|
+
raise ValueError(
|
|
5616
|
+
f"RULE_SET_ARGS_INVALID: rules[{index}].request "
|
|
5617
|
+
f"missing={sorted(missing_request)}, unknown={sorted(unknown_request)}"
|
|
5618
|
+
)
|
|
5619
|
+
if "events" in args["request"] or "addEvents" in args["request"]:
|
|
5620
|
+
_requested_action_ret(
|
|
5621
|
+
"ClientSetFieldAction", 251,
|
|
5622
|
+
args["request"].get("events"), args["request"].get("addEvents"),
|
|
5623
|
+
)
|
|
5624
|
+
if command in {"ensureConditionalFieldState", "ensureConditionalFieldChange"}:
|
|
4350
5625
|
blank = str(args.get("operator") or "").strip() in {"empty", "not-empty"}
|
|
4351
5626
|
if blank and args.get("value") is not None:
|
|
4352
5627
|
raise ValueError(
|
|
@@ -4358,6 +5633,15 @@ def _validate_rule_set_request(request):
|
|
|
4358
5633
|
f"RULE_SET_ARGS_INVALID: rules[{index}].value is required with "
|
|
4359
5634
|
f"operator {args.get('operator')!r}"
|
|
4360
5635
|
)
|
|
5636
|
+
if "addEvents" in args:
|
|
5637
|
+
if "events" in args:
|
|
5638
|
+
raise ValueError(f"RULE_SET_ARGS_INVALID: rules[{index}] mixes events and addEvents")
|
|
5639
|
+
if "events" in args or "addEvents" in args:
|
|
5640
|
+
action_types = _rule_set_action_types(command, args)
|
|
5641
|
+
for action_type, default_ret in action_types:
|
|
5642
|
+
_requested_action_ret(
|
|
5643
|
+
action_type, default_ret, args.get("events"), args.get("addEvents"),
|
|
5644
|
+
)
|
|
4361
5645
|
normalized.append({
|
|
4362
5646
|
"id": str(item.get("id") or f"rule-{index + 1}").strip(),
|
|
4363
5647
|
"command": command,
|
|
@@ -4365,7 +5649,10 @@ def _validate_rule_set_request(request):
|
|
|
4365
5649
|
})
|
|
4366
5650
|
calculations = [
|
|
4367
5651
|
item for item in normalized
|
|
4368
|
-
if item["command"] in {
|
|
5652
|
+
if item["command"] in {
|
|
5653
|
+
"ensureFieldCalculation", "ensureTimeDifferenceCalculation",
|
|
5654
|
+
"ensureConditionalCalculation",
|
|
5655
|
+
}
|
|
4369
5656
|
]
|
|
4370
5657
|
for item in normalized:
|
|
4371
5658
|
if item["command"] != "ensureConditionalClearFields":
|
|
@@ -4378,7 +5665,11 @@ def _validate_rule_set_request(request):
|
|
|
4378
5665
|
item["args"].get("entity") or ""
|
|
4379
5666
|
).casefold():
|
|
4380
5667
|
continue
|
|
4381
|
-
operands =
|
|
5668
|
+
operands = (
|
|
5669
|
+
{calculation["args"]["start"], calculation["args"]["end"]}
|
|
5670
|
+
if calculation["command"] == "ensureTimeDifferenceCalculation"
|
|
5671
|
+
else {calculation["args"]["left"], calculation["args"]["right"]}
|
|
5672
|
+
)
|
|
4382
5673
|
target = calculation["args"]["target"]
|
|
4383
5674
|
if operands.intersection(targets) and target not in targets:
|
|
4384
5675
|
targets.append(target)
|
|
@@ -4393,15 +5684,30 @@ def _execute_rule_set_item(cfg, token, form_number, item):
|
|
|
4393
5684
|
args = item["args"]
|
|
4394
5685
|
entity = args.get("entity")
|
|
4395
5686
|
command = item["command"]
|
|
5687
|
+
if command == "ensureConditionalClientAppearance":
|
|
5688
|
+
return do_ensure_conditional_client_appearance(
|
|
5689
|
+
cfg, token, form_number, args["request"],
|
|
5690
|
+
)
|
|
5691
|
+
if command == "ensureRuleFunctionFormula":
|
|
5692
|
+
return do_ensure_rule_function_formula(
|
|
5693
|
+
cfg, token, form_number, entity, args["target"], args["call"],
|
|
5694
|
+
args.get("addEvents"), args.get("events"),
|
|
5695
|
+
)
|
|
4396
5696
|
if command == "ensureFieldCalculation":
|
|
4397
5697
|
return do_ensure_field_calculation(
|
|
4398
5698
|
cfg, token, form_number, entity, args["target"], args["left"],
|
|
4399
5699
|
args["operator"], args["right"],
|
|
4400
5700
|
)
|
|
5701
|
+
if command == "ensureTimeDifferenceCalculation":
|
|
5702
|
+
return do_ensure_time_difference_calculation(
|
|
5703
|
+
cfg, token, form_number, entity, args["target"],
|
|
5704
|
+
args["start"], args["end"],
|
|
5705
|
+
)
|
|
4401
5706
|
if command == "ensureConditionalFieldState":
|
|
4402
5707
|
return do_ensure_conditional_field_state(
|
|
4403
5708
|
cfg, token, form_number, entity, args["conditionField"],
|
|
4404
5709
|
args["operator"], args.get("value"), args["target"], args["behavior"],
|
|
5710
|
+
args.get("addEvents"), args.get("events"),
|
|
4405
5711
|
)
|
|
4406
5712
|
if command == "ensureConditionalEntryStyle":
|
|
4407
5713
|
return do_ensure_conditional_entry_style(
|
|
@@ -4413,6 +5719,7 @@ def _execute_rule_set_item(cfg, token, form_number, item):
|
|
|
4413
5719
|
return do_ensure_conditional_control_state(
|
|
4414
5720
|
cfg, token, form_number, entity, args["conditionField"],
|
|
4415
5721
|
args["operator"], args["value"], args["targetControl"], args["behavior"],
|
|
5722
|
+
args.get("addEvents"), args.get("events"),
|
|
4416
5723
|
)
|
|
4417
5724
|
if command == "ensureConditionalClearFields":
|
|
4418
5725
|
targets = args["targets"]
|
|
@@ -4420,28 +5727,40 @@ def _execute_rule_set_item(cfg, token, form_number, item):
|
|
|
4420
5727
|
targets = [targets]
|
|
4421
5728
|
return do_ensure_conditional_clear_fields(
|
|
4422
5729
|
cfg, token, form_number, entity, args["conditionField"],
|
|
4423
|
-
args["operator"], args["value"], targets,
|
|
5730
|
+
args["operator"], args["value"], targets, args.get("addEvents"), args.get("events"),
|
|
5731
|
+
)
|
|
5732
|
+
if command == "ensureConditionalFieldChange":
|
|
5733
|
+
return do_ensure_conditional_field_change(
|
|
5734
|
+
cfg, token, form_number, entity, args["conditionField"],
|
|
5735
|
+
args["operator"], args.get("value"), args["target"],
|
|
5736
|
+
args.get("addEvents"), args.get("events"),
|
|
4424
5737
|
)
|
|
4425
5738
|
if command == "ensureConditionalCopyFieldValue":
|
|
5739
|
+
if "mappings" in args:
|
|
5740
|
+
return do_ensure_copy_field_mappings(cfg, token, form_number, args)
|
|
4426
5741
|
return do_ensure_conditional_copy_field_value(
|
|
4427
5742
|
cfg, token, form_number, entity, args["conditionField"],
|
|
4428
5743
|
args["operator"], args["value"], args["source"], args["target"],
|
|
4429
|
-
bool(args.get("onlyEmptyTarget")),
|
|
5744
|
+
bool(args.get("onlyEmptyTarget")), args.get("addEvents"), args.get("events"),
|
|
4430
5745
|
)
|
|
4431
5746
|
if command == "ensureConditionalCalculation":
|
|
4432
5747
|
return do_ensure_conditional_calculation(
|
|
4433
5748
|
cfg, token, form_number, entity, args["conditionField"],
|
|
4434
5749
|
args["operator"], args["value"], args["target"], args["left"],
|
|
4435
|
-
args["calculation"], args["right"],
|
|
5750
|
+
args["calculation"], args["right"], args.get("addEvents"), args.get("events"),
|
|
4436
5751
|
)
|
|
4437
5752
|
if command == "ensureConditionalCarryReferenceProperty":
|
|
5753
|
+
if "mappings" in args:
|
|
5754
|
+
return do_ensure_carry_reference_mappings(cfg, token, form_number, args)
|
|
4438
5755
|
return do_ensure_conditional_carry_reference_property(
|
|
4439
5756
|
cfg, token, form_number, entity, args.get("conditionField"),
|
|
4440
5757
|
args.get("operator"), args.get("value"), args["sourceReference"],
|
|
4441
5758
|
args["sourceProperty"], args["target"], bool(args.get("onlyEmptyTarget")),
|
|
5759
|
+
args.get("addEvents"), args.get("events"),
|
|
4442
5760
|
)
|
|
4443
5761
|
return do_ensure_quantity_conversion(
|
|
4444
5762
|
cfg, token, form_number, entity, args["sourceQty"], args["targetQty"],
|
|
5763
|
+
args.get("addEvents"), args.get("events"),
|
|
4445
5764
|
)
|
|
4446
5765
|
|
|
4447
5766
|
|
|
@@ -4477,8 +5796,11 @@ def _apply_rule_set_items(cfg, token, form_number, items):
|
|
|
4477
5796
|
inventory = _RULE_BATCH_INVENTORY.get()
|
|
4478
5797
|
if inventory is not None:
|
|
4479
5798
|
inventory["rows"].clear()
|
|
5799
|
+
from .execution_errors import attach_execution_error
|
|
5800
|
+
error = attach_execution_error(result)
|
|
4480
5801
|
results.append({
|
|
4481
5802
|
"id": item["id"], "command": item["command"], "result": result,
|
|
5803
|
+
**({key: error[key] for key in ("errorCategory", "capabilityLimitConfirmed")} if error.get("status") is False else {}),
|
|
4482
5804
|
**({"derivedTargets": item["derivedTargets"]} if item.get("derivedTargets") else {}),
|
|
4483
5805
|
})
|
|
4484
5806
|
if not _is_api_success(result):
|
|
@@ -4836,9 +6158,12 @@ def do_update_rule_verified(cfg, token, form_number, rule_config):
|
|
|
4836
6158
|
},
|
|
4837
6159
|
}
|
|
4838
6160
|
preserved_rule = copy.deepcopy(preserved_rule)
|
|
4839
|
-
resolved =
|
|
6161
|
+
resolved = _resolve_call_field_change_targets(
|
|
4840
6162
|
cfg, token, form_number, rule_config
|
|
4841
6163
|
)
|
|
6164
|
+
resolved = _validate_fill_qty_action_scope(
|
|
6165
|
+
cfg, token, form_number, resolved
|
|
6166
|
+
)
|
|
4842
6167
|
resolved = _validate_set_field_fmt_targets(cfg, token, form_number, resolved)
|
|
4843
6168
|
resolved = _resolve_row_fmt_targets(cfg, token, form_number, resolved)
|
|
4844
6169
|
resolved = _resolve_form_control_targets(cfg, token, form_number, resolved)
|
|
@@ -4852,6 +6177,9 @@ def do_update_rule_verified(cfg, token, form_number, rule_config):
|
|
|
4852
6177
|
if _is_api_success(result):
|
|
4853
6178
|
try:
|
|
4854
6179
|
result = _attach_rule_readback(cfg, token, form_number, resolved, result, preserved_rule=preserved_rule)
|
|
6180
|
+
warnings = runtime_action_warnings(resolved)
|
|
6181
|
+
if _is_api_success(result) and warnings:
|
|
6182
|
+
result["runtimeVerification"] = {"verified": False, "warnings": warnings}
|
|
4855
6183
|
except Exception as exc:
|
|
4856
6184
|
result.update({"status": False, "errorCode": "RULE_READBACK_FAILED", "message": str(exc),
|
|
4857
6185
|
"writeAttempted": True, "metadataRetained": True,
|