Overview

The abstract part layer now follows the new core Ind. Tag model. The calculation Ind. Tag column can split rows by actual user-entered tag text, only treats true user overrides as adjusted values, and applies row-level Ind. Tag adjustments to each underlying part. A post-merge hook also repairs pre-17.0 adjustment keys when older drawings are loaded.

MR: Ind. Tag migration and restoration changes

ProdPart has also been expanded with first-class support for option additions, option import, and option-level customization state. Those changes provide the part foundation for the new query import workflow in cm.abstract.part.query, where imported option items can be dragged into the query dialog and applied as additions or special overrides.

CET 17.0 also changes where option specials live at runtime. Before 17.0, both PartSpecial and OptionSpecial data were stored together in the snapper-level PartSpecialHolder under cSpecialHolderKey, and option-special lookup depended on a combined part-and-option key. In 17.0, option specials move into a separate per-part OptionSpecialHolder, stored on the owner under a key built from optionSpecialHolderPropKey(part) and the part's sourceId(). That split is what makes stable option identity, inserted-option ordering, and per-part option-special migration possible.

Because older drawings still load old option specials from PartSpecialHolder, packages that need backward compatibility must opt in to the new migration helpers such as ProdPartSpecialMigrator and DsPartSpecialMigrator during their load/migration flow.

Compile Time Changes

class ProdPart

ProdPart now distinguishes between base part options and all part options, where allPartOptions() includes custom inserted options. The old spec-option invalidation flow has been broadened accordingly.

Added: extend public PartOption[] allPartOptions()
Added: extend public void setPartOptions(PartOption[] options, bool invalidatePartOptions=true)
Added: extend public PartOption[] rebuildPartOptions()
Added: extend public PartOption[] insertCustomOptions(PartOption[] options)

Old: extend public void invalidateSpecOptions(bool invalidateOptionPriceSum=true)
New: extend public void invalidatePartOptions(bool invalidateOptionPriceSum=true)

If you extend ProdPart and cache or mutate options, update your code to invalidate part options through invalidatePartOptions(...) and to choose between options() and allPartOptions() appropriately.

class ProdPart custom option ordering interfaces

ProdPart now exposes explicit interfaces for tracking custom inserted options by anchor key. These interfaces are important if you build, reorder, export, or inspect additional options.

Added: public str->CustomSpecOption anchorKeyToCustom
Added: extend public CustomSpecOption[] customSpecOptions(PropObj s=null)
Added: extend public str->CustomOptionSpecial getCustomOptionSpecials(PropObj s=null)
Added: extend public void putFollowingCustom(str anchorKey, CustomSpecOption custom)
Added: extend public CustomSpecOption getFollowingCustom(str anchorKey)
Added: extend public CustomSpecOption getFollowingCustom(PartOptionItem anchor)

class PartOption

PartOption is part of the 17.0 migration surface because inserted custom options now need a public insertion hook at the option-branch level.

Added: extend public void insertPartOptionItem(int index, PartOptionItem newOption)

If you implement a custom PartOption, add support for insertPartOptionItem(...) so ProdPart.insertCustomOptions(...) and related added-option flows can splice inserted options into the correct branch.

class ProdPartOption

ProdPartOption is the standard product-part implementation of the new PartOption insertion interface.

Added: public void insertPartOptionItem(int index, PartOptionItem newOption)

The default 17.0 product-part option flow now relies on ProdPartOption.insertPartOptionItem(...) when rebuilding allPartOptions() with inserted custom options.

class PartOptionItem

PartOptionItem is now an important part of the public migration surface because 17.0 uses option identity more broadly across query import, option-special lookup, and additional-option ordering.

Added/important usage: extend public str key()
Added/important usage: extend public void setKey(str value)
Added/important usage: extend public str generateKey()
Added/important usage: extend public double upcharge(Part owner=null, Space space=null, bool translatePrice=false)
Added/important usage: extend public int sequence()
Added/important usage: extend public str getUserDefined(str type)
Added/important usage: extend public void setUserDefined(str type, str contents)

Implementers should treat key() as the stable option identifier used by query rows, option-special storage, and imported/additional-option workflows.

class SpecOption

SpecOption is now more directly part of the migration story because 17.0 relies on its stable key for option identity.

Added/important usage: public str uniqueKey
Added/important usage: public str key()
Added/important usage: public void setKey(str value)
Added/important usage: public str groupCode
Added/important usage: public str groupCode()
Added/important usage: extend public str adjustmentKey()
Added/important usage: public str specialsKey()

Old: public constructor(str code, str description, str groupDescription=null,
                        int level=1, bool exportable=true)
New: public constructor(str code, str description, str groupCode=null, str groupDescription=null,
                        int level=1, bool exportable=true)

Old: public constructor(str code, str description, str groupDescription=null,
                        int level=1, double price=0.0, bool exportable=true)
New: public constructor(str code, str description, str groupCode=null, str groupDescription=null,
                        int level=1, double price=0.0, bool exportable=true)

The practical effect is that option-special and query-row identity now follows SpecOption.key() rather than being built from the owning part special key. adjustmentKey() remains the adjustment-facing identity, while specialsKey() now follows the stable option key. groupCode is now first-class option data and is carried through option creation and import flows, rather than existing only as loosely attached metadata.

For custom option rows, this also replaces the older CustomSpecOption key-storage model. The old dedicated _specialsKey field on CustomSpecOption is no longer the source of truth; instead, the special's specialsKey() now maps to the generated option item's PartOptionItem.key() / SpecOption.uniqueKey.

class OptionSpecial

OptionSpecial now exposes setter interfaces for feature metadata used by the new option export and added-option flows.

Removed: public Part originalPart
Removed: public str originalOptStr
Removed: extend public Part originalPart=(Part value)

Added/important usage: extend public str featureCode=(str value)
Added/important usage: extend public str featureDescription=(str value)

class CustomOptionSpecial

CustomOptionSpecial now exposes explicit anchor and feature-metadata interfaces for 17.0 added-option ordering and export.

Added/important usage: public str anchorOptKey
Added/important usage: extend public str anchorOptKey=(str value)
Added/important usage: public str featureCode=(str value)
Added/important usage: public str featureDescription=(str value)

anchorOptKey identifies which normal option or added option a custom option follows. The feature setters are used when custom options are exported back out as XML, OFDA, PMX, or SIF option data.

class OptionSpecialHolder

Option specials are no longer expected to live in PartSpecialHolder.specials() for 17.0-created data. OptionSpecialHolder is the new public holder type for per-part option-special state.

Added: public class OptionSpecialHolder
Added: extend public bool any()
Added: extend public bool empty()
Added: extend public void clear()
Added: extend public void put(str key, OptionSpecial special)
Added: extend public OptionSpecial get(str key)
Added: extend public void remove(str key)
Added: extend public void removeOptionSpecials()
Added: extend public bool anySpecials()
Added: extend public bool emptySpecials()
Added: extend public void putAddition(CustomOptionSpecial custom)
Added: extend public CustomOptionSpecial getAddition(str key)
Added: extend public void removeAddition(str key)
Added: extend public void removeAdditions()
Added: extend public bool anyAdditions()
Added: extend public bool emptyAdditions()
Added: extend public CustomSpecOption[] getAdditionalOptions()

Option-special holder functions

The holder is managed through a new shared function surface in cm.abstract.part.optionSpecialHolderFunctions.cm. These names are useful search targets if your code still assumes all specials live under cSpecialHolderKey.

Added: public const str cOptionSpecialHolder = "optionSpecialHolder";
Added: public str optionSpecialHolderPropKey(Part part)
Added: public OptionSpecialHolder initOptionSpecialHolder(PropObj s, Part part)
Added: public OptionSpecialHolder getOptionSpecialHolder(PropObj s, Part part)
Added: public void removeOptionSpecialHolder(PropObj s, Part part, bool invalidateWorldPrice)
Added: public void putOptionSpecial(PropObj s, Part part, str key, OptionSpecial special, bool invalidateWorldPrice)
Added: public OptionSpecial getOptionSpecial(PropObj s, Part part, str key)
Added: public void removeOptionSpecial(PropObj s, Part part, str key, bool invalidateWorldPrice)
Added: public void removeAllOptionSpecials(PropObj s, Part part, bool invalidateWorldPrice)
Added: public void putAdditionalOption(PropObj s, Part part, CustomOptionSpecial special, bool invalidateWorldPrice)
Added: public CustomOptionSpecial getAdditionalOptionSpecial(PropObj s, Part part, str key)
Added: public CustomOptionSpecial getAdditionalOptionSpecial(PropObj s, Part part, PartOptionItem opt)
Added: public void removeAdditionalOption(PropObj s, Part part, str key, bool invalidateWorldPrice)
Added: public void removeAdditionalOption(PropObj s, Part part, PartOptionItem opt, bool invalidateWorldPrice)
Added: public void removeAdditionalOptions(PropObj s, Part part, bool invalidateWorldPrice)

class ProdPart option-special and addition APIs

ProdPart now has a larger public API for option specials, additional options, and option customization state. These APIs are used by the new product-part query import workflow.

Added: extend public OptionSpecialHolder initOptionSpecialHolder()
Added: extend public OptionSpecialHolder optionSpecialHolder(PropObj s=null)

Added: extend public OptionSpecial getOptSpecial(str key, PropObj s=null)

Old: extend public void putOptSpecial(str key, OptionSpecial special, PropObj s=null, bool invalidateWorldPrice=true)
New: extend public void putOptSpecial(str key, OptionSpecial special, bool invalidateWorldPrice, PropObj s=null)

Old: extend public void putOptSpecial(PartOptionItem opt, OptionSpecial special, PropObj s=null, bool invalidateWorldPrice=true)
New: extend public void putOptSpecial(PartOptionItem opt, OptionSpecial special, bool invalidateWorldPrice, PropObj s=null)

Old: extend public void removeOptSpecial(str optSpecialKey, PropObj s=null, bool invalidateWorldPrice=true)
New: extend public void removeOptSpecial(str optSpecialKey, bool invalidateWorldPrice, PropObj s=null)

Old: extend public void removeOptSpecial(PartOptionItem opt, PropObj s=null, bool invalidateWorldPrice=true)
New: extend public void removeOptSpecial(PartOptionItem opt, bool invalidateWorldPrice, PropObj s=null)

Old: extend public void removeOptSpecials(PropObj s=null, bool invalidateWorldPrice=true)
New: extend public void removeOptSpecials(bool invalidateWorldPrice, PropObj s=null)

Added: extend public bool isAdditionalOption(PartOptionItem opt, PropObj s=null)
Added: extend public void putAdditionalOption(CustomOptionSpecial special, bool invalidateWorldPrice, PropObj s=null)
Added: extend public CustomOptionSpecial getAdditionalOptionSpecial(str key, PropObj s=null)
Added: extend public CustomOptionSpecial getAdditionalOptionSpecial(PartOptionItem opt, PropObj s=null)
Added: extend public void removeAdditionalOption(str key, bool invalidateWorldPrice, PropObj s=null)
Added: extend public void removeAdditionalOption(PartOptionItem opt, bool invalidateWorldPrice, PropObj s=null)
Added: extend public void removeAdditionalOptions(bool invalidateWorldPrice, PropObj s=null)
Added: extend public customizationType customizationStatus(PartOptionItem option, PropObj s=null)
Added: extend public symbol{} customizationStates(PartOptionItem option, PropObj s=null)

class ProdPartSpecialMigrator

ProdPartSpecialMigrator is the new public migration helper for moving pre-17.0 product-part option specials out of PartSpecialHolder and into the new per-part holder structure.

Added: public class ProdPartSpecialMigrator extends PartSpecialMigrator
Added: public void attemptMigrateSpecials(Part part, PropObj s)
Added: extend public void attemptMigrateOptionSpecials(Part part, PropObj s)
Added: extend public void attemptMigrateCustomOptionsSpecials(Part part, PropObj s)
Added: extend public str oldOptSpecialsKey(Part part, PartOptionItem opt)
Added: extend public str oldOptSpecialsKey(PartOptionItem opt)

Override oldOptSpecialsKey(...) if your package previously saved option specials with package-specific keys.

class ProdPartCreator

ProdPartCreator is the product-part-specific creator base used for 17.0 additions and overrides. It extends PartCreator, generates a ProdPart, replays options through processOptions(...), and includes option codes in the generated flattenable key.

Added: public class ProdPartCreator extends PartCreator
Added: public PartOption[] options
Added: public constructor(PartData data, PartOption[] options=null)
Added: public Part generatePart(PropObj owner=null)
Added: public str generateFlattenableKey(Part part)
Added: extend public void processOptions(ProdPart part)

class ProdPartAddition

ProdPartAddition is the new creator type for additional product parts. Instead of applying its PartOption[] directly to the generated part, it converts them into CustomOptionSpecials and stores them as additional options in OptionSpecialHolder.

Added: public class ProdPartAddition extends ProdPartCreator
Added: public void processOptions(ProdPart part)
Added: extend public CustomOptionSpecial createCustomOptionSpecial(Part part, PartOptionItem opt)

class ProdPartOverride

ProdPartOverride is the new creator type for product-part overrides. It generates a ProdPart and applies the stored PartOption[] directly with setPartOptions(...).

Added: public class ProdPartOverride extends ProdPartCreator
Added: public void processOptions(ProdPart part)

These types participate in the core Snapper.fetchParts() customization flow through the owner-level PartCreator APIs documented in cm.core.part.

class ProdPart changed signatures and moved flows

Several existing ProdPart interfaces changed shape in 17.0.

Old: public Double upcharge()
New: public Double upcharge(bool includeChildren=false, Space space=null, bool translatePrice=true, bool invalidate=false)

Old: extend public CustomSpecOption[] customSpecOptions()
New: extend public CustomSpecOption[] customSpecOptions(PropObj s=null)

Old: public bool containsAnySpecials(PropObj s=null)
New: public bool containsAnySpecials(bool ignoreStandardSpecial=false, PropObj s=null)

Old: public Double upcharge()
New: public Double upcharge(bool includeChildren=false, Space space=null, bool translatePrice=true, bool invalidate=false)

Old: extend public CustomSpecOption[] customSpecOptions()
New: extend public CustomSpecOption[] customSpecOptions(PropObj s=null)

Old: extend public void initializePartFromItemData(ItemData item, ProjectInformation projectInfo)
New: public void initializePartFromItemData(ItemData item, ProjectInformation projectInfo)

The important runtime consequence is that product-part option upcharge is now expected to translate to the current CET currency by default through the shared translatePrice=true flow. If your manufacturer package overrides option pricing, verify that option rows and option-derived totals are still correct and are not translated twice.

The following shared import/export hooks are also now part of the effective ProdPart surface because 17.0 routes more behavior through common Part logic:

Added/changed usage: public void importOFDASpecItemTag(XmlTag tag, OFDAHeaderContent header=null)
Added: extend public void importOptionTag(XmlTag tag, OFDAHeaderContent header=null)

class ProdPart option identity

Option special keys now use the option's own stable key instead of composing a key from the part special key.

Old: extend public str optSpecialKey(PartOptionItem opt) {
         return specialsKey() # opt.specialsKey();
     }
New: extend public str optSpecialKey(PartOptionItem opt) {
         return opt.?key();
     }

If you persisted or compared option special keys manually, update that logic to use the option key directly.

Other ProdPart public changes

Added: public loadFailedResult loadFailed(ObjectFormatter formatter, LoadFailure failure)
Added: public PartInfoTree[] infoTrees()

Old: public void removeAllSpecials(PropObj s=null, bool invalidateWorldPrice=true)
New: public void removeAllSpecials(bool invalidateWorldPrice, PropObj s=null)

Added: public void removeOverrideCreator(bool invalidateWorldPrice, PropObj s=null)
Added: public void removeAdditionCreator(bool invalidateWorldPrice, PropObj s=null)
Added: public loadFailedResult loadFailed(ObjectFormatter formatter, LoadFailure failure)
Added: public PartInfoTree[] infoTrees()
Added: public void removeAllSpecials(PropObj s=null, bool invalidateWorldPrice=true)
Added: public void removeOverrideCreator(PropObj s=null, bool invalidateWorldPrice=true)
Added: public void removeAdditionCreator(PropObj s=null, bool invalidateWorldPrice=true)

Runtime/Behavior Changes

class AbsTagPartColumn

The Ind. Tag column is now splittable by user-modified Ind. Tag text. This keeps calculation rows separated when the underlying part number is the same but the user override text differs.

value(..) now uses Part.itemTagText() and isAdjusted(..) now checks Part.isUsrModItemTagInfo(), so only explicit user overrides are treated as adjusted values.

This split/merge behavior is accomplished through the updated cm.core.part.Part.finalFlattenableKey() behavior, not just column logic. Because part adjustments are tied to the final flattenable identity used during merge, any extension with saved or custom part-adjustment flows should test those workflows with user-modified Ind. Tags.

Row-level Ind. Tag adjustments are now applied per part

indTagAdjustmentHook(..) is now registered through partRowAdjustmentApplyHooks. When the adjusted column is AbsTagPartColumn, the current value is applied to every part in the row and the temporary PartColumnAdjustment is removed afterward.

Legacy single-part adjustments are remapped after merge finalization

fixCET17Adjustments(..) now runs from appendPostFinalizeAfterMergeHook(..) and restores older single-part adjustments that were saved with the pre-17.0 flattenable key format. This is mainly relevant when opening 16.5-and-earlier drawings that contain user-modified Ind. Tags or other single-part adjustments.

class ProdPart

When calculation or ExtendedPartData imports contain an indTag, ProdPart now routes that value through setUserItemTagInfo(..) so it participates in the new shared user-tag storage and reset-behavior system.

Product parts now distinguish base options from inserted custom options

allPartOptions() now includes custom inserted options and is used by more export and query flows. This means code that previously assumed options() and the effective option sequence were the same may now see different behavior when custom options have been added.

Option specials are now stored per part in OptionSpecialHolder

Before 17.0, both part specials and option specials were stored together in the owner snapper's PartSpecialHolder, and option-special keys were composed from the part key and option key. In 17.0, each part instead gets its own OptionSpecialHolder, stored on the owner under optionSpecialHolderPropKey(part), which includes the part's sourceId().

The practical effect is:

  • PartSpecialHolder remains the owner-level holder for part specials, overrides, and part additions
  • OptionSpecialHolder.specials() now stores only that part's default-option specials
  • OptionSpecialHolder.additions() now stores only that part's additional CustomOptionSpecials
  • option-special lookup is now anchored to stable part identity plus the option's own key()

If your extension still inspects the snapper-level PartSpecialHolder.specials() map for option specials, migrate that code to OptionSpecialHolder and the ProdPart helper APIs.

Older option-special data requires migration on load

When loading pre-17.0 drawings, old option specials can still exist in the shared PartSpecialHolder under legacy combined keys. ProdPartSpecialMigrator exists to read those old entries, move them into the new per-part holder, and then remove the old keys.

This migration is not automatic for every package. If your package used package-specific part keys or option keys, or if it persists older drawings that must preserve option specials and additional options, you need to opt in to the migrator flow and override the key-conversion methods as needed. In practice, CET discovers the migrator through the owner snapper's partSpecialMigrator prop/default and then invokes it from the snapper load path.

anchorKeyToCustom now drives ordering for added options

Added options are no longer just an unordered bag of CustomOptionSpecials. CustomOptionSpecial.anchorOptKey identifies which option or added option a custom option follows, and ProdPart.anchorKeyToCustom caches that relationship after insertCustomOptions(...) rebuilds the effective option sequence.

This is important because the cache is used by downstream export/import and query flows to preserve added-option order:

  • query import and drag/drop add options by updating anchorOptKey
  • DsPart OFDA/XML export uses getFollowingCustom(...) to emit custom options after the correct anchor option
  • DsPData SIF and PMX export use getFollowingCustom(...) to generate additional option rows in the correct order

If you create or reorder additional options yourself, make sure you invalidate and rebuild part options so the anchor cache is refreshed before exporting or reading back option order.

OFDA/XML export now writes base price and option-list price data

The 17.0 OFDA/XML export flow now uses Part.xmlUserDefined(...), ProdPart.xmlUserDefined(...), and package-specific overrides such as DsPart to write additional pricing data into exported XML.

For product parts, option-list prices are exported through cOptionListTag. Base price export is handled through the shared cBasePriceTag path in Part.xmlUserDefined(...).

Older drawings with both part adjustments and option specials can lose a part adjustment

As part of the 17.0 OptionSpecial cleanup, the old originalPart / originalOptStr flattenable-key path was removed. A practical consequence is that some older drawings that contain both a part adjustment and an option special on the same effective part can lose the part adjustment after load because the persisted adjustment key no longer resolves the same way.

This is most relevant for packages that saved older drawings with mixed part-adjustment and option-special state. Those flows should be tested explicitly when validating 17.0 migration behavior.

ProdPartAddition and ProdPartOverride are applied during Snapper.fetchParts()

The creator objects are stored on the owner first, but they do not affect calculations until the owner later calls fetchParts(). At that point the core flow calls applyCustomParts(this, localParts) after getParts(...) and afterGetParts(...), which is where additions and overrides become real generated parts.

For product parts, that means:

  • ProdPartAddition.part(owner) generates a ProdPart and turns its stored PartOption[] into additional CustomOptionSpecials in that part's OptionSpecialHolder
  • ProdPartOverride.part(owner) generates a ProdPart and applies its stored PartOption[] directly with setPartOptions(...)
  • only then are the generated parts merged into the owner's fetched local part list

This matters if you are debugging calculations, query dialogs, or exports and expect a stored creator to have immediate effect before fetchParts() has run.

Additional options are now first-class runtime data

Custom option additions are now stored and ordered explicitly through anchor keys. This allows imported or user-added options to be inserted after specific existing options and preserved in query/import workflows.

If you rely on option order, option export, or custom option persistence, test those flows with added options and anchor-key updates.

Option customization state now includes additions and inherited overrides

At runtime, a product-part option can now report:

  • special
  • addition
  • override when the owning part is overridden

This affects query-grid highlighting and any extension logic that uses option customization state to decide visibility or behavior.