CET 17.0 refactors Ind. Tag identity and storage in cm.core.part. User-entered Ind. Tag text is now stored separately from the default ItemTagInfo key, part identity for Ind. Tags and part annotations is driven by sourceId(), and drawings from 16.5 and earlier are migrated on load so existing user-modified tags, AN/AD annotations, and single-part adjustments continue to work.
MR: Ind. Tag migration and restoration changes
The same stable-identity work also introduces broader Part support for additions, overrides, and customization state. Those APIs are important context for the new import/customization workflows in cm.core.part.query and cm.abstract.part.query, where imported parts are applied as additions or overrides and need to map back to a stable logical part identity.
CET 17.0 also separates part-special storage from option-special storage. PartSpecialHolder remains the shared owner-level container for part specials, overrides, and additional parts, while cm.abstract.part introduces a separate per-part OptionSpecialHolder for option specials and added options. Older drawings may therefore need holder migration in addition to key migration.
PartIf your custom Part subclass overrides Ind. Tag identity or ItemTagInfo key generation, move that logic to the new override points below.
Old: extend public str itemTagKey() New: final public str itemTagKey() New override point: extend public str sourceId() Old: extend public str itemTagInfoKey() New: final public str itemTagInfoKey() New override point: extend public str defaultItemTagInfoKey() Old: extend public ItemTagInfo setItemTagInfo(str text) New: extend public ItemTagInfo setUserItemTagInfo(str text)
PartProxyPartProxy now has a matching hook for the final flattenable-key phase.
Added: extend public void finalizeFlattenbleKey(Part part)
This gives proxy-based part implementations a dedicated extension point for adjusting the part after the flattenable key has been finalized.
The constructor argument previously named partSourceId is now sourceId in core Part and in downstream part subclasses that forward into it.
Old: public constructor(PartData data, str partSourceId=null) New: public constructor(PartData data, str sourceId=null) Old: public constructor(Snapper snapper, str articleCode, str description, double basePrice, Double optionPriceSum, ..., str partSourceId=null) New: public constructor(Snapper snapper, str articleCode, str description, double basePrice, Double optionPriceSum, ..., str sourceId=null) Old: public constructor(Snapper snapper, str articleCode, str description, double listPrice, ..., str partSourceId=null) New: public constructor(Snapper snapper, str articleCode, str description, double listPrice, ..., str sourceId=null) Added: extend public void setSourceId(str id) Added: extend public str sourceId() Added: extend public str{} sourceIds()
Part customization APIsPart now exposes public APIs for additions, overrides, and overall customization state.
These are used by the new query import workflow.
Added: extend public bool isOverride(PropObj s=null) Added: extend public void putOverrideCreator(PartCreator overrideSpecial, bool invalidateWorldPrice, PropObj s=null) Added: extend public PartCreator getOverrideCreator(PropObj s=null) Added: extend public void removeOverrideCreator(bool invalidateWorldPrice, PropObj s=null) Added: extend public bool isAddition(PropObj s=null) Added: extend public void putAdditionCreator(PartCreator additionSpecial, bool invalidateWorldPrice, PropObj s=null) Added: extend public PartCreator getAdditionCreator(PropObj s=null) Added: extend public void removeAdditionCreator(bool invalidateWorldPrice, PropObj s=null) Added: extend public customizationType customizationStatus(PropObj s=null) Added: extend public symbol{} customizationStates(PropObj s=null)
Part special and standard-special APIsPart now separates user-visible specials from internal standard specials, and several special-management signatures now require the invalidateWorldPrice argument before PropObj s.
Old: extend public bool containsSpecial(PropObj s=null) New: extend public bool containsSpecial(bool ignoreStandardSpecial=false, PropObj s=null) Old: extend public bool containsAnySpecials(PropObj s=null) New: extend public bool containsAnySpecials(bool ignoreStandardSpecial=false, PropObj s=null) Old: extend public PartSpecial getSpecial(PropObj s=null) New: extend public PartSpecial getSpecial(bool ignoreStandardSpecial=false, PropObj s=null) Old: extend public PartSpecial getSpecial(str id, PropObj s=null) New: extend public PartSpecial getSpecial(str id, bool ignoreStandardSpecial=false, PropObj s=null) Old: extend public void putSpecial(PartSpecial special, PropObj s=null, bool invalidateWorldPrice=true) New: extend public void putSpecial(PartSpecial special, bool invalidateWorldPrice, PropObj s=null) Old: extend public void putSpecial(str id, PartSpecial special, PropObj s=null, bool invalidateWorldPrice=true) New: extend public void putSpecial(str id, PartSpecial special, bool invalidateWorldPrice, PropObj s=null) Old: extend public void removeSpecial(str id, PropObj s=null, bool invalidateWorldPrice=true) New: extend public void removeSpecial(str id, bool invalidateWorldPrice, PropObj s=null) Old: extend public void removeSpecial(PropObj s=null, bool invalidateWorldPrice=true) New: extend public void removeSpecial(bool invalidateWorldPrice, PropObj s=null) Old: extend public void removeAllSpecials(PropObj s=null, bool invalidateWorldPrice=true) New: extend public void removeAllSpecials(bool invalidateWorldPrice, PropObj s=null) Added: extend public PartSpecial getStandardSpecial(PropObj s=null) Added: extend public PartSpecial getStandardSpecial(str id, PropObj s=null) Added: extend public void putStandardSpecial(PartSpecial special, bool invalidateWorldPrice, PropObj s=null) Added: extend public void putStandardSpecial(str id, PartSpecial special, bool invalidateWorldPrice, PropObj s=null) Added: extend public void removeStandardSpecial(str id, bool invalidateWorldPrice, PropObj s=null) Added: extend public void removeStandardSpecial(bool invalidateWorldPrice, PropObj s=null)
Standard specials are a new lower-priority internal-special system. getSpecial(...) now falls back to getStandardSpecial(...) unless ignoreStandardSpecial=true is passed. Implementers should also expect standard specials to be treated differently from user specials in user-facing state such as highlighting and customization-status checks.
PartSpecialThe PartSpecial constructor is now more permissive and can be called without explicitly passing all fields.
Old: public constructor(str partNum, str descr, bool priceReplace, double amount) New: public constructor(str partNum=null, str descr=null, bool priceReplace=true, double amount=0)
PartSpecialHolderPartSpecialHolder remains part of the public cm.core.part surface, but in 17.0 it is more clearly the holder for part-level customization data.
If your code previously assumed option specials also lived here forever, that assumption is no longer valid.
Changed meaning: public class PartSpecialHolder Added: public str->PartSpecial standardSpecials() Added: public str->PartCreator overrides() Added: private PartCreator[] additions() Added: private str->int additionKeyToIdx() Added: extend public PartSpecial getStandardSpecial(str key) Added: extend public void putStandardSpecial(str key, PartSpecial special) Added: extend public void removeStandardSpecial(str key) Added: extend public PartCreator getOverrideCreator(str key) Added: extend public void putOverrideCreator(str key, PartCreator special) Added: extend public void removeOverrideCreator(str key) Added: extend public void removeAllOverrides() Added: extend public void putAdditionCreator(PartCreator custom) Added: extend public PartCreator getAdditionCreator(int index) Added: extend public PartCreator getAdditionCreator(str key) Added: extend public void removeAdditionCreator(int index) Added: extend public void removeAdditionCreator(str additionKey) Added: extend public void removeAllAdditions() Added: extend public Part[] getAdditionalParts(PropObj s) Added: extend public void appendAdditionKeyToIdx(str key) Added: extend public void rebuildAdditionKeyToIdx()
The shared helper functions in cm.core.part.partSpecialHolderFunctions.cm also changed shape in 17.0. These names are useful search targets if your code manages owner-level part specials directly.
Old: public void removeSpecialHolder(PropObj s, bool invalidateWorldPrice=true) New: public void removeSpecialHolder(PropObj s, bool invalidateWorldPrice) Old: public void putSpecial(PropObj s, str key, PartSpecial special, bool invalidateWorldPrice=true) New: public void putSpecial(PropObj s, str key, PartSpecial special, bool invalidateWorldPrice) Old: public void removeSpecial(PropObj s, str key, bool invalidateWorldPrice=true) New: public void removeSpecial(PropObj s, str key, bool invalidateWorldPrice) Old: public void removeAllSpecials(PropObj s, bool invalidateWorldPrice=true) New: public void removeAllSpecials(PropObj s, bool invalidateWorldPrice) Added: public bool containsStandardSpecial(PropObj s) Added: public PartSpecial getStandardSpecial(PropObj s, str key) Added: public void putStandardSpecial(PropObj s, str key, PartSpecial special, bool invalidateWorldPrice) Added: public void removeStandardSpecial(PropObj s, str key, bool invalidateWorldPrice) Added: public void removeAllStandardSpecials(PropObj s, bool invalidateWorldPrice) Added: public void putOverrideCreator(PropObj s, str key, PartCreator creator, bool invalidateWorldPrice) Added: public void removeOverrideCreator(PropObj s, str key, bool invalidateWorldPrice) Added: public void removeAllOverrides(PropObj s, bool invalidateWorldPrice) Added: public void putAdditionCreator(PropObj s, PartCreator custom, bool invalidateWorldPrice) Added: public void removeAdditionCreator(PropObj s, int index, bool invalidateWorldPrice) Added: public void removeAdditionCreator(PropObj s, str key, bool invalidateWorldPrice) Added: public void removeAllAdditions(PropObj s, bool invalidateWorldPrice)
PartSpecialMigratorPartSpecialMigrator is the new base migration helper for packages that need to remap old special keys to 17.0 sourceId()-based keys.
Added: public class PartSpecialMigrator Added: public bool pendingMigration Added: public str{} migrated Added: public Version version Added: extend public void migrateSpecials(Part[] parts, PropObj s) Added: extend public void beforeMigration(Part[] parts, PropObj s) Added: extend public void attemptMigrateSpecials(Part part, PropObj s) Added: extend public str oldPartSpecialsKey(Part part) Added: extend public void afterMigration(Part[] parts, PropObj s)
If your package used a non-default pre-17.0 part-special key, override oldPartSpecialsKey(...) in a subclass.
PartSpecialMigrator is discovered during loadThe migrator lookup happens from cm.core.Snapper, which is important if you are trying to wire a package-specific migrator into drawing load.
Important load APIs on Snapper: extend public void initPartSpecialMigrator(ObjectFormatter formatter) extend public PartSpecialMigrator getPartSpecialMigrator(bool init=false) extend public void migratePartSpecials(Part[] parts)
At a high level, Snapper.loaded1() calls initPartSpecialMigrator(formatter). If the snapper already has a non-empty PartSpecialHolder and the loaded package version is older than the current runtime, CET resolves this."partSpecialMigrator" through getPartSpecialMigrator(init=true), initializes it from the prop default if needed, and marks pendingMigration = true with the loaded package version. Later, migratePartSpecials(parts) checks that flag and calls migrateSpecials(parts, this).
That means a migrator is not found by package scanning or global registration. It is found by the snapper exposing a partSpecialMigrator prop/default that returns the right PartSpecialMigrator instance.
PartCreatorPartCreator is the new base customization object used for part additions and overrides.
It stores PartData, generates a stable creator key, lazily creates a Part, and can replay extended adjustments onto the generated part.
Added: public class PartCreator Added: public constructor(PartData data) Added: extend public str key() Added: extend public str generateKey() Added: extend public Part part(PropObj owner=null) Added: extend public void invalidate() Added: extend public Part generatePart(PropObj owner=null) Added: extend public str generateFlattenableKey(Part part) Added: extend public void setPartDataValues(Part part, PropObj owner=null) Added: extend public void applyExtendedAdjustments(Part part, Space space)
If your extension needs custom addition or override generation, subclass PartCreator instead of treating additions and overrides as raw Part instances.
Part metadata APIsCompany and catalog access are now part of the core Part API instead of only being available through higher-level part types.
Added: extend public str companyCode() Added: extend public str catalog() Added: extend public str catalogCode() Added: extend public double pkgCount() Added: extend public void generatePartDetails(PartDetails partDetails)
If you previously depended on AbsPart-specific metadata accessors, prefer the new core Part interfaces where possible. pkgCount() is now part of the shared core part surface and feeds both column/UI output and exported part details.
Part pricing APIsThe shared core pricing APIs now consistently honor CET currency translation in more places.
Changed behavior/important usage: extend public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true) Changed behavior/important usage: extend public double optionPriceSum(bool includeChildren=false, Space space=null, bool translatePrice=true, bool invalidate=false) Changed behavior/important usage: extend public Double upcharge(bool includeChildren=false, Space space=null, bool translatePrice=true, bool invalidate=false) Changed behavior/important usage: final public double listPrice(bool includeChildren=false, Space space=null, bool translatePrice=true) Changed behavior/important usage: final public double validListPrice(bool includeChildren=false, Space space=null, bool translatePrice=true) Changed behavior/important usage: extend public double totalBasePrice(bool includeChildren=false, Space space=null, bool translatePrice=true) Changed behavior/important usage: extend public double totalOptionPriceSum(bool includeChildren=false, Space space=null, bool translatePrice=true, bool invalidate=false) Changed behavior/important usage: extend public double totalListPrice(bool includeChildren=false, Space space=null, bool translatePrice=true) Changed behavior/important usage: extend public double totalValidListPrice(bool includeChildren=false, Space space=null, bool translatePrice=true)
The signatures are mostly unchanged, but 17.0 changes the shared runtime behavior so these methods now translate prices to the current CET currency more consistently. If your package overrides pricing logic, verify that your override neither skips the new translation path nor translates prices twice.
PartDataPart data objects now expose package count as part of the shared core data contract.
Added: extend public double pkgCount() Added: extend public double setPkgCount(double value)
BasicPartDataBasicPartData now stores package count directly.
Added: public double _pkgCount = 1 Added: public double pkgCount() Added: public double setPkgCount(double value)
PartDetailsPMX/export part details now include package count.
Added: final public int packageCount() Added: final public int packageCount=(int value)
The shared core pricing flow now translates base prices and option upcharge prices to the current CET currency more consistently. In practice, this affects BOM-visible areas such as Calculations, query dialogs, print/report flows, article-view style part presentations, and order/export paths that rely on the shared pricing APIs.
This can require migration for manufacturers. If your extension overrides basePrice(...), optionPriceSum(...), upcharge(...), listPrice(...), totalListPrice(...), supportsDefaultCurrency(...), or related export helpers, validate that prices still come out correctly when the CET currency differs from the part's default currency.
The new core part settings support three reset modes:
Switching between these modes converts existing user-modified Ind. Tags to the new keying structure so user overrides are preserved.
When drawings from 16.5 and earlier are loaded, CET now:
sourceId()finalFlattenableKey() now reflects Ind. Tag split stateThe new Ind. Tag split/merge behavior is backed by changes to Part.finalFlattenableKey(). User-modified Ind. Tag state now affects the final key used by core merge and adjustment logic, which means it can change how rows merge and which key is used for single-part adjustments.
Older saved adjustments are remapped through legacyFinalFlattenableKey() where possible, but extension developers should still test any part-adjustment workflows that depend on persisted adjustment keys, especially when user-modified Ind. Tags are present.
If your part logic is implemented through PartProxy, note that the new finalizeFlattenbleKey() hook is now part of that flattenable-key flow. Proxy code can participate directly in the finalization step instead of only relying on earlier key-generation behavior.
Package count is now treated as shared core part metadata instead of only being surfaced by package-specific part types. Part.pkgCount(), PartData.pkgCount(), and Part.generatePartDetails(...) now carry that value through core column logic and PMX/export detail generation.
If your package previously treated package count as package-specific behavior, review any custom part-data or export code that should now participate in the shared core package-count flow.
User-defined tag text is now stored off the default ItemTagInfo key instead of replacing it directly. If your extension parses or persists user-tag keys manually, update that logic to use cItemTagInfoDelim = ":::" and the UserTagInfoHolder APIs instead of the old inline key format.
UserTagInfoHolder improves restoration behaviorUser-modified Ind. Tag info is now centralized on the snapper through UserTagInfoHolder. This gives CET a stable place to preserve explicit overrides while resetting Ind. Tags, switching reset behaviors, and migrating older drawings, which is much cleaner than the previous restoration flow that relied on pure key matching against globally stored modifications.
sourceId() is now the stable identity used by multiple migration pathsThe same sourceId() value now drives:
itemTagKey())specialsKey())cm.abstract.partIf your custom Part subclass needs stable user-modified Ind. Tags or annotations across part-number changes, or participates in query import/customization flows, make sure sourceId() returns a stable per-logical-part identifier.
Snapper.fetchParts()The new addition/override workflow is applied during the owner's fetchParts() call.
The important order in cm.core.Snapper.fetchParts(...) is:
beforeGetParts(env)getParts(env, visited)afterGetParts(env)migratePartSpecials(localParts) // NEWapplyCustomParts(this, localParts) // NEWapplyCustomParts(...) then does two things for the current owner:
getAdditionalParts(this), which generates each added part from its PartCreatorlocalPartsOverrides are matched by stable part identity. The replacement loop starts from each part's sourceId(), sets overridePart.setOveriddenPartKey(part.sourceId()), and can continue while another override exists for the replacement key.
The practical migration consequence is that additions and overrides only affect the current snapper's local parts before child-part collection. If you are debugging why a customization does or does not appear in calculations, this fetchParts() ordering is the first place to check.
CET 17.0 introduces standard specials as a separate internal-special concept alongside ordinary user specials. customizationStates(...) only treats true user specials as the special customization state, and callers can use ignoreStandardSpecial=true in containsSpecial(...) / containsAnySpecials(...) when they want user-facing state such as row highlighting to ignore standard specials.
Related UI such as Calculations and the query grid do not highlight rows just because a standard special exists.
For 17.0-created data, PartSpecialHolder should now be thought of as the owner-level holder for:
PartSpecialOlder drawings can still load legacy option specials from this holder until a package-specific migrator moves them into OptionSpecialHolder, but new code should not treat PartSpecialHolder.specials() as the long-term storage location for option specials.
PartSpecialMigrator provides the shared migration pattern, but packages with custom historical key formats still need to opt in and provide the right subclass or override logic through the snapper's partSpecialMigrator prop/default. If your extension changed part identity before 17.0, test loading older drawings with specials, overrides, additions, and option specials rather than assuming the default oldPartSpecialsKey(...) implementation is sufficient.