From 4e6ce16a431e40c044693b01aa5c314a577f887d Mon Sep 17 00:00:00 2001 From: Kasper Lind Date: Fri, 28 Aug 2026 08:21:15 +0200 Subject: [PATCH 1/5] basic functionality added --- .../.gitignore | 7 + .../.vscode/settings.json | 9 + .../ProductConfig.permissionset.al | 10 + .../app.json | 44 +++ .../Extensions/PTEProductCardExt.PageExt.al | 97 +++++ .../PTEProductMaterialsExt.TableExt.al | 12 + .../ProductSetup/PTEProductJobItem.Table.al | 362 ++++++++++++++++++ .../PTEProductJobItemColors.Page.al | 136 +++++++ .../ProductSetup/PTEProductJobItemSub.Page.al | 241 ++++++++++++ .../PTEProductSetupState.Codeunit.al | 125 ++++++ .../PTEProductTemplateMgt.Codeunit.al | 318 +++++++++++++++ 11 files changed, 1361 insertions(+) create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/.gitignore create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/.vscode/settings.json create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/app.json create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductMaterialsExt.TableExt.al create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemColors.Page.al create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductSetupState.Codeunit.al create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/.gitignore b/Product Configuration Enhancement/Product Configuration Enhancement/.gitignore new file mode 100644 index 0000000..aeefae1 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/.gitignore @@ -0,0 +1,7 @@ +#This file lists which files should be ignored by git +*.app +rad.json +.snapshots/ +.alcache/ +.altestrunner/ +.vscode/launch.json diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/.vscode/settings.json b/Product Configuration Enhancement/Product Configuration Enhancement/.vscode/settings.json new file mode 100644 index 0000000..c023ecd --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "al.enableCodeAnalysis": true, + "al.codeAnalyzers": [ + "${CodeCop}", + "${UICop}", + "${PerTenantExtensionCop}" + ], + "al.packageCachePath": ".alpackages" +} \ No newline at end of file diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al b/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al new file mode 100644 index 0000000..9e9cf00 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al @@ -0,0 +1,10 @@ +permissionset 75000 ProductConfig +{ + Assignable = true; + Permissions = tabledata "PTE Product Job Item" = RIMD, + table "PTE Product Job Item" = X, + codeunit "PTE Product Setup State" = X, + codeunit "PTE Product Template Mgt" = X, + page "PTE Product Job Item Colors" = X, + page "PTE Product Job Item Sub" = X; +} \ No newline at end of file diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/app.json b/Product Configuration Enhancement/Product Configuration Enhancement/app.json new file mode 100644 index 0000000..0be1a44 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/app.json @@ -0,0 +1,44 @@ +{ + "id": "8d206bcf-bc23-4041-9c54-ccfeef79998d", + "name": "Product Configuration Enhancement", + "publisher": "PrintVis A/S", + "version": "1.0.0.0", + "brief": "Product Job Item templates and user field setup for PrintVis products.", + "description": "Adds Product Job Item template lines to the PrintVis Product Card and syncs them to Job Items when a product is applied to a case. Also surfaces the PrintVis Job user fields on the Product Card.", + "privacyStatement": "http://www.printvis.com", + "EULA": "http://www.printvis.com", + "help": "https://learn.printvis.com/", + "url": "http://www.printvis.com", + "dependencies": [ + { + "id": "5452f323-059e-499a-9753-5d2c07eef904", + "name": "PrintVis", + "publisher": "PrintVis A/S", + "version": "28.0.0.0" + } + ], + "screenshots": [], + "idRanges": [ + { + "from": 75000, + "to": 75100 + } + ], + "application": "28.0.0.0", + "platform": "28.0.0.0", + + "contextSensitiveHelpUrl": "https://learn.printvis.com/", + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": false, + "includeSourceInSymbolFile": true + }, + "runtime": "15.0", + "target": "Cloud", + "features": [ + "TranslationFile" + ], + "supportedLocales": [ + "en-US" + ] +} diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al new file mode 100644 index 0000000..8f9b239 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al @@ -0,0 +1,97 @@ +pageextension 75006 "PTE Product Card Ext" extends "PVS Product Card" +{ + layout + { + // Hide the base "Inks and Consumables" product colors part; the Product Job Item + // sub page below replaces it for job-item based colors/materials configuration. + modify(InksAndConsumables) + { + Visible = false; + } + + addafter(InksAndConsumables) + { + part(PTE_ProductJobItemSub; "PTE Product Job Item Sub") + { + ApplicationArea = All; + Caption = 'Product Job Items'; + SubPageLink = "Product Code" = field("Code"); + } + } + } + + actions + { + addafter(AdditionalInfo) + { + group("PTE_UserFields") + { + Caption = 'User Fields'; + action("PTE_PUSH_USERFIELDS_1") + { + ApplicationArea = All; + Caption = 'User Fields 1'; + Image = PeriodStatus; + ShortCutKey = 'Shift+Ctrl+F1'; + ToolTip = 'User Fields (1) JOB'; + Visible = PUSH_USERFIELDS_1Visible; + + trigger OnAction() + var + ProductSetupState: Codeunit "PTE Product Setup State"; + begin + ProductSetupState.Form_Userfield_Edit_Product(0, Rec.Code); + end; + } + action("PTE_PUSH_USERFIELDS_2") + { + ApplicationArea = All; + Caption = 'User Fields 2'; + Image = ReopenPeriod; + ShortCutKey = 'Shift+Ctrl+F2'; + ToolTip = 'User Fields (2) JOB'; + Visible = PUSH_USERFIELDS_2Visible; + + trigger OnAction() + var + ProductSetupState: Codeunit "PTE Product Setup State"; + begin + ProductSetupState.Form_Userfield_Edit_Product(1, Rec.Code); + end; + } + action("PTE_PUSH_USERFIELDS_3") + { + ApplicationArea = All; + Caption = 'User Fields 3'; + Image = ClosePeriod; + ShortCutKey = 'Shift+Ctrl+F3'; + ToolTip = 'User Fields (3) JOB'; + Visible = PUSH_USERFIELDS_3Visible; + + trigger OnAction() + var + ProductSetupState: Codeunit "PTE Product Setup State"; + begin + ProductSetupState.Form_Userfield_Edit_Product(2, Rec.Code); + end; + } + } + } + } + + trigger OnOpenPage() + var + SetupRec: Record "PVS General Setup"; + begin + SetupRec.Get(); + + PUSH_USERFIELDS_1Visible := SetupRec."Enable User Fields Job 1"; + PUSH_USERFIELDS_2Visible := SetupRec."Enable User Fields Job 2"; + PUSH_USERFIELDS_3Visible := SetupRec."Enable User Fields Job 3"; + end; + + var + PUSH_USERFIELDS_1Visible: Boolean; + PUSH_USERFIELDS_2Visible: Boolean; + PUSH_USERFIELDS_3Visible: Boolean; +} diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductMaterialsExt.TableExt.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductMaterialsExt.TableExt.al new file mode 100644 index 0000000..325ff36 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductMaterialsExt.TableExt.al @@ -0,0 +1,12 @@ +tableextension 75005 "PTE Product Materials Ext" extends "PVS Product Materials" +{ + fields + { + field(75010; "Job Item No."; Integer) + { + Caption = 'Job Item No.'; + DataClassification = SystemMetadata; + TableRelation = "PTE Product Job Item"."Job Item No." where("Product Code" = field("Product Code")); + } + } +} diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al new file mode 100644 index 0000000..ebf911c --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al @@ -0,0 +1,362 @@ +table 75000 "PTE Product Job Item" +{ + DataClassification = ToBeClassified; + + fields + { + field(1; "Product Code"; Code[20]) + { + Caption = 'Product Code'; + DataClassification = SystemMetadata; + TableRelation = "PVS Product".Code; + ToolTip = 'Specifies the product code.'; + } + field(2; "Job Item No."; Integer) + { + Caption = 'Job Item No.'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the job item number.'; + } + field(6010116; "Format Code"; Code[20]) + { + DataClassification = CustomerContent; + Caption = 'Format Code'; + ToolTip = 'Rather than using Format1 and Format2, a Format Code may be entered to give the size of the Paper/Substrate or Plate.'; + TableRelation = "PVS Format Code" where(Type = filter(Miscellaneous | "Final format")); + ValidateTableRelation = false; + + trigger OnValidate() + var + GeneralSetup: Record "PVS General Setup"; + FormatCode: Record "PVS Format Code"; + begin + if not FormatCode.Get(Rec."Format Code") then + exit; + + if GeneralSetup.Get() then; + + if GeneralSetup."Manual Grain Direction" then begin + Rec.Validate(Width, FormatCode."Width Centimeter"); + Rec.Validate(Length, FormatCode."Length Centimeter"); + end else + if GeneralSetup."Format Entry" = GeneralSetup."format entry"::"Depth x Width" then begin + Rec.Validate("Format1", FormatCode."Length Centimeter"); + Rec.Validate("Format2", FormatCode."Width Centimeter"); + end else begin + Rec.Validate("Format1", FormatCode."Width Centimeter"); + Rec.Validate("Format2", FormatCode."Length Centimeter"); + end; + end; + } + field(6010051; "Format1"; Decimal) + { + DataClassification = CustomerContent; + AutoFormatType = 0; + DecimalPlaces = 2 : 6; + BlankZero = true; + Caption = 'Format1'; + ToolTip = 'Size of the current product (width or length depending on setup).'; + } + field(6010052; "Format2"; Decimal) //widrg + { + DataClassification = CustomerContent; + AutoFormatType = 0; + DecimalPlaces = 2 : 6; + BlankZero = true; + Caption = 'Format2'; + ToolTip = 'Size of the current product (width or length depending on setup).'; + } + //Refactored to Length and Width fields only used for the format code validation, the Format1 and Format2 fields are used for the actual format code values. + field(6010130; Length; Decimal) + { + DataClassification = CustomerContent; + AutoFormatType = 0; + DecimalPlaces = 2 : 6; + Caption = 'Length'; + ToolTip = 'Specifies the length used for format code validation.'; + Description = 'PRINTVIS'; + } + //Refactored to Length and Width fields only used for the format code validation, the Format1 and Format2 fields are used for the actual format code values. + + field(6010131; Width; Decimal) + { + DataClassification = CustomerContent; + AutoFormatType = 0; + DecimalPlaces = 2 : 6; + Caption = 'Width'; + ToolTip = 'Specifies the width used for format code validation.'; + } + + field(6010054; "Weight"; Decimal) + { + DataClassification = CustomerContent; + AutoFormatType = 0; + DecimalPlaces = 2 : 6; + BlankZero = true; + Caption = 'Weight'; + ToolTip = 'Displays the weight of the Paper.'; + } + field(6010073; "Imposition Code"; Code[20]) + { + DataClassification = CustomerContent; + Caption = 'Imposition Code'; + TableRelation = "PVS Imposition Code"; + ToolTip = 'Use to link a specific Imposition Type to the Item that will be transferred to the new case if: 1.Field is empty in template, AND 2. Number of items in the length and width on imposition code must match length and width [leaves] of the template case, or the "Manual Pages On The Sheet" is set to True, by entering manual pages on sheet or impose 1/2.'; + + trigger OnValidate() + var + ImpositionRec: Record "PVS Imposition Code"; + begin + if "Imposition Code" <> '' then begin + ImpositionRec.Get("Imposition Code"); + Rec."Format1" := ImpositionRec."Format 1"; + Rec."Format2" := ImpositionRec."Format 2"; + end; + end; + } + field(6010105; "Paper No."; Code[20]) + { + DataClassification = CustomerContent; + Caption = 'Paper No.'; + ToolTip = 'Specifies the paper/substrate item number.'; + TableRelation = Item; + + trigger OnValidate() + var + ItemRec: Record Item; + begin + // Mirror PVS Job Sheet.Change_Paper: pull the paper weight from the item. + // Only overwrite when the item has a weight defined, to avoid clearing a + // manually entered value when the paper item has no weight. + if ItemRec.Get("Paper No.") then + if ItemRec."PVS Weight" <> 0 then + Validate(Weight, ItemRec."PVS Weight"); + end; + } + field(6010115; "Paper Description"; Text[100]) + { + CalcFormula = lookup(Item.Description where("No." = field("Paper No."))); + Caption = 'Paper Description'; + ToolTip = 'Specifies the description of the paper/substrate item.'; + Editable = false; + FieldClass = FlowField; + } + field(6010106; "Finishing"; Code[20]) + { + DataClassification = CustomerContent; + Caption = 'Finishing'; + ToolTip = 'If a certain Finishing Type code is to be added to estimations for this Job Item, enter it here.'; + TableRelation = "PVS Finishing Types"; + } + field(6010330; "Colors Front"; Integer) + { + DataClassification = CustomerContent; + Caption = 'Colors Front'; + ToolTip = 'Displays number of colors on the front side.'; + } + field(6010331; "Colors Back"; Integer) + { + DataClassification = CustomerContent; + Caption = 'Colors Back'; + ToolTip = 'Displays number of colors on the back side.'; + } + field(6010401; "Tool 1"; Code[20]) + { + DataClassification = CustomerContent; + captionclass = 'PVS,FIELD6010388-1'; + Caption = 'Tool 1'; + ToolTip = 'Displays the first Tool for the current PrintVis Product, to properly load the required tools when doing re-runs of the product.'; + TableRelation = "PVS Tool"; + } + field(6010402; "Tool 2"; Code[20]) + { + DataClassification = CustomerContent; + captionclass = 'PVS,FIELD6010388-2'; + Caption = 'Tool 2'; + ToolTip = 'Displays the second Tool for the current PrintVis Product, to properly load the required tools when doing re-runs of the product.'; + TableRelation = "PVS Tool"; + } + field(6010403; "Tool 3"; Code[20]) + { + DataClassification = CustomerContent; + captionclass = 'PVS,FIELD6010388-3'; + Caption = 'Tool 3'; + ToolTip = 'Displays the third Tool for the current PrintVis Product, to properly load the required tools when doing re-runs of the product.'; + TableRelation = "PVS Tool"; + } + field(6010404; "Tool 4"; Code[20]) + { + DataClassification = CustomerContent; + captionclass = 'PVS,FIELD6010388-4'; + Caption = 'Tool 4'; + ToolTip = 'Displays the fourth Tool for the current PrintVis Product, to properly load the required tools when doing re-runs of the product.'; + TableRelation = "PVS Tool"; + } + field(6010410; "Media Type"; Option) + { + DataClassification = CustomerContent; + Caption = 'Media Type'; + ToolTip = 'Specifies the media type (e.g. Envelope).'; + OptionCaption = ' ,Envelope,Envelope Plain,Envelope Window'; + OptionMembers = " ",Envelope,"Envelope Plain","Envelope Window"; + } + field(6010420; "List Of Units"; Code[20]) + { + DataClassification = CustomerContent; + Caption = 'List Of Units'; + ToolTip = 'Specifies the list of units code.'; + TableRelation = "PVS Calculation Unit Setup".Code where("List Of Units" = const(true)); + } + field(6010411; "Envelope Window Shape Type"; Option) + { + DataClassification = CustomerContent; + Caption = 'Envelope Window Shape Type'; + ToolTip = 'Specifies the shape type of the envelope window.'; + OptionCaption = ' ,Rectangular,Round,Path'; + OptionMembers = " ",Rectangular,Round,Path; + } + field(6010412; "Envelope Window Size X"; Decimal) + { + DataClassification = CustomerContent; + AutoFormatType = 0; + DecimalPlaces = 2 : 6; + BlankZero = true; + Caption = 'Envelope Window Size X'; + ToolTip = 'Specifies the horizontal size of the envelope window.'; + } + field(6010413; "Envelope Window Size Y"; Decimal) + { + DataClassification = CustomerContent; + AutoFormatType = 0; + DecimalPlaces = 2 : 6; + BlankZero = true; + Caption = 'Envelope Window Size Y'; + ToolTip = 'Specifies the vertical size of the envelope window.'; + } + field(6010414; "Opacity"; Option) + { + DataClassification = CustomerContent; + Caption = 'Opacity'; + ToolTip = 'Specifies the opacity type of the product.'; + OptionCaption = ' ,Opaque,Translucent,Transparent'; + OptionMembers = " ",Opaque,Translucent,Transparent; + } + field(6010415; "Opacity Level"; Decimal) + { + DataClassification = CustomerContent; + AutoFormatType = 0; + BlankZero = true; + Caption = 'Opacity Level'; + ToolTip = 'Specifies the opacity level of the product.'; + DecimalPlaces = 0 : 0; + } + field(70; "Component Type"; Code[20]) + { + DataClassification = CustomerContent; + Caption = 'Component Type'; + ToolTip = 'Specifies the component type for this job item, defining how it is treated in the production calculation.'; + TableRelation = "PVS Component Types"; + } + field(71; "Component Type Description"; Text[50]) + { + CalcFormula = lookup("PVS Component Types".Description where(Code = field("Component Type"))); + Caption = 'Component Type Description'; + ToolTip = 'Specifies the description of the component type.'; + Editable = false; + FieldClass = FlowField; + } + field(23; "No. Of Pages"; Integer) + { + DataClassification = CustomerContent; + BlankZero = true; + Caption = 'No. of Pages'; + ToolTip = 'Specifies the total number of pages for this job item. Must be an even number.'; + + trigger OnValidate() + var + unevenErr: Label 'No. of pages cannot be uneven'; + begin + if ("No. Of Pages" mod 2) <> 0 then + Error(unevenErr); + end; + } + field(25; "Pages With Print"; Decimal) + { + DataClassification = CustomerContent; + BlankZero = true; + Caption = 'Pages with Print'; + ToolTip = 'Specifies the number of pages that carry print for this job item.'; + DecimalPlaces = 0 : 4; + } + } + + keys + { + key(Key1; "Product Code", "Job Item No.") + { + Clustered = true; + } + } + + fieldgroups + { + // Add changes to field groups here + } + + trigger OnInsert() + var + ProductJobItem: Record "PTE Product Job Item"; + ProductRec: Record "PVS Product"; + begin + if "Job Item No." = 0 then begin + ProductJobItem.SetRange("Product Code", "Product Code"); + if ProductJobItem.FindLast() then + "Job Item No." := ProductJobItem."Job Item No." + 1 + else + "Job Item No." := 1; + end; + ProductRec.Get("Product Code"); + if ProductRec."Format Code" <> '' then + Validate("Format Code", ProductRec."Format Code") + else begin + + "Format1" := ProductRec."Format1"; + "Format2" := ProductRec."Format2"; + Length := ProductRec.Length; + Width := ProductRec.Width; + end; + Weight := ProductRec.Weight; + "Imposition Code" := ProductRec."Imposition Code"; + "Paper No." := ProductRec."Paper No."; + Finishing := ProductRec.Finishing; + "Colors Front" := ProductRec."Colors Front"; + "Colors Back" := ProductRec."Colors Back"; + "Tool 1" := ProductRec."Tool 1"; + "Tool 2" := ProductRec."Tool 2"; + "Tool 3" := ProductRec."Tool 3"; + "Tool 4" := ProductRec."Tool 4"; + "Media Type" := ProductRec."Media Type"; + "Envelope Window Shape Type" := ProductRec."Envelope Window Shape Type"; + "Envelope Window Size X" := ProductRec."Envelope Window Size X"; + "Envelope Window Size Y" := ProductRec."Envelope Window Size Y"; + Opacity := ProductRec.Opacity; + "Opacity Level" := ProductRec."Opacity Level"; + + end; + + trigger OnModify() + begin + + end; + + trigger OnDelete() + begin + + end; + + trigger OnRename() + begin + + end; + +} diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemColors.Page.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemColors.Page.al new file mode 100644 index 0000000..4b3be60 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemColors.Page.al @@ -0,0 +1,136 @@ +/// +/// Popup list page showing PVS Product Materials (inks / consumables) for a specific +/// Product Job Item. Opened from the "PTE Product Job Item Sub" page via the +/// "Colors / Materials" action or by drilling down on the Colors Front / Colors Back fields. +/// +page 75002 "PTE Product Job Item Colors" +{ + PageType = List; + SourceTable = "PVS Product Materials"; + Caption = 'Job Item Colors / Materials'; + ApplicationArea = All; + InsertAllowed = true; + DeleteAllowed = true; + + layout + { + area(Content) + { + repeater(Lines) + { + field("Item Quality Code"; Rec."Item Quality Code") + { + ApplicationArea = All; + ToolTip = 'Specifies the quality of the ink or consumable item.'; + } + field("Item No."; Rec."Item No.") + { + ApplicationArea = All; + ToolTip = 'Specifies the ink or consumable item number.'; + + trigger OnLookup(var Text: Text): Boolean + var + Item: Record Item; + begin + // When an Item Quality Code is specified on the line, restrict the + // Item drilldown/lookup to items that share the same quality code. + if Rec."Item Quality Code" <> '' then + Item.SetRange("PVS Item Quality Code", Rec."Item Quality Code"); + + if Page.RunModal(0, Item) = Action::LookupOK then begin + Text := Item."No."; + exit(true); + end; + exit(false); + end; + } + field(Text; Rec.Text) + { + ApplicationArea = All; + ToolTip = 'Description transferred from the selected item, may be overwritten.'; + } + field("Text 2"; Rec."Text 2") + { + ApplicationArea = All; + ToolTip = 'Secondary description transferred from the selected item, may be overwritten.'; + } + field("F/B"; Rec."F/B") + { + ApplicationArea = All; + ToolTip = 'Specifies whether the color is printed on the Front, Back, or Both sides.'; + } + field(Type; Rec.Type) + { + ApplicationArea = All; + ToolTip = 'Specifies the color type: Base Color, Spot Color, or Other.'; + } + field("Coverage Pct."; Rec."Coverage Pct.") + { + ApplicationArea = All; + ToolTip = 'Specifies the percentage of the sheet area covered with this ink at 100% coverage.'; + } + field("Area Per Weight"; Rec."Area Per Weight") + { + ApplicationArea = All; + ToolTip = 'Specifies how much area is covered with one unit of ink at 100% coverage.'; + } + field("No. Of Washups"; Rec."No. Of Washups") + { + ApplicationArea = All; + ToolTip = 'Specifies the number of color changes and washups to count for this sheet.'; + } + field("Cliche No."; Rec."Cliche No.") + { + ApplicationArea = All; + ToolTip = 'Specifies the cliche item number for flexo production.'; + Visible = false; + } + field("Plate No."; Rec."Plate No.") + { + ApplicationArea = All; + ToolTip = 'Specifies the printing plate item number.'; + Visible = false; + } + field("Job Item No."; Rec."Job Item No.") + { + ApplicationArea = All; + ToolTip = 'Specifies which Product Job Item this material line belongs to.'; + } + field("Product Code"; Rec."Product Code") + { + ApplicationArea = All; + ToolTip = 'Specifies the product code.'; + Visible = false; + } + field("Entry No."; Rec."Entry No.") + { + ApplicationArea = All; + ToolTip = 'Specifies the entry number.'; + Visible = false; + } + } + } + } + + trigger OnNewRecord(BelowxRec: Boolean) + var + ProductMaterials: Record "PVS Product Materials"; + FilterProductCode: Code[20]; + FilterJobItemNo: Integer; + begin + // Inherit the filtered Product Code and Job Item No. so every new line + // is automatically linked to the correct product and job item. + FilterProductCode := CopyStr(Rec.GetFilter("Product Code"), 1, MaxStrLen(Rec."Product Code")); + Evaluate(FilterJobItemNo, Rec.GetFilter("Job Item No.")); + + Rec."Product Code" := FilterProductCode; + Rec."Job Item No." := FilterJobItemNo; + + // Auto-increment Entry No. within this product. + ProductMaterials.SetRange("Product Code", FilterProductCode); + if ProductMaterials.FindLast() then + Rec."Entry No." := ProductMaterials."Entry No." + 1 + else + Rec."Entry No." := 1; + end; +} diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al new file mode 100644 index 0000000..a10e053 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al @@ -0,0 +1,241 @@ +page 75001 "PTE Product Job Item Sub" +{ + PageType = ListPart; + SourceTable = "PTE Product Job Item"; + Caption = 'Product Job Items'; + InsertAllowed = false; + DeleteAllowed = true; + DelayedInsert = true; + AutoSplitKey = true; + SourceTableView = sorting("Product Code", "Job Item No."); + ApplicationArea = All; + + layout + { + area(Content) + { + repeater(General) + { + field("Product Code"; Rec."Product Code") + { + + Visible = false; + } + field("Job Item No."; Rec."Job Item No.") + { + + } + field("Component Type"; Rec."Component Type") + { + + } + field("Component Type Description"; Rec."Component Type Description") + { + + } + field("No. Of Pages"; Rec."No. Of Pages") + { + + } + field("Pages With Print"; Rec."Pages With Print") + { + + } + field("Format Code"; Rec."Format Code") + { + + } + field("Format1"; Rec."Format1") + { + + } + field("Format2"; Rec."Format2") + { + + } + field(Weight; Rec.Weight) + { + + } + field("Imposition Code"; Rec."Imposition Code") + { + + } + field("Paper No."; Rec."Paper No.") + { + + } + field("Paper Description"; Rec."Paper Description") + { + + } + field(Finishing; Rec.Finishing) + { + + } + field("Colors Front"; Rec."Colors Front") + { + + trigger OnDrillDown() + begin + OpenColorsPage(); + end; + } + field("Colors Back"; Rec."Colors Back") + { + + trigger OnDrillDown() + begin + OpenColorsPage(); + end; + } + field("Media Type"; Rec."Media Type") + { + + } + field("List Of Units"; Rec."List Of Units") + { + + + trigger OnLookup(var Text: Text): Boolean + var + ProductRec: Record "PVS Product"; + Temp_CalcUnitSetup: Record "PVS Calculation Unit Setup" temporary; + PageMgt: Codeunit "PVS Page Management"; + ProductGroupCode: Code[20]; + begin + if ProductRec.Get(Rec."Product Code") then + ProductGroupCode := ProductRec."Product Group Code"; + + PageMgt.Get_ListOfUnitTMP(ProductGroupCode, false, Temp_CalcUnitSetup); + + Commit(); + if Page.RunModal(Page::"PVS Calculation List of Units", Temp_CalcUnitSetup) = Action::LookupOK then begin + Rec.Validate("List Of Units", Temp_CalcUnitSetup.Code); + Text := Temp_CalcUnitSetup.Code; + exit(true); + end; + end; + } + field("Envelope Window Shape Type"; Rec."Envelope Window Shape Type") + { + + } + field("Envelope Window Size X"; Rec."Envelope Window Size X") + { + + } + field("Envelope Window Size Y"; Rec."Envelope Window Size Y") + { + + } + field(Opacity; Rec.Opacity) + { + + } + field("Opacity Level"; Rec."Opacity Level") + { + + } + field("Tool1"; Rec."Tool 1") + { + + } + field("Tool2"; Rec."Tool 2") + { + + } + field("Tool3"; Rec."Tool 3") + { + + } + field("Tool4"; Rec."Tool 4") + { + + } + } + } + + } + + actions + { + area(Processing) + { + action(NewJobItem) + { + + Caption = 'New Job Item'; + ToolTip = 'Creates a new product job item line for this product.'; + Image = NewRow; + + trigger OnAction() + var + ProductJobItem: Record "PTE Product Job Item"; + begin + ProductJobItem.Init(); + ProductJobItem."Product Code" := Rec."Product Code"; + ProductJobItem.Insert(true); + CurrPage.Update(false); + end; + } + action(CopyJobItem) + { + + Caption = 'Copy Job Item'; + ToolTip = 'Creates a copy of the current product job item line.'; + Image = Copy; + Enabled = HasJobItemLines; + trigger OnAction() + var + NewProductJobItem: Record "PTE Product Job Item"; + begin + Rec.TestField("Product Code"); + NewProductJobItem := Rec; + NewProductJobItem."Job Item No." := 0; + NewProductJobItem.Insert(true); + CurrPage.Update(false); + end; + } + action(ViewColors) + { + ApplicationArea = All; + Caption = 'Colors / Materials'; + ToolTip = 'Opens the colors and materials lines for the selected job item.'; + Image = ItemLedger; + trigger OnAction() + begin + OpenColorsPage(); + end; + } + } + + } + + trigger OnAfterGetCurrRecord() + var + ProductJobItem: Record "PTE Product Job Item"; + begin + ProductJobItem.SetRange("Product Code", Rec."Product Code"); + HasJobItemLines := not ProductJobItem.IsEmpty(); + end; + + var + HasJobItemLines: Boolean; + + procedure GetCurrentJobItemNo(): Integer + begin + exit(Rec."Job Item No."); + end; + + local procedure OpenColorsPage() + var + ProductMaterials: Record "PVS Product Materials"; + begin + Rec.TestField("Product Code"); + ProductMaterials.SetRange("Product Code", Rec."Product Code"); + if Rec."Job Item No." <> 0 then + ProductMaterials.SetRange("Job Item No.", Rec."Job Item No."); + Page.RunModal(Page::"PTE Product Job Item Colors", ProductMaterials); + end; +} diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductSetupState.Codeunit.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductSetupState.Codeunit.al new file mode 100644 index 0000000..d1bc119 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductSetupState.Codeunit.al @@ -0,0 +1,125 @@ +/// +/// SingleInstance codeunit that holds transient UI state for the PVS Product Setup pages. +/// Used to communicate the currently selected Product Job Item No. from the job item +/// subpage to sibling subpages (e.g. the Item Colors Part) without requiring a round-trip +/// through the parent page record. +/// +codeunit 75003 "PTE Product Setup State" +{ + SingleInstance = true; + + var + CurrentJobItemNo: Integer; + CurrentProductCode: Code[20]; + + procedure SetCurrentJobItem(ProductCode: Code[20]; JobItemNo: Integer) + begin + CurrentProductCode := ProductCode; + CurrentJobItemNo := JobItemNo; + end; + + procedure GetCurrentJobItemNo(): Integer + begin + exit(CurrentJobItemNo); + end; + + procedure GetCurrentProductCode(): Code[20] + begin + exit(CurrentProductCode); + end; + + /// + /// Opens the combined User Fields page for a Product. + /// Field/option definitions are read from the Job User Field setup, while values are + /// stored under the Product table keyed by the Product Code. + /// + procedure Form_Userfield_Edit_Product(in_Subtype: Integer; in_ProductCode: Code[50]) + var + TempBufferRec: Record "PVS Userfield Field Buffer" temporary; + UserFieldMgt: Codeunit "PVS Userfield Management"; + DataCode: Code[20]; + begin + DataCode := CopyStr(in_ProductCode, 1, MaxStrLen(DataCode)); + if DataCode = '' then + exit; + + UserFieldMgt.Populate_Initial_Values(Database::"PVS Product", in_Subtype, DataCode, '', 0, 0, 0, 0, 0); + Commit(); + + TempBufferRec.SetRange("Table ID", Database::"PVS Product"); + TempBufferRec.SetRange("Table Subtype", in_Subtype); + TempBufferRec.SetRange("Table Code", DataCode); + TempBufferRec.SetRange(ID1, 0); + TempBufferRec.SetRange(ID2, 0); + TempBufferRec.SetRange(ID3, 0); + TempBufferRec.SetRange(ID4, 0); + TempBufferRec.SetRange(ID5, 0); + UserFieldMgt.Populate_Fields_Tmp_Buffer(TempBufferRec, true, 0, 0, 0, 0); + + Page.Run(Page::"PVS Userfields Combined", TempBufferRec); + end; + + /// + /// Copies the Product User Field values onto the Job User Fields when a Product is applied to a Job. + /// + procedure Transfer_Product_Userfields_To_Job(in_ProductCode: Code[50]; in_JobID: Integer; in_JobNo: Integer; in_Version: Integer) + var + FieldRec: Record "PVS Userfield Field"; + ProductValueRec: Record "PVS Userfield Field Value"; + JobValueRec: Record "PVS Userfield Field Value"; + ProductCode: Code[20]; + Subtype: Integer; + begin + ProductCode := CopyStr(in_ProductCode, 1, MaxStrLen(ProductCode)); + if ProductCode = '' then + exit; + if in_JobID = 0 then + exit; + + for Subtype := 0 to 2 do begin + FieldRec.Reset(); + FieldRec.SetRange("Table ID", Database::"PVS Job"); + FieldRec.SetRange("Table Subtype", Subtype); + FieldRec.SetRange("Table Code", ''); + if FieldRec.FindSet() then + repeat + ProductValueRec.Reset(); + ProductValueRec.SetRange("Table ID", Database::"PVS Product"); + ProductValueRec.SetRange("Table Subtype", Subtype); + ProductValueRec.SetRange("Table Code", ProductCode); + ProductValueRec.SetRange("Field No.", FieldRec."Field No."); + ProductValueRec.SetRange(ID1, 0); + ProductValueRec.SetRange(ID2, 0); + ProductValueRec.SetRange(ID3, 0); + ProductValueRec.SetRange(ID4, 0); + ProductValueRec.SetRange(ID5, 0); + ProductValueRec.SetRange(Code1, ''); + if ProductValueRec.FindFirst() then + if ProductValueRec.Text <> '' then + if not JobValueRec.Get(Database::"PVS Job", Subtype, '', FieldRec."Field No.", + in_JobID, in_JobNo, in_Version, 0, 0, '', 10000) + then begin + JobValueRec.Init(); + JobValueRec."Table ID" := Database::"PVS Job"; + JobValueRec."Table Subtype" := Subtype; + JobValueRec."Table Code" := ''; + JobValueRec."Field No." := FieldRec."Field No."; + JobValueRec.ID1 := in_JobID; + JobValueRec.ID2 := in_JobNo; + JobValueRec.ID3 := in_Version; + JobValueRec.ID4 := 0; + JobValueRec.ID5 := 0; + JobValueRec.Code1 := ''; + JobValueRec."Entry No." := 10000; + JobValueRec."Option Value Entry No." := ProductValueRec."Option Value Entry No."; + JobValueRec.Text := ProductValueRec.Text; + JobValueRec.Insert(true); + end else begin + JobValueRec."Option Value Entry No." := ProductValueRec."Option Value Entry No."; + JobValueRec.Text := ProductValueRec.Text; + JobValueRec.Modify(true); + end; + until FieldRec.Next() = 0; + end; + end; +} diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al new file mode 100644 index 0000000..f743b79 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al @@ -0,0 +1,318 @@ +codeunit 75004 "PTE Product Template Mgt" +{ + trigger OnRun() + begin + end; + + /// + /// When a product is validated on a PVS Job, apply the product header fields + /// (Format Code / Colors), transfer the product user fields to the job, and sync + /// all matching PVS Job Item records with the field values from the corresponding + /// PTE Product Job Item template lines. + /// Matching is by Job Item No. Only primary lines (Entry No. = 1) are targeted. + /// Extra existing PVS Job Items without a template match are left untouched. + /// + [EventSubscriber(ObjectType::Codeunit, Codeunit::"PVS Product Management", OnAfterValidateProductPVSJob, '', false, false)] + local procedure OnAfterValidateProductPVSJob(var Job: Record "PVS Job"; var xJob: Record "PVS Job") + var + ProductSetupState: Codeunit "PTE Product Setup State"; + begin + // 1. Copy product header fields (Format Code / Colors) to the job header. + ApplyProductHeaderFields(Job); + + // 2. Transfer the product user field values to the job. + ProductSetupState.Transfer_Product_Userfields_To_Job(Job."Product Code", Job.ID, Job.Job, Job.Version); + + // 3. Sync template lines to the job items. + SyncTemplateLinesToJobItems(Job); + end; + + /// + /// Copies the product header fields (Format Code, Colors Front, Colors Back) to the + /// PVS Job when a product is applied. Formerly executed inside + /// PVS Product Management.ValidateProductPVSJob; moved here so the base app remains + /// unchanged. Persisted with Modify(false) so the values survive the subsequent + /// Job.Get refresh performed by SyncTemplateLinesToJobItems. + /// + local procedure ApplyProductHeaderFields(var Job: Record "PVS Job") + var + Product: Record "PVS Product"; + Changed: Boolean; + begin + if Job."Product Code" = '' then + exit; + if not Product.Get(Job."Product Code") then + exit; + + if (Product."Format Code" <> '') and (Job."Format Code" <> Product."Format Code") then begin + Job.Validate("Format Code", Product."Format Code"); + Changed := true; + end; + + if (Product."Colors Front" <> 0) or (Product."Colors Back" <> 0) then begin + if Job."Colors Front" <> Product."Colors Front" then begin + Job.Validate("Colors Front", Product."Colors Front"); + Changed := true; + end; + if Job."Colors Back" <> Product."Colors Back" then begin + Job.Validate("Colors Back", Product."Colors Back"); + Changed := true; + end; + end; + + if Changed then + Job.Modify(false); + + OnAfterApplyProductHeaderFields(Job); + end; + + local procedure SyncTemplateLinesToJobItems(var Job: Record "PVS Job") + var + TmplLine: Record "PTE Product Job Item"; + JobItem: Record "PVS Job Item"; + JobSheet: Record "PVS Job Sheet"; + PageMgt: Codeunit "PVS Page Management"; + SheetMgt: Codeunit "PVS Sheet Management"; + UnitCode: Code[20]; + Changed: Boolean; + SheetChanged: Boolean; + begin + if Job."Product Code" = '' then + exit; + + TmplLine.SetRange("Product Code", Job."Product Code"); + TmplLine.SetCurrentKey("Product Code", "Job Item No."); + if not TmplLine.FindSet() then + exit; + + repeat + // Locate primary PVS Job Item for this job + template line number + JobItem.SetRange(ID, Job.ID); + JobItem.SetRange(Job, Job.Job); + JobItem.SetRange(Version, Job.Version); + JobItem.SetRange("Job Item No.", TmplLine."Job Item No."); + JobItem.SetRange("Entry No.", 1); + if not JobItem.FindFirst() then begin + // Job Item does not exist: create a new Sheet + Job Item via PVS Sheet Management. + // Event_Create_Sheet takes var in_Rec and calls FindLast() before returning, + // so JobItem is already positioned on the newly created record afterwards. + JobItem.Reset(); + JobItem.ID := Job.ID; + JobItem.Job := Job.Job; + JobItem.Version := Job.Version; + SheetMgt.Event_Create_Sheet(JobItem, false); + + // If the record was not repositioned (creation failed), skip this line. + if JobItem."Entry No." = 0 then + continue; + end; + + Clear(Changed); + + // --- 1. Component Type --- + // Apply first: sets description only, no size or calc cascades. + if (TmplLine."Component Type" <> '') and (JobItem."Component Type" <> TmplLine."Component Type") then begin + JobItem.Validate("Component Type", TmplLine."Component Type"); + Changed := true; + end; + + // --- 2. No. Of Pages --- + // Standalone even-number validation. Must precede Pages With Print. + if JobItem."No. Of Pages" <> TmplLine."No. Of Pages" then + if (TmplLine."No. Of Pages" <> 0) and ((TmplLine."No. Of Pages" mod 2) = 0) then begin + JobItem.Validate("No. Of Pages", TmplLine."No. Of Pages"); + Changed := true; + end; + + // --- 3. Pages With Print --- + // Logically depends on No. Of Pages; validate after. + if (TmplLine."Pages With Print" <> 0) and (JobItem."Pages With Print" <> TmplLine."Pages With Print") then begin + JobItem.Validate("Pages With Print", TmplLine."Pages With Print"); + Changed := true; + end; + + // --- 4-5. Size: Length and Width --- + // Resolved from template using priority: Imposition wins > Format Code > direct Length/Width. + // Length validated before Width to trigger imposition recalc only once. + ApplyTemplateSizeToJobItem(TmplLine, JobItem, Changed); + + // --- 6-7. Colors --- + // Standalone; no cascades between front and back. + if (TmplLine."Colors Front" <> 0) and (JobItem."Colors Front" <> TmplLine."Colors Front") then begin + JobItem.Validate("Colors Front", TmplLine."Colors Front"); + Changed := true; + end; + if (TmplLine."Colors Back" <> 0) and (JobItem."Colors Back" <> TmplLine."Colors Back") then begin + JobItem.Validate("Colors Back", TmplLine."Colors Back"); + Changed := true; + end; + + // --- 8. Paper --- + // The Job Item's "Paper Item No." is a non-editable FlowField into the Sheet, + // so Paper (like Weight) is applied at sheet level below - see step 19. + + // --- 9-12. Tools --- + // All standalone; no cascades between tool slots. + if (TmplLine."Tool 1" <> '') and (JobItem.Tool <> TmplLine."Tool 1") then begin + JobItem.Validate(Tool, TmplLine."Tool 1"); + Changed := true; + end; + if (TmplLine."Tool 2" <> '') and (JobItem."Tool 2" <> TmplLine."Tool 2") then begin + JobItem.Validate("Tool 2", TmplLine."Tool 2"); + Changed := true; + end; + if (TmplLine."Tool 3" <> '') and (JobItem."Tool 3" <> TmplLine."Tool 3") then begin + JobItem.Validate("Tool 3", TmplLine."Tool 3"); + Changed := true; + end; + if (TmplLine."Tool 4" <> '') and (JobItem."Tool 4" <> TmplLine."Tool 4") then begin + JobItem.Validate("Tool 4", TmplLine."Tool 4"); + Changed := true; + end; + + // --- 13-18. Media / Envelope / Opacity --- + // All standalone; grouped by functional area to minimize record traffic. + if (TmplLine."Media Type" <> TmplLine."Media Type"::" ") and (JobItem."Media Type" <> TmplLine."Media Type") then begin + JobItem.Validate("Media Type", TmplLine."Media Type"); + Changed := true; + end; + if (TmplLine."Envelope Window Shape Type" <> TmplLine."Envelope Window Shape Type"::" ") and (JobItem."Envelope Window Shape Type" <> TmplLine."Envelope Window Shape Type") then begin + JobItem.Validate("Envelope Window Shape Type", TmplLine."Envelope Window Shape Type"); + Changed := true; + end; + if (TmplLine."Envelope Window Size X" <> 0) and (JobItem."Envelope Window Size X" <> TmplLine."Envelope Window Size X") then begin + JobItem.Validate("Envelope Window Size X", TmplLine."Envelope Window Size X"); + Changed := true; + end; + if (TmplLine."Envelope Window Size Y" <> 0) and (JobItem."Envelope Window Size Y" <> TmplLine."Envelope Window Size Y") then begin + JobItem.Validate("Envelope Window Size Y", TmplLine."Envelope Window Size Y"); + Changed := true; + end; + if (TmplLine.Opacity <> TmplLine.Opacity::" ") and (JobItem.Opacity <> TmplLine.Opacity) then begin + JobItem.Validate(Opacity, TmplLine.Opacity); + Changed := true; + end; + if (TmplLine."Opacity Level" <> 0) and (JobItem."Opacity Level" <> TmplLine."Opacity Level") then begin + JobItem.Validate("Opacity Level", TmplLine."Opacity Level"); + Changed := true; + end; + + // Persist all field changes in a single Modify before List Of Units. + // Use Modify(false) to avoid triggering PVS Job Item's OnModify cascade, + // which would call Modify() on the parent PVS Job record and cause an + // optimistic concurrency conflict when the page tries to save Product Code. + // Field-level triggers have already run via Validate() above. + if Changed then + JobItem.Modify(false); + + // --- 19. Paper / Weight --- + // Both are stored on the PVS Job Sheet (the Job Item's "Paper Item No." and + // "Paper Weight" are FlowField lookups into it). Applied at sheet level so their + // OnValidate cascades run. Paper is applied before Weight because changing paper + // (Change_Paper) can reset the default weight, which we then override. + // Skipped silently when no Sheet exists yet (Sheet ID = 0). + if JobItem."Sheet ID" <> 0 then + if JobSheet.Get(JobItem."Sheet ID") then begin + Clear(SheetChanged); + if (TmplLine."Paper No." <> '') and (JobSheet."Paper Item No." <> TmplLine."Paper No.") then begin + JobSheet.Validate("Paper Item No.", TmplLine."Paper No."); + SheetChanged := true; + end; + if (TmplLine.Weight <> 0) and (JobSheet.Weight <> TmplLine.Weight) then begin + JobSheet.Validate(Weight, TmplLine.Weight); + SheetChanged := true; + end; + if SheetChanged then + JobSheet.Modify(true); + end; + + // --- 20. List Of Units --- + // Always validated when set; uses PVS sheet-level unit assignment logic. + // Must be after Modify so Job_Item_Input_Unit reads the persisted state. + // Skipped silently when no Sheet exists yet (Sheet ID = 0). + if TmplLine."List Of Units" <> '' then + if JobItem."Sheet ID" <> 0 then begin + UnitCode := TmplLine."List Of Units"; + PageMgt.Job_Item_Input_Unit(JobItem, UnitCode); + end; + + until TmplLine.Next() = 0; + + // Refresh the Job record from the database. SheetMgt.Event_Create_Sheet and + // field Validate cascades may have internally called Modify() on the PVS Job + // record, bumping its DB timestamp. Because Job is passed as var, re-reading it + // here updates the caller's (PVS Product Management) copy so its subsequent + // Modify() uses the current timestamp and avoids the optimistic concurrency + // conflict on the case card page. + if Job.Get(Job.ID, Job.Job, Job.Version) then; + end; + + /// + /// Resolves the final Length and Width for a PVS Job Item from the template line. + /// Priority: Imposition Code (wins) > Format Code > direct Length/Width values. + /// Validates Length before Width to trigger imposition recalculation only once. + /// + local procedure ApplyTemplateSizeToJobItem(TmplLine: Record "PTE Product Job Item"; var JobItem: Record "PVS Job Item"; var Changed: Boolean) + var + GeneralSetup: Record "PVS General Setup"; + ImpositionRec: Record "PVS Imposition Code"; + FormatCode: Record "PVS Format Code"; + FinalLength: Decimal; + FinalWidth: Decimal; + begin + if not GeneralSetup.Get() then + GeneralSetup.Init(); + + FinalLength := 0; + FinalWidth := 0; + + // Imposition Code wins: read Format 1 / Format 2 from imposition record + if TmplLine."Imposition Code" <> '' then begin + if ImpositionRec.Get(TmplLine."Imposition Code") then + if GeneralSetup."Format Entry" = GeneralSetup."format entry"::"Depth x Width" then begin + // Depth x Width: Format 1 = depth/length, Format 2 = width + FinalLength := ImpositionRec."Format 1"; + FinalWidth := ImpositionRec."Format 2"; + end else begin + // Width x Length (default): Format 1 = width, Format 2 = length + FinalWidth := ImpositionRec."Format 1"; + FinalLength := ImpositionRec."Format 2"; + end; + end else + if TmplLine."Format Code" <> '' then begin + // Format Code: resolve centimeters via setup + if FormatCode.Get(TmplLine."Format Code") then + if GeneralSetup."Manual Grain Direction" then begin + FinalWidth := FormatCode."Width Centimeter"; + FinalLength := FormatCode."Length Centimeter"; + end else + if GeneralSetup."Format Entry" = GeneralSetup."format entry"::"Depth x Width" then begin + FinalLength := FormatCode."Length Centimeter"; + FinalWidth := FormatCode."Width Centimeter"; + end else begin + FinalWidth := FormatCode."Width Centimeter"; + FinalLength := FormatCode."Length Centimeter"; + end; + end else begin + // No format driver: use template Length/Width directly + FinalLength := TmplLine.Length; + FinalWidth := TmplLine.Width; + end; + + // Apply Length first (triggers imposition recalculation internally once), + // then Width; skip zero values to avoid clearing existing data. + if (FinalLength <> 0) and (JobItem.Length <> FinalLength) then begin + JobItem.Validate(Length, FinalLength); + Changed := true; + end; + if (FinalWidth <> 0) and (JobItem.Width <> FinalWidth) then begin + JobItem.Validate(Width, FinalWidth); + Changed := true; + end; + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterApplyProductHeaderFields(var Job: Record "PVS Job") + begin + end; +} From be129565dcd034e8071ecacf91e8470c8fcc7297 Mon Sep 17 00:00:00 2001 From: Kasper Lind Date: Fri, 28 Aug 2026 09:39:08 +0200 Subject: [PATCH 2/5] fix list of unit pop up --- .../ProductSetup/PTEProductJobItem.Table.al | 139 ++++++++++++++++++ .../ProductSetup/PTEProductJobItemSub.Page.al | 34 +++++ .../PTEProductTemplateMgt.Codeunit.al | 51 +++++-- 3 files changed, 210 insertions(+), 14 deletions(-) diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al index ebf911c..2bc008d 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al @@ -206,6 +206,17 @@ table 75000 "PTE Product Job Item" Caption = 'List Of Units'; ToolTip = 'Specifies the list of units code.'; TableRelation = "PVS Calculation Unit Setup".Code where("List Of Units" = const(true)); + + trigger OnValidate() + begin + ResolveConfiguration(); + end; + } + field(6010421; "Configuration"; Code[20]) + { + DataClassification = CustomerContent; + Caption = 'Configuration'; + ToolTip = 'Specifies the Cost Center Configuration to use for the List Of Units. Resolved automatically when unambiguous, or selected manually when the machine/cost center has more than one matching configuration (mirrors the "PVS Calculation Configurations" selection shown on the PVS Job Item).'; } field(6010411; "Envelope Window Shape Type"; Option) { @@ -359,4 +370,132 @@ table 75000 "PTE Product Job Item" end; + /// + /// Resolves the Cost Center Configuration for the current "List Of Units" selection, + /// mirroring the base app's ambiguity handling on the real PVS Job Item (page + /// "PVS Calculation Configurations" / 6010357): candidate configurations are filtered + /// by this line's current Format1/Format2/Weight against each configuration's + /// Min/Max Printing Format and Weight ranges. + /// - 0 matches: "Configuration" is cleared (left for manual/runtime resolution). + /// - 1 match: assigned silently. + /// - 2+ matches: the user is prompted via "PVS Calculation Configurations" to pick one. + /// + local procedure ResolveConfiguration() + var + TempMatchingUnitSetup: Record "PVS Calculation Unit Setup" temporary; + Page_Configurations: Page "PVS Calculation Configurations"; + Config_ARR: array[100] of Code[20]; + Config_Max: Integer; + MatchCount: Integer; + begin + Rec.Configuration := ''; + + if "List Of Units" = '' then + exit; + + MatchCount := GetMatchingConfigurations(TempMatchingUnitSetup); + case MatchCount of + 0: + exit; + 1: + begin + TempMatchingUnitSetup.FindFirst(); + Rec.Configuration := TempMatchingUnitSetup.Configuration; + end; + else begin + Commit(); + Clear(Page_Configurations); + Page_Configurations.Set_Filter_Units(TempMatchingUnitSetup); + if Page_Configurations.RunModal() = Action::OK then begin + Page_Configurations.Find_Marked(Config_Max, Config_ARR); + if Config_Max > 0 then + if TempMatchingUnitSetup.Get(TempMatchingUnitSetup.Type::"Price Unit", Config_ARR[1]) then + Rec.Configuration := TempMatchingUnitSetup.Configuration; + end; + end; + end; + end; + + /// + /// Builds the set of Cost Center Configurations that are compatible with this line's + /// current Format1/Format2/Weight, under the Cost Center resolved from the "List Of + /// Units" calculation unit. Each match is represented by its concrete "PVS Calculation + /// Unit Setup" record, because the "PVS Calculation Configurations" picker page + /// (6010357) actually has SourceTable = "PVS Calculation Unit Setup" (confirmed from + /// the base app symbols) - not "PVS Cost Center Configuration". Also used by the sub + /// page to let the user manually reopen the picker and override the automatically + /// resolved Configuration. + /// + /// Filled with one Calculation Unit Setup record per matching Configuration. + /// The number of matching configurations found. + procedure GetMatchingConfigurations(var TempMatchingUnitSetup: Record "PVS Calculation Unit Setup" temporary): Integer + var + UnitSetup: Record "PVS Calculation Unit Setup"; + CostCenterConfig: Record "PVS Cost Center Configuration"; + CandidateUnitSetup: Record "PVS Calculation Unit Setup"; + begin + TempMatchingUnitSetup.Reset(); + TempMatchingUnitSetup.DeleteAll(); + + if "List Of Units" = '' then + exit(0); + + if not UnitSetup.Get(UnitSetup.Type::"Price Unit", "List Of Units") then + exit(0); + + if UnitSetup."Cost Center Code" = '' then + exit(0); + + CostCenterConfig.SetRange("Cost Center Code", UnitSetup."Cost Center Code"); + if CostCenterConfig.FindSet() then + repeat + if IsConfigurationMatch(CostCenterConfig) then begin + CandidateUnitSetup.Reset(); + CandidateUnitSetup.SetRange(Type, CandidateUnitSetup.Type::"Price Unit"); + CandidateUnitSetup.SetRange("Cost Center Code", CostCenterConfig."Cost Center Code"); + CandidateUnitSetup.SetRange(Configuration, CostCenterConfig.Configuration); + CandidateUnitSetup.SetRange("List Of Units", false); + if CandidateUnitSetup.FindFirst() then + if not TempMatchingUnitSetup.Get(CandidateUnitSetup.Type, CandidateUnitSetup.Code) then begin + TempMatchingUnitSetup := CandidateUnitSetup; + TempMatchingUnitSetup.Insert(); + end; + end; + until CostCenterConfig.Next() = 0; + + exit(TempMatchingUnitSetup.Count()); + end; + + /// + /// Checks whether the given Cost Center Configuration's Min/Max Printing Format and + /// Weight ranges accommodate this line's current Format1/Format2/Weight values. A + /// blank/zero limit on the configuration, or a blank/zero value on this line, is + /// treated as "no constraint" for that dimension. + /// + local procedure IsConfigurationMatch(CostCenterConfig: Record "PVS Cost Center Configuration"): Boolean + begin + if (Format1 <> 0) then begin + if (CostCenterConfig."Max Printing Format Length" <> 0) and (Format1 > CostCenterConfig."Max Printing Format Length") then + exit(false); + if (CostCenterConfig."Min Print Format Length" <> 0) and (Format1 < CostCenterConfig."Min Print Format Length") then + exit(false); + end; + + if (Format2 <> 0) then begin + if (CostCenterConfig."Max Printing Format Width" <> 0) and (Format2 > CostCenterConfig."Max Printing Format Width") then + exit(false); + if (CostCenterConfig."Min Print Format Width" <> 0) and (Format2 < CostCenterConfig."Min Print Format Width") then + exit(false); + end; + + if (Weight <> 0) then begin + if (CostCenterConfig."Max Weight" <> 0) and (Weight > CostCenterConfig."Max Weight") then + exit(false); + if (CostCenterConfig."Min Weight" <> 0) and (Weight < CostCenterConfig."Min Weight") then + exit(false); + end; + + exit(true); + end; + } diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al index a10e053..48a22d1 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al @@ -117,6 +117,40 @@ page 75001 "PTE Product Job Item Sub" end; end; } + field("Configuration"; Rec."Configuration") + { + ToolTip = 'Specifies the Cost Center Configuration resolved for the List Of Units. Automatically resolved when unambiguous; drill down to pick manually when the machine has more than one matching configuration.'; + + trigger OnLookup(var Text: Text): Boolean + var + TempMatchingUnitSetup: Record "PVS Calculation Unit Setup" temporary; + Page_Configurations: Page "PVS Calculation Configurations"; + Config_ARR: array[100] of Code[20]; + Config_Max: Integer; + begin + if Rec."List Of Units" = '' then + exit(false); + + if Rec.GetMatchingConfigurations(TempMatchingUnitSetup) = 0 then + exit(false); + + Commit(); + Clear(Page_Configurations); + Page_Configurations.Set_Filter_Units(TempMatchingUnitSetup); + if Page_Configurations.RunModal() = Action::OK then begin + Page_Configurations.Find_Marked(Config_Max, Config_ARR); + if Config_Max = 0 then + exit(false); + + if not TempMatchingUnitSetup.Get(TempMatchingUnitSetup.Type::"Price Unit", Config_ARR[1]) then + exit(false); + + Rec.Validate("Configuration", TempMatchingUnitSetup.Configuration); + Text := TempMatchingUnitSetup.Configuration; + exit(true); + end; + end; + } field("Envelope Window Shape Type"; Rec."Envelope Window Shape Type") { diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al index f743b79..e45e334 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al @@ -73,9 +73,11 @@ codeunit 75004 "PTE Product Template Mgt" JobSheet: Record "PVS Job Sheet"; PageMgt: Codeunit "PVS Page Management"; SheetMgt: Codeunit "PVS Sheet Management"; + SingleInstance: Codeunit "PVS SingleInstance"; UnitCode: Code[20]; Changed: Boolean; SheetChanged: Boolean; + PrevGUINotAllowed: Boolean; begin if Job."Product Code" = '' then exit; @@ -85,6 +87,13 @@ codeunit 75004 "PTE Product Template Mgt" if not TmplLine.FindSet() then exit; + // Suppress any base-app popups (e.g. "PVS Calculation Configurations" ambiguity + // prompt, or List Of Units surcharge prompts) while template lines are applied + // automatically. The base app is left to auto-resolve a default silently; there + // is no writable Configuration field on "PVS Job Sheet" to force a specific value. + PrevGUINotAllowed := SingleInstance.Get_GUINOTALLOWED(); + SingleInstance.Set_GUINOTALLOWED(true); + repeat // Locate primary PVS Job Item for this job + template line number JobItem.SetRange(ID, Job.ID); @@ -205,15 +214,37 @@ codeunit 75004 "PTE Product Template Mgt" if Changed then JobItem.Modify(false); - // --- 19. Paper / Weight --- - // Both are stored on the PVS Job Sheet (the Job Item's "Paper Item No." and - // "Paper Weight" are FlowField lookups into it). Applied at sheet level so their - // OnValidate cascades run. Paper is applied before Weight because changing paper + // --- 19. List Of Units --- + // Assigns the Controlling (Sheet) Unit and, through it, resolves the Cost + // Center Configuration for the sheet. Must run BEFORE Finishing/Paper/Weight: + // the base app's "PVS Calculation Configurations" selection page is prompted + // when Finishing/Paper/Weight are validated on a sheet whose Configuration has + // not been resolved yet. Manually picking List Of Units first (as done on the + // Product Job Item sub page / base Job Items list) avoids that prompt, so the + // same order is used here. Must be after Modify so Job_Item_Input_Unit reads + // the persisted state. Skipped silently when no Sheet exists yet (Sheet ID = 0). + if TmplLine."List Of Units" <> '' then + if JobItem."Sheet ID" <> 0 then begin + UnitCode := TmplLine."List Of Units"; + PageMgt.Job_Item_Input_Unit(JobItem, UnitCode); + end; + + // --- 20. Finishing / Paper / Weight --- + // All three are stored on the PVS Job Sheet (the Job Item's "Paper Item No." and + // "Paper Weight" are FlowField lookups into it). Applied at sheet level, after + // List Of Units above, so the sheet's Configuration is already resolved and the + // "PVS Calculation Configurations" selection page does not get triggered. Finishing + // is applied before Paper/Weight since it is the more likely input to that + // configuration resolution. Paper is applied before Weight because changing paper // (Change_Paper) can reset the default weight, which we then override. // Skipped silently when no Sheet exists yet (Sheet ID = 0). if JobItem."Sheet ID" <> 0 then if JobSheet.Get(JobItem."Sheet ID") then begin Clear(SheetChanged); + if (TmplLine.Finishing <> '') and (JobSheet.Finishing <> TmplLine.Finishing) then begin + JobSheet.Validate(Finishing, TmplLine.Finishing); + SheetChanged := true; + end; if (TmplLine."Paper No." <> '') and (JobSheet."Paper Item No." <> TmplLine."Paper No.") then begin JobSheet.Validate("Paper Item No.", TmplLine."Paper No."); SheetChanged := true; @@ -226,18 +257,10 @@ codeunit 75004 "PTE Product Template Mgt" JobSheet.Modify(true); end; - // --- 20. List Of Units --- - // Always validated when set; uses PVS sheet-level unit assignment logic. - // Must be after Modify so Job_Item_Input_Unit reads the persisted state. - // Skipped silently when no Sheet exists yet (Sheet ID = 0). - if TmplLine."List Of Units" <> '' then - if JobItem."Sheet ID" <> 0 then begin - UnitCode := TmplLine."List Of Units"; - PageMgt.Job_Item_Input_Unit(JobItem, UnitCode); - end; - until TmplLine.Next() = 0; + SingleInstance.Set_GUINOTALLOWED(PrevGUINotAllowed); + // Refresh the Job record from the database. SheetMgt.Event_Create_Sheet and // field Validate cascades may have internally called Modify() on the PVS Job // record, bumping its DB timestamp. Because Job is passed as var, re-reading it From 8f3a358fa8ae80be97bc2afdf69206013de04082 Mon Sep 17 00:00:00 2001 From: Kasper Lind Date: Thu, 3 Sep 2026 08:40:39 +0200 Subject: [PATCH 3/5] end user doc --- .../Documentation.md | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/Documentation.md diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/Documentation.md b/Product Configuration Enhancement/Product Configuration Enhancement/Documentation.md new file mode 100644 index 0000000..f221b45 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/Documentation.md @@ -0,0 +1,136 @@ +# Product Configuration Enhancement – End-User Documentation + +## Overview + +**Product Configuration Enhancement** is an add-on for PrintVis that lets you define +detailed, reusable **Job Item templates** directly on the Product Card. When a product is +applied to a Job, these templates are automatically copied onto the Job's Job Items, +saving time on repetitive manual setup for multi-component products (for example, a book +with a cover and inner pages, or an envelope with several sub-parts). + +It also exposes the PrintVis **Job User Fields** setup on the Product Card, so User Field +values can be prepared once on the product and automatically carried over to every Job +that uses it. + +## Where to find it + +Everything is added to the standard **PrintVis Product Card** page: + +- A new **Product Job Items** section (below the "Inks and Consumables" area). +- A **User Fields** action group (next to the "Additional Info" actions). + +No new menu items or role center entries are required — open any Product Card as usual. + +## Product Job Items + +The **Product Job Items** part on the Product Card lets you build one or more template +lines describing the components of the product (e.g. "Cover", "Text pages", "Insert"). +Each line is a full template for one Job Item, including size, paper, colors, tools, +finishing and more. + +### Fields on a Product Job Item line + +| Field | Description | +|---|---| +| Job Item No. | Sequential number identifying the line. Assigned automatically for new lines. | +| Component Type | Classifies how this job item is treated in production calculation (e.g. Cover, Text). | +| No. of Pages | Total number of pages for this component. Must be an even number. | +| Pages with Print | Number of pages that carry print. | +| Format Code | Optional standard format (paper/substrate/plate size) to apply. When set, it fills in Format1/Format2 automatically according to the general Format Entry setup. | +| Format1 / Format2 | Manual size values, used when no Format Code is selected. | +| Weight | Paper/substrate weight. | +| Imposition Code | Links a specific imposition type; also used to derive Format1/Format2 automatically for pages-on-sheet layouts. | +| Paper No. | The paper/substrate item. Selecting an item automatically pulls its weight (if defined). | +| Paper Description | Read-only description of the selected paper item. | +| Finishing | Finishing type to add to estimations for this component. | +| Colors Front / Colors Back | Number of print colors on the front and back side. | +| Tool 1–4 | Tools required when re-running this component. | +| Media Type | Optional media classification (e.g. Envelope, Envelope Plain, Envelope Window). | +| List of Units | The calculation unit (cost center/machine) this component is produced on. | +| Configuration | The Cost Center Configuration to use for the List of Units. Resolved automatically when only one configuration matches the component's size/weight; if several match, you are prompted to choose. | +| Envelope Window Shape Type / Size X / Size Y | Envelope window details, relevant only for envelope media types. | +| Opacity / Opacity Level | Opacity classification and level of the substrate. | + +### Working with Product Job Item lines + +- **New Job Item** – adds a blank template line to the product. Header values (format, + paper, colors, tools, etc.) already set on the Product itself are copied in as a + starting point. +- **Copy Job Item** – duplicates the currently selected line as a new line, useful when + several components are very similar. +- **Colors / Materials** – opens the ink/consumable lines (colors, coverage, washups, + plates, etc.) for the selected job item. You can also drill down directly from the + **Colors Front** or **Colors Back** fields to open the same list. +- Deleting a line removes that component's template; it does not affect Job Items already + created on existing Jobs. + +### Colors / Materials + +Each Product Job Item can have its own set of ink/consumable lines, matching the standard +PrintVis "Inks and Consumables" concept but scoped per component: + +| Field | Description | +|---|---| +| Item Quality Code | Quality of the ink/consumable. | +| Item No. | The ink/consumable item. Lookup is restricted to items matching the selected Item Quality Code, if any. | +| Text / Text 2 | Description(s), pulled from the item and editable. | +| F/B | Whether the color prints on the Front, Back, or Both sides. | +| Type | Base Color, Spot Color, or Other. | +| Coverage Pct. | Percentage of the sheet covered at 100% coverage. | +| Area Per Weight | Area covered per unit of ink at 100% coverage. | +| No. of Washups | Number of color changes/washups to count. | + +New lines automatically inherit the Product Code and Job Item No. of the component you +opened them from. + +## User Fields on the Product Card + +Three new actions, **User Fields 1**, **User Fields 2**, and **User Fields 3**, are added +next to the standard Additional Info actions. Each opens the combined user field editor +for the product, using the same field/option definitions configured for **Job User +Fields** in PrintVis General Setup. + +- Each action is only visible if the corresponding Job User Fields group (1, 2, or 3) is + enabled in **General Setup**. +- Shortcut keys: Shift+Ctrl+F1, Shift+Ctrl+F2, Shift+Ctrl+F3. +- Values entered here are stored against the product and are automatically copied to the + Job's user fields the next time this product is applied to a Job (see below). + +## What happens when a product is applied to a Job + +When you set/change the **Product Code** on a PVS Job, the following happens +automatically, without any extra steps: + +1. **Header fields** – if the product has a Format Code and/or Colors Front/Back set, + these are copied onto the Job header. +2. **User Fields** – any values entered via User Fields 1/2/3 on the product are copied + onto the Job's user fields. +3. **Job Items** – every Product Job Item line on the product is matched to a Job Item on + the Job (by Job Item No.): + - If a matching Job Item does not yet exist, a new sheet and Job Item are created. + - If it exists, its fields are updated wherever the template line has a value set + (component type, page counts, size, paper, colors, tools, media/envelope/opacity + settings, list of units/configuration). + - Fields left blank on the template are not overwritten, so manual adjustments already + made on the Job are preserved where the template doesn't specify a value. + - Any extra Job Items on the Job that don't correspond to a template line are left + untouched. + +This means the recommended workflow is: + +1. Build out the product's components once, on the Product Card, using **Product Job + Items**. +2. Set up User Fields on the product if applicable. +3. Apply the product to any Job as usual — sizes, paper, colors, tools and user fields for + every component are filled in automatically. + +## Tips + +- Set up **Format Code** or **Imposition Code** on a template line instead of manual + Format1/Format2 where possible, so sizes stay consistent if the underlying master data + changes. +- Use **Copy Job Item** to quickly create near-identical components (e.g. multiple insert + types) and then adjust only the differing fields. +- If **Configuration** shows blank after choosing a **List of Units**, either no + configuration matches the current size/weight, or more than one does — in the latter + case, drill down into **Configuration** to pick the correct one manually. From 6cbfe37227852948c625a2b2703d57abf663b70a Mon Sep 17 00:00:00 2001 From: Brandon Steele Date: Thu, 3 Sep 2026 13:14:41 -0400 Subject: [PATCH 4/5] UpdateBasedonUFTesting --- .../ProductConfig.permissionset.al | 1 - .../Extensions/PTEProductCardExt.PageExt.al | 24 +++- .../PTEProductSetupState.Codeunit.al | 125 ------------------ .../PTEProductTemplateMgt.Codeunit.al | 25 +++- 4 files changed, 40 insertions(+), 135 deletions(-) delete mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductSetupState.Codeunit.al diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al b/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al index 9e9cf00..cc693ab 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al @@ -3,7 +3,6 @@ permissionset 75000 ProductConfig Assignable = true; Permissions = tabledata "PTE Product Job Item" = RIMD, table "PTE Product Job Item" = X, - codeunit "PTE Product Setup State" = X, codeunit "PTE Product Template Mgt" = X, page "PTE Product Job Item Colors" = X, page "PTE Product Job Item Sub" = X; diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al index 8f9b239..61dbca7 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al @@ -32,15 +32,19 @@ pageextension 75006 "PTE Product Card Ext" extends "PVS Product Card" ApplicationArea = All; Caption = 'User Fields 1'; Image = PeriodStatus; + Promoted = true; + PromotedCategory = Process; ShortCutKey = 'Shift+Ctrl+F1'; ToolTip = 'User Fields (1) JOB'; Visible = PUSH_USERFIELDS_1Visible; trigger OnAction() var - ProductSetupState: Codeunit "PTE Product Setup State"; + UserFieldMgt: Codeunit "PVS Userfield Management"; + Code1: Code[20]; begin - ProductSetupState.Form_Userfield_Edit_Product(0, Rec.Code); + Code1 := CopyStr(Rec.Code, 1, MaxStrLen(Code1)); + UserFieldMgt.Form_Userfield_Edit(6010313, 0, '', Code1, 0, 0, 0, 0, 0); end; } action("PTE_PUSH_USERFIELDS_2") @@ -48,15 +52,19 @@ pageextension 75006 "PTE Product Card Ext" extends "PVS Product Card" ApplicationArea = All; Caption = 'User Fields 2'; Image = ReopenPeriod; + Promoted = true; + PromotedCategory = Process; ShortCutKey = 'Shift+Ctrl+F2'; ToolTip = 'User Fields (2) JOB'; Visible = PUSH_USERFIELDS_2Visible; trigger OnAction() var - ProductSetupState: Codeunit "PTE Product Setup State"; + UserFieldMgt: Codeunit "PVS Userfield Management"; + Code1: Code[20]; begin - ProductSetupState.Form_Userfield_Edit_Product(1, Rec.Code); + Code1 := CopyStr(Rec.Code, 1, MaxStrLen(Code1)); + UserFieldMgt.Form_Userfield_Edit(6010313, 1, '', Code1, 0, 0, 0, 0, 0); end; } action("PTE_PUSH_USERFIELDS_3") @@ -64,15 +72,19 @@ pageextension 75006 "PTE Product Card Ext" extends "PVS Product Card" ApplicationArea = All; Caption = 'User Fields 3'; Image = ClosePeriod; + Promoted = true; + PromotedCategory = Process; ShortCutKey = 'Shift+Ctrl+F3'; ToolTip = 'User Fields (3) JOB'; Visible = PUSH_USERFIELDS_3Visible; trigger OnAction() var - ProductSetupState: Codeunit "PTE Product Setup State"; + UserFieldMgt: Codeunit "PVS Userfield Management"; + Code1: Code[20]; begin - ProductSetupState.Form_Userfield_Edit_Product(2, Rec.Code); + Code1 := CopyStr(Rec.Code, 1, MaxStrLen(Code1)); + UserFieldMgt.Form_Userfield_Edit(6010313, 2, '', Code1, 0, 0, 0, 0, 0); end; } } diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductSetupState.Codeunit.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductSetupState.Codeunit.al deleted file mode 100644 index d1bc119..0000000 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductSetupState.Codeunit.al +++ /dev/null @@ -1,125 +0,0 @@ -/// -/// SingleInstance codeunit that holds transient UI state for the PVS Product Setup pages. -/// Used to communicate the currently selected Product Job Item No. from the job item -/// subpage to sibling subpages (e.g. the Item Colors Part) without requiring a round-trip -/// through the parent page record. -/// -codeunit 75003 "PTE Product Setup State" -{ - SingleInstance = true; - - var - CurrentJobItemNo: Integer; - CurrentProductCode: Code[20]; - - procedure SetCurrentJobItem(ProductCode: Code[20]; JobItemNo: Integer) - begin - CurrentProductCode := ProductCode; - CurrentJobItemNo := JobItemNo; - end; - - procedure GetCurrentJobItemNo(): Integer - begin - exit(CurrentJobItemNo); - end; - - procedure GetCurrentProductCode(): Code[20] - begin - exit(CurrentProductCode); - end; - - /// - /// Opens the combined User Fields page for a Product. - /// Field/option definitions are read from the Job User Field setup, while values are - /// stored under the Product table keyed by the Product Code. - /// - procedure Form_Userfield_Edit_Product(in_Subtype: Integer; in_ProductCode: Code[50]) - var - TempBufferRec: Record "PVS Userfield Field Buffer" temporary; - UserFieldMgt: Codeunit "PVS Userfield Management"; - DataCode: Code[20]; - begin - DataCode := CopyStr(in_ProductCode, 1, MaxStrLen(DataCode)); - if DataCode = '' then - exit; - - UserFieldMgt.Populate_Initial_Values(Database::"PVS Product", in_Subtype, DataCode, '', 0, 0, 0, 0, 0); - Commit(); - - TempBufferRec.SetRange("Table ID", Database::"PVS Product"); - TempBufferRec.SetRange("Table Subtype", in_Subtype); - TempBufferRec.SetRange("Table Code", DataCode); - TempBufferRec.SetRange(ID1, 0); - TempBufferRec.SetRange(ID2, 0); - TempBufferRec.SetRange(ID3, 0); - TempBufferRec.SetRange(ID4, 0); - TempBufferRec.SetRange(ID5, 0); - UserFieldMgt.Populate_Fields_Tmp_Buffer(TempBufferRec, true, 0, 0, 0, 0); - - Page.Run(Page::"PVS Userfields Combined", TempBufferRec); - end; - - /// - /// Copies the Product User Field values onto the Job User Fields when a Product is applied to a Job. - /// - procedure Transfer_Product_Userfields_To_Job(in_ProductCode: Code[50]; in_JobID: Integer; in_JobNo: Integer; in_Version: Integer) - var - FieldRec: Record "PVS Userfield Field"; - ProductValueRec: Record "PVS Userfield Field Value"; - JobValueRec: Record "PVS Userfield Field Value"; - ProductCode: Code[20]; - Subtype: Integer; - begin - ProductCode := CopyStr(in_ProductCode, 1, MaxStrLen(ProductCode)); - if ProductCode = '' then - exit; - if in_JobID = 0 then - exit; - - for Subtype := 0 to 2 do begin - FieldRec.Reset(); - FieldRec.SetRange("Table ID", Database::"PVS Job"); - FieldRec.SetRange("Table Subtype", Subtype); - FieldRec.SetRange("Table Code", ''); - if FieldRec.FindSet() then - repeat - ProductValueRec.Reset(); - ProductValueRec.SetRange("Table ID", Database::"PVS Product"); - ProductValueRec.SetRange("Table Subtype", Subtype); - ProductValueRec.SetRange("Table Code", ProductCode); - ProductValueRec.SetRange("Field No.", FieldRec."Field No."); - ProductValueRec.SetRange(ID1, 0); - ProductValueRec.SetRange(ID2, 0); - ProductValueRec.SetRange(ID3, 0); - ProductValueRec.SetRange(ID4, 0); - ProductValueRec.SetRange(ID5, 0); - ProductValueRec.SetRange(Code1, ''); - if ProductValueRec.FindFirst() then - if ProductValueRec.Text <> '' then - if not JobValueRec.Get(Database::"PVS Job", Subtype, '', FieldRec."Field No.", - in_JobID, in_JobNo, in_Version, 0, 0, '', 10000) - then begin - JobValueRec.Init(); - JobValueRec."Table ID" := Database::"PVS Job"; - JobValueRec."Table Subtype" := Subtype; - JobValueRec."Table Code" := ''; - JobValueRec."Field No." := FieldRec."Field No."; - JobValueRec.ID1 := in_JobID; - JobValueRec.ID2 := in_JobNo; - JobValueRec.ID3 := in_Version; - JobValueRec.ID4 := 0; - JobValueRec.ID5 := 0; - JobValueRec.Code1 := ''; - JobValueRec."Entry No." := 10000; - JobValueRec."Option Value Entry No." := ProductValueRec."Option Value Entry No."; - JobValueRec.Text := ProductValueRec.Text; - JobValueRec.Insert(true); - end else begin - JobValueRec."Option Value Entry No." := ProductValueRec."Option Value Entry No."; - JobValueRec.Text := ProductValueRec.Text; - JobValueRec.Modify(true); - end; - until FieldRec.Next() = 0; - end; - end; -} diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al index e45e334..62ac876 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al @@ -14,19 +14,38 @@ codeunit 75004 "PTE Product Template Mgt" /// [EventSubscriber(ObjectType::Codeunit, Codeunit::"PVS Product Management", OnAfterValidateProductPVSJob, '', false, false)] local procedure OnAfterValidateProductPVSJob(var Job: Record "PVS Job"; var xJob: Record "PVS Job") - var - ProductSetupState: Codeunit "PTE Product Setup State"; begin // 1. Copy product header fields (Format Code / Colors) to the job header. ApplyProductHeaderFields(Job); // 2. Transfer the product user field values to the job. - ProductSetupState.Transfer_Product_Userfields_To_Job(Job."Product Code", Job.ID, Job.Job, Job.Version); + ApplyProductUserFields(Job); // 3. Sync template lines to the job items. SyncTemplateLinesToJobItems(Job); end; + /// + /// Copies the userfield values assigned to the product into a job + /// + local procedure ApplyProductUserFields(var Job: Record "PVS Job") + var + UserfieldValueRec: Record "PVS Userfield Field Value"; + UserfieldMgt: Codeunit "PVS Userfield Management"; + UniqueTables: Dictionary of [Integer, Boolean]; + ProductCode: Code[20]; + begin + ProductCode := CopyStr(Job."Product Code", 1, MaxStrLen(ProductCode)); + UserfieldValueRec.SetRange(Code1, ProductCode); + if UserfieldValueRec.Findset() then + repeat + if not UniqueTables.ContainsKey(UserfieldValueRec."Table ID") then begin + UniqueTables.Add(UserfieldValueRec."Table ID", true); + UserfieldMgt.Copy_Record_UserFields(UserfieldValueRec."Table ID", '', ProductCode, 0, 0, 0, 0, 0, '', Job.ID, Job.Job, Job.Version, 0, 0, false); + end; + until UserfieldValueRec.Next() = 0; + end; + /// /// Copies the product header fields (Format Code, Colors Front, Colors Back) to the /// PVS Job when a product is applied. Formerly executed inside From cdf9aba3be207a3413abd44dd1ef17c8c571c455 Mon Sep 17 00:00:00 2001 From: Kasper Lind Date: Fri, 9 Oct 2026 15:58:00 +0200 Subject: [PATCH 5/5] update --- .../Documentation.md | 6 +- .../ProductConfig.permissionset.al | 3 + .../app.json | 2 +- .../Extensions/PTEProductCardExt.PageExt.al | 18 +- .../ProductSetup/PTEProductJobItem.Table.al | 2 +- .../PTEProductJobItemColors.Page.al | 4 + .../ProductSetup/PTEProductJobItemSub.Page.al | 6 +- .../PTEProductTemplateMgt.Codeunit.al | 244 +++++++++++++++++- .../PTEProductUserfieldID.Table.al | 91 +++++++ 9 files changed, 344 insertions(+), 32 deletions(-) create mode 100644 Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductUserfieldID.Table.al diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/Documentation.md b/Product Configuration Enhancement/Product Configuration Enhancement/Documentation.md index f221b45..996475e 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/Documentation.md +++ b/Product Configuration Enhancement/Product Configuration Enhancement/Documentation.md @@ -93,8 +93,10 @@ Fields** in PrintVis General Setup. - Each action is only visible if the corresponding Job User Fields group (1, 2, or 3) is enabled in **General Setup**. - Shortcut keys: Shift+Ctrl+F1, Shift+Ctrl+F2, Shift+Ctrl+F3. -- Values entered here are stored against the product and are automatically copied to the - Job's user fields the next time this product is applied to a Job (see below). +- Values entered here are stored against the product itself (tagged as table "PVS + Product", not as a Job), so they cannot be mistaken for real Job entries by other + Job-related logic. They are automatically copied to the Job's user fields the next + time this product is applied to a Job (see below). ## What happens when a product is applied to a Job diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al b/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al index cc693ab..36ff9ce 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/ProductConfig.permissionset.al @@ -2,7 +2,10 @@ permissionset 75000 ProductConfig { Assignable = true; Permissions = tabledata "PTE Product Job Item" = RIMD, + tabledata "PTE Product Userfield ID" = RIMD, + tabledata "PVS Userfield Field Value" = RIMD, table "PTE Product Job Item" = X, + table "PTE Product Userfield ID" = X, codeunit "PTE Product Template Mgt" = X, page "PTE Product Job Item Colors" = X, page "PTE Product Job Item Sub" = X; diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/app.json b/Product Configuration Enhancement/Product Configuration Enhancement/app.json index 0be1a44..141af43 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/app.json +++ b/Product Configuration Enhancement/Product Configuration Enhancement/app.json @@ -2,7 +2,7 @@ "id": "8d206bcf-bc23-4041-9c54-ccfeef79998d", "name": "Product Configuration Enhancement", "publisher": "PrintVis A/S", - "version": "1.0.0.0", + "version": "1.0.0.2", "brief": "Product Job Item templates and user field setup for PrintVis products.", "description": "Adds Product Job Item template lines to the PrintVis Product Card and syncs them to Job Items when a product is applied to a case. Also surfaces the PrintVis Job user fields on the Product Card.", "privacyStatement": "http://www.printvis.com", diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al index 61dbca7..0d0d7e4 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/Extensions/PTEProductCardExt.PageExt.al @@ -40,11 +40,9 @@ pageextension 75006 "PTE Product Card Ext" extends "PVS Product Card" trigger OnAction() var - UserFieldMgt: Codeunit "PVS Userfield Management"; - Code1: Code[20]; + ProductTemplateMgt: Codeunit "PTE Product Template Mgt"; begin - Code1 := CopyStr(Rec.Code, 1, MaxStrLen(Code1)); - UserFieldMgt.Form_Userfield_Edit(6010313, 0, '', Code1, 0, 0, 0, 0, 0); + ProductTemplateMgt.EditProductUserFields(Rec.Code, 0); end; } action("PTE_PUSH_USERFIELDS_2") @@ -60,11 +58,9 @@ pageextension 75006 "PTE Product Card Ext" extends "PVS Product Card" trigger OnAction() var - UserFieldMgt: Codeunit "PVS Userfield Management"; - Code1: Code[20]; + ProductTemplateMgt: Codeunit "PTE Product Template Mgt"; begin - Code1 := CopyStr(Rec.Code, 1, MaxStrLen(Code1)); - UserFieldMgt.Form_Userfield_Edit(6010313, 1, '', Code1, 0, 0, 0, 0, 0); + ProductTemplateMgt.EditProductUserFields(Rec.Code, 1); end; } action("PTE_PUSH_USERFIELDS_3") @@ -80,11 +76,9 @@ pageextension 75006 "PTE Product Card Ext" extends "PVS Product Card" trigger OnAction() var - UserFieldMgt: Codeunit "PVS Userfield Management"; - Code1: Code[20]; + ProductTemplateMgt: Codeunit "PTE Product Template Mgt"; begin - Code1 := CopyStr(Rec.Code, 1, MaxStrLen(Code1)); - UserFieldMgt.Form_Userfield_Edit(6010313, 2, '', Code1, 0, 0, 0, 0, 0); + ProductTemplateMgt.EditProductUserFields(Rec.Code, 2); end; } } diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al index 2bc008d..606a1bd 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItem.Table.al @@ -4,7 +4,7 @@ table 75000 "PTE Product Job Item" fields { - field(1; "Product Code"; Code[20]) + field(1; "Product Code"; Code[50]) { Caption = 'Product Code'; DataClassification = SystemMetadata; diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemColors.Page.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemColors.Page.al index 4b3be60..247b4a9 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemColors.Page.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemColors.Page.al @@ -120,6 +120,10 @@ page 75002 "PTE Product Job Item Colors" begin // Inherit the filtered Product Code and Job Item No. so every new line // is automatically linked to the correct product and job item. + // Rec ("PVS Product Materials", base app) caps Product Code at 20 characters - + // shorter than "PTE Product Job Item"/"PVS Product".Code (Code[50]). This is an + // existing base-app limitation of the Colors/Materials feature, not something this + // extension can widen, so the value is truncated to fit here (same as before). FilterProductCode := CopyStr(Rec.GetFilter("Product Code"), 1, MaxStrLen(Rec."Product Code")); Evaluate(FilterJobItemNo, Rec.GetFilter("Job Item No.")); diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al index 48a22d1..1822a72 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductJobItemSub.Page.al @@ -223,11 +223,9 @@ page 75001 "PTE Product Job Item Sub" trigger OnAction() var NewProductJobItem: Record "PTE Product Job Item"; + ProductTemplateMgt: Codeunit "PTE Product Template Mgt"; begin - Rec.TestField("Product Code"); - NewProductJobItem := Rec; - NewProductJobItem."Job Item No." := 0; - NewProductJobItem.Insert(true); + ProductTemplateMgt.CopyProductJobItem(Rec, NewProductJobItem); CurrPage.Update(false); end; } diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al index 62ac876..30566c9 100644 --- a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductTemplateMgt.Codeunit.al @@ -26,24 +26,201 @@ codeunit 75004 "PTE Product Template Mgt" end; /// - /// Copies the userfield values assigned to the product into a job + /// Deletes a product's stored default User Field values (table "PTE Product Userfield + /// ID") when the product itself is deleted, so no orphaned default value rows are left + /// behind pointing at a Product Code that no longer exists. + /// + [EventSubscriber(ObjectType::Table, Database::"PVS Product", OnAfterDeleteEvent, '', false, false)] + local procedure OnAfterDeleteProduct(var Rec: Record "PVS Product"; RunTrigger: Boolean) + var + ProductUserfieldValue: Record "PTE Product Userfield ID"; + ProductJobItem: Record "PTE Product Job Item"; + begin + if Rec.IsTemporary() then + exit; + + ProductUserfieldValue.SetRange("Product Code", Rec.Code); + ProductUserfieldValue.DeleteAll(false); + + // Remove the product's job item template lines (with trigger, so dependent + // extensions can clean up data linked to each template line). + ProductJobItem.SetRange("Product Code", Rec.Code); + if not ProductJobItem.IsEmpty() then + ProductJobItem.DeleteAll(true); + end; + + /// + /// Creates a copy of a product job item template line on the same product with the next + /// free Job Item No. Raises OnAfterCopyProductJobItem so dependent extensions can copy + /// their own data linked to the template line. + /// + procedure CopyProductJobItem(SourceProductJobItem: Record "PTE Product Job Item"; var NewProductJobItem: Record "PTE Product Job Item") + begin + SourceProductJobItem.TestField("Product Code"); + NewProductJobItem := SourceProductJobItem; + NewProductJobItem."Job Item No." := 0; + NewProductJobItem.Insert(true); + OnAfterCopyProductJobItem(SourceProductJobItem, NewProductJobItem); + end; + + /// + /// Copies the default userfield values stored for the product (table "PTE Product + /// Userfield ID", see ) into a job. "PVS Userfield + /// Management".Copy_Record_UserFields only copies between two identities within the + /// SAME "PVS Userfield Field Value" table, so the product's own defaults (which live in + /// our own table, not in "PVS Userfield Field Value") are first materialized there as + /// temporary rows tagged Table ID = "PVS Job", ID1 = SessionId() (see + /// ), then copied from that identity to the + /// real job's identity, then the temporary rows are removed again. SessionId() is + /// already guaranteed unique for the duration of this call - no reserved/looked-up + /// surrogate ID is needed. /// local procedure ApplyProductUserFields(var Job: Record "PVS Job") var - UserfieldValueRec: Record "PVS Userfield Field Value"; + ProductUserfieldID: Record "PTE Product Userfield ID"; UserfieldMgt: Codeunit "PVS Userfield Management"; - UniqueTables: Dictionary of [Integer, Boolean]; - ProductCode: Code[20]; + StagingId: Integer; begin - ProductCode := CopyStr(Job."Product Code", 1, MaxStrLen(ProductCode)); - UserfieldValueRec.SetRange(Code1, ProductCode); - if UserfieldValueRec.Findset() then + if Job."Product Code" = '' then + exit; + + // No default User Field values have ever been saved for this product - nothing to + // transfer. + if not ProductUserfieldID.HasValues(Job."Product Code") then + exit; + + StagingId := SessionId(); + StageProductUserFieldsAsJob(StagingId, Job."Product Code"); + UserfieldMgt.Copy_Record_UserFields(Database::"PVS Job", '', '', StagingId, 0, 0, 0, 0, '', Job.ID, Job.Job, Job.Version, 0, 0, false); + DeleteStagedJobUserFields(StagingId); + end; + + /// + /// Opens the Userfield edit form for a Product using the Job User Field 1/2/3 group + /// setup/labels (table 6010313, "PVS Job"). "PVS Userfield Management".Form_Userfield_Edit + /// only works against "PVS Userfield Field Value" (table 6010313), so the product's + /// existing default values (stored in our own table "PTE Product Userfield ID") are first + /// materialized there as temporary rows tagged Table ID = "PVS Job" before the edit form + /// is opened; afterwards, whatever the form left staged is saved back into "PTE Product + /// Userfield ID" as the product's new default values, and the temporary rows are removed. + /// InputProductCode is the full Product Code (Code[50], matching "PVS Product".Code). It + /// is never truncated or length-checked: staging/committing is keyed by SessionId(), not + /// by the Product Code string itself, so no reserved/looked-up surrogate ID is needed. + /// + procedure EditProductUserFields(InputProductCode: Code[50]; GroupIndex: Integer) + var + UserFieldMgt: Codeunit "PVS Userfield Management"; + StagingId: Integer; + begin + StagingId := SessionId(); + StageProductUserFieldsAsJob(StagingId, InputProductCode); + UserFieldMgt.Form_Userfield_Edit(Database::"PVS Job", GroupIndex, '', '', StagingId, 0, 0, 0, 0); + CommitStagedJobUserFieldsToProduct(StagingId, InputProductCode); + end; + + /// + /// Materializes the product's stored default values (table "PTE Product Userfield ID") + /// as rows in "PVS Userfield Field Value" tagged Table ID = "PVS Job", ID1 = StagingId, + /// so base app procedures that only operate on "PVS Userfield Field Value" (Form_Userfield_ + /// Edit, Copy_Record_UserFields) can read them as their source/current values. ID1 (an + /// Integer) is used instead of Code1 (Code[20]) so the full Product Code (Code[50]) + /// never needs to be truncated or length-checked. Each staged row's "Table Subtype" is set + /// from the stored "Group Index" so that Form_Userfield_Edit (which filters by subtype per + /// User Field group) only ever operates on the single group being edited, while the other + /// two groups' defaults stay staged untouched alongside it. + /// + local procedure StageProductUserFieldsAsJob(StagingId: Integer; ProductCode: Code[50]) + var + ProductUserfieldValue: Record "PTE Product Userfield ID"; + begin + ProductUserfieldValue.SetRange("Product Code", ProductCode); + if ProductUserfieldValue.FindSet() then repeat - if not UniqueTables.ContainsKey(UserfieldValueRec."Table ID") then begin - UniqueTables.Add(UserfieldValueRec."Table ID", true); - UserfieldMgt.Copy_Record_UserFields(UserfieldValueRec."Table ID", '', ProductCode, 0, 0, 0, 0, 0, '', Job.ID, Job.Job, Job.Version, 0, 0, false); - end; - until UserfieldValueRec.Next() = 0; + InsertOrUpdateStagedValue(StagingId, ProductUserfieldValue."Group Index", ProductUserfieldValue."Field No.", ProductUserfieldValue."Value Entry No.", ProductUserfieldValue.Text); + until ProductUserfieldValue.Next() = 0; + end; + + /// + /// Inserts or updates a single staged value row in "PVS Userfield Field Value", tagged + /// Table ID = "PVS Job", Table Subtype = GroupIndex, ID1 = StagingId - the transient + /// identity used to materialize a product's defaults there. See + /// . + /// + local procedure InsertOrUpdateStagedValue(StagingId: Integer; GroupIndex: Integer; FieldNo: Integer; ValueEntryNo: Integer; Value: Text[250]) + var + StagedRec: Record "PVS Userfield Field Value"; + begin + StagedRec.SetRange("Table ID", Database::"PVS Job"); + StagedRec.SetRange("Table Subtype", GroupIndex); + StagedRec.SetRange(ID1, StagingId); + StagedRec.SetRange("Field No.", FieldNo); + StagedRec.SetRange("Entry No.", ValueEntryNo); + if StagedRec.FindFirst() then begin + StagedRec.Text := Value; + StagedRec.Modify(false); + end else begin + StagedRec.Init(); + StagedRec."Table ID" := Database::"PVS Job"; + StagedRec."Table Subtype" := GroupIndex; + StagedRec.ID1 := StagingId; + StagedRec."Field No." := FieldNo; + StagedRec."Entry No." := ValueEntryNo; + StagedRec.Text := Value; + StagedRec.Insert(false); + end; + end; + + /// + /// Removes the temporary "PVS Userfield Field Value" rows tagged Table ID = "PVS Job", + /// ID1 = StagingId, once their content has been safely copied elsewhere (see + /// and ). + /// Must only be called AFTER the data has been copied out - never before, or the + /// rows would be lost with nothing to restore them. + /// + local procedure DeleteStagedJobUserFields(StagingId: Integer) + var + JobRec: Record "PVS Userfield Field Value"; + begin + JobRec.SetRange("Table ID", Database::"PVS Job"); + JobRec.SetRange(ID1, StagingId); + JobRec.DeleteAll(false); + end; + + /// + /// Saves the staged "PVS Userfield Field Value" rows (Table ID = "PVS Job", ID1 = + /// StagingId) - i.e. whatever Form_Userfield_Edit just added or changed - as the + /// product's new default values in "PTE Product Userfield ID", then removes the staged + /// rows. This is a full replace (existing default value rows for the product are deleted + /// first) rather than a pure upsert, so a field that was cleared during editing does not + /// remain behind as a stale default. + /// + local procedure CommitStagedJobUserFieldsToProduct(StagingId: Integer; ProductCode: Code[50]) + var + StagedRec: Record "PVS Userfield Field Value"; + ProductUserfieldValue: Record "PTE Product Userfield ID"; + begin + ProductUserfieldValue.SetRange("Product Code", ProductCode); + ProductUserfieldValue.DeleteAll(false); + + StagedRec.SetRange("Table ID", Database::"PVS Job"); + StagedRec.SetRange(ID1, StagingId); + if StagedRec.FindSet() then + repeat + ProductUserfieldValue.Init(); + ProductUserfieldValue."Product Code" := ProductCode; + ProductUserfieldValue."Group Index" := StagedRec."Table Subtype"; + ProductUserfieldValue."Field No." := StagedRec."Field No."; + ProductUserfieldValue."Value Entry No." := StagedRec."Entry No."; + ProductUserfieldValue.Text := StagedRec.Text; + // RunTrigger must be true here: "Entry No." is deliberately left unassigned + // above and is only ever set by the table's OnInsert trigger (FindLast()+1). + // Insert(false) would skip that trigger, leaving every row at "Entry No." = 0 + // and causing a "record already exists" error on the second row inserted for + // the same product. + ProductUserfieldValue.Insert(true); + until StagedRec.Next() = 0; + + DeleteStagedJobUserFields(StagingId); end; /// @@ -98,6 +275,10 @@ codeunit 75004 "PTE Product Template Mgt" SheetChanged: Boolean; PrevGUINotAllowed: Boolean; begin + // Raised before the blank-product exit so subscribers can clean up data from a + // previously applied product (also when the product is cleared). + OnBeforeSyncTemplateLinesToJobItems(Job); + if Job."Product Code" = '' then exit; @@ -276,8 +457,15 @@ codeunit 75004 "PTE Product Template Mgt" JobSheet.Modify(true); end; + // --- 21. Extension point --- + // Raised while GUI is still suppressed, with both the template line and the + // PVS Job Item it was applied to (Job Item No. may differ from the template). + OnAfterSyncTemplateLineToJobItem(TmplLine, JobItem, Job); + until TmplLine.Next() = 0; + OnAfterSyncTemplateLinesToJobItems(Job); + SingleInstance.Set_GUINOTALLOWED(PrevGUINotAllowed); // Refresh the Job record from the database. SheetMgt.Event_Create_Sheet and @@ -357,4 +545,36 @@ codeunit 75004 "PTE Product Template Mgt" local procedure OnAfterApplyProductHeaderFields(var Job: Record "PVS Job") begin end; + + /// + /// Raised before the product template lines are synced to the job items, also when the + /// job has no product (product cleared). + /// + [IntegrationEvent(false, false)] + local procedure OnBeforeSyncTemplateLinesToJobItems(var Job: Record "PVS Job") + begin + end; + + /// + /// Raised after a template line has been applied to its PVS Job Item (incl. List Of Units + /// and sheet fields). GUI is suppressed while this event runs. + /// + [IntegrationEvent(false, false)] + local procedure OnAfterSyncTemplateLineToJobItem(TemplateLine: Record "PTE Product Job Item"; var JobItem: Record "PVS Job Item"; var Job: Record "PVS Job") + begin + end; + + /// + /// Raised after all template lines have been synced, before the Job record is refreshed. + /// GUI is suppressed while this event runs. + /// + [IntegrationEvent(false, false)] + local procedure OnAfterSyncTemplateLinesToJobItems(var Job: Record "PVS Job") + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterCopyProductJobItem(SourceProductJobItem: Record "PTE Product Job Item"; var NewProductJobItem: Record "PTE Product Job Item") + begin + end; } diff --git a/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductUserfieldID.Table.al b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductUserfieldID.Table.al new file mode 100644 index 0000000..db5be23 --- /dev/null +++ b/Product Configuration Enhancement/Product Configuration Enhancement/src/ProductSetup/PTEProductUserfieldID.Table.al @@ -0,0 +1,91 @@ +table 75003 "PTE Product Userfield ID" +{ + Caption = 'Product Userfield Value'; + DataClassification = CustomerContent; + + fields + { + field(1; "Entry No."; Integer) + { + Caption = 'Entry No.'; + DataClassification = SystemMetadata; + ToolTip = 'Specifies the internal identifier of this stored default User Field value line.'; + } + field(2; "Product Code"; Code[50]) + { + Caption = 'Product Code'; + DataClassification = CustomerContent; + NotBlank = true; + TableRelation = "PVS Product".Code; + ToolTip = 'Specifies the product this default User Field value belongs to.'; + } + field(3; "Group Index"; Integer) + { + Caption = 'User Field Group'; + DataClassification = CustomerContent; + ToolTip = 'Specifies which of the three independent User Field groups (User Fields 1/2/3 on the Product Card, GroupIndex 0/1/2, mirrors "PVS Userfield Field Value"."Table Subtype") this default value belongs to. Field numbers are only unique within a single group, so this must be part of the unique key alongside "Field No." - otherwise two groups that both happen to use the same field number would collide.'; + } + field(4; "Field No."; Integer) + { + Caption = 'Field No.'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the User Field number (as defined in the PrintVis Job User Field 1/2/3 setup) this default value is for.'; + } + field(5; "Value Entry No."; Integer) + { + Caption = 'Value Entry No.'; + DataClassification = CustomerContent; + BlankZero = true; + ToolTip = 'Specifies the entry number of this value within the User Field (mirrors "PVS Userfield Field Value"."Entry No.", allowing more than one value to be stored for fields that support multiple entries).'; + } + field(6; "Text"; Text[250]) + { + Caption = 'Text'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the default value stored for this User Field, copied to the Job''s User Fields when this product is applied to a Job.'; + } + } + + keys + { + key(Key1; "Entry No.","Product Code","Field No.","Value Entry No.") + { + Clustered = true; + } + } + + /// + /// Assigns "Entry No." as the current highest value + 1 (mirrors the pattern used by + /// "PTE Product Job Item"."Job Item No."). Deliberately NOT using the built-in + /// AutoIncrement property: Business Central computes AutoIncrement values as + /// MAX(existing) + 1 at insert time rather than reserving a true monotonic sequence, so + /// a DeleteAll immediately followed by several Inserts in the same transaction (see + /// "PTE Product Template Mgt".CommitStagedJobUserFieldsToProduct) can hand out a value + /// that collides with a row not yet fully cleared, raising a "record already exists" + /// error. Explicitly computing the next value here avoids that failure mode. + /// + trigger OnInsert() + var + ProductUserfieldValue: Record "PTE Product Userfield ID"; + begin + if "Entry No." = 0 then begin + ProductUserfieldValue.SetCurrentKey("Entry No."); + if ProductUserfieldValue.FindLast() then + "Entry No." := ProductUserfieldValue."Entry No." + 1 + else + "Entry No." := 1; + end; + end; + + /// + /// Returns true if any default User Field values have already been saved for ProductCode. + /// Used to skip the transfer to a Job entirely when a product has none. + /// + procedure HasValues(ProductCode: Code[50]): Boolean + var + ProductUserfieldID: Record "PTE Product Userfield ID"; + begin + ProductUserfieldID.SetRange("Product Code", ProductCode); + exit(not ProductUserfieldID.IsEmpty()); + end; +}