diff --git a/CHANGELOG.md b/CHANGELOG.md index c649170..54b99a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org) using the following scheme: MAJOR.MINOR.PATCH +## [0.46.0] - 2026-05-29 + +### Added +- Dolibarr extrafield +- Business object extrafield encoding + +## [0.45.0] - 2026-05-26 + +### Changed +- Fixed DolibarrIntervention decoding/encoding + ## [0.44.0] - 2026-05-21 ### Changed @@ -62,13 +73,13 @@ and this project adheres to [Semantic Versioning](https://semver.org) using the ## [0.34.0] - 2026-04-28 -### Added -- Fixed LaReponse article content property type +### Fixed +- LaReponse article content property type ## [0.33.0] - 2026-04-27 -### Added -- Fixed LaReponse article and comment protocols +### Fixed +- LaReponse article and comment protocols ## [0.32.0] - 2026-04-27 diff --git a/Sources/SwiftDolibarr/Models/Core/Common/CommonBusinessObject.swift b/Sources/SwiftDolibarr/Models/Core/Common/CommonBusinessObject.swift index ab8acac..7392a4d 100644 --- a/Sources/SwiftDolibarr/Models/Core/Common/CommonBusinessObject.swift +++ b/Sources/SwiftDolibarr/Models/Core/Common/CommonBusinessObject.swift @@ -171,6 +171,7 @@ public class CommonBusinessObject: Equatable, Codable, DolibarrBusinessObject { hasher.combine(id) hasher.combine(statusCode) hasher.combine(optional: entityId) + hasher.combine(optional: arrayOptions) hasher.combine(optional: notePublic) hasher.combine(optional: notePrivate) } @@ -180,6 +181,7 @@ public class CommonBusinessObject: Equatable, Codable, DolibarrBusinessObject { try container.encodeIfNotEmpty(id, forKey: .id) try container.encodeIfNotEmpty(statusCode, forKey: .statusCode) try container.encodeIfPresentAndNotEmpty(entityId, forKey: .entityId) + try container.encodeIfPresent(arrayOptions, forKey: .arrayOptions) try container.encodeIfPresent(notePublic, forKey: .notePublic) try container.encodeIfPresent(notePrivate, forKey: .notePrivate) } diff --git a/Sources/SwiftDolibarr/Models/Core/Intervention/DolibarrInterventionLine.swift b/Sources/SwiftDolibarr/Models/Core/Intervention/DolibarrInterventionLine.swift index 7fcf620..95aab3f 100644 --- a/Sources/SwiftDolibarr/Models/Core/Intervention/DolibarrInterventionLine.swift +++ b/Sources/SwiftDolibarr/Models/Core/Intervention/DolibarrInterventionLine.swift @@ -85,8 +85,8 @@ public final class DolibarrInterventionLine: CommonBusinessObjectLine { case durationSeconds = "duration" case durationSecondsV21 = "duree" case interventionId = "fk_fichinter" - case desccriptionDecode = "desc" - case descriptionEncode = "description" + case desc = "desc" + case description = "description" } // MARK: - Inits @@ -115,7 +115,7 @@ public final class DolibarrInterventionLine: CommonBusinessObjectLine { self.dateIntervene = try container.decode(Int.self, forKey: .date) self.durationSeconds = try container.decode(String.self, forKey: .durationSeconds) self.interventionId = try container.decode(String.self, forKey: .interventionId) - self.description = try container.decodeIfPresent(String.self, forKey: .desccriptionDecode) + self.description = try container.decodeIfPresent(String.self, forKey: .desc) try super.init(from: decoder) if self.durationSeconds.isEmpty { self.durationSeconds = try container.decode(String.self, forKey: .durationSecondsV21) @@ -170,7 +170,8 @@ public final class DolibarrInterventionLine: CommonBusinessObjectLine { try container.encode(durationSeconds, forKey: .durationSeconds) try container.encode(durationSeconds, forKey: .durationSecondsV21) try container.encode(interventionId, forKey: .interventionId) - try container.encodeIfPresent(description, forKey: .descriptionEncode) + try container.encodeIfPresent(description, forKey: .description) + try container.encodeIfPresent(description, forKey: .desc) try super.encode(to: encoder) } diff --git a/Sources/SwiftDolibarr/Models/Core/Setup/DolibarrExtrafield.swift b/Sources/SwiftDolibarr/Models/Core/Setup/DolibarrExtrafield.swift new file mode 100644 index 0000000..6904baa --- /dev/null +++ b/Sources/SwiftDolibarr/Models/Core/Setup/DolibarrExtrafield.swift @@ -0,0 +1,248 @@ +// Copyright 2026 M34D - William Mead +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// DolibarrExtrafield.swift +// SwiftDolibarr +// +// Created by William Mead on 29/05/2026. +// + +import Foundation +#if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) +import OSLog +#endif + +/// A Dolibarr extrafield. +/// +/// Maps to the Dolibarr `/setup/extrafields` REST API endpoint. Extrafields +/// are user-defined custom attributes attached to a specific Dolibarr object +/// (identified by ``elementType``), allowing the data model to be extended +/// without modifying core tables. +/// +/// Each extrafield has a ``type`` (e.g. `varchar`, `int`, `date`, `select`), +/// a display ``label``, and optional metadata such as ``size``, +/// ``default``, ``computed`` expression, ``required`` / ``unique`` flags, +/// display ``position``, and ``visibility`` (list visibility). +/// +/// - Note: The ``elementType`` value matches the Dolibarr element it extends +/// (e.g. `societe`, `facture`, `commande`, `propal`, `product`). +/// - SeeAlso: ``DolibarrObject`` +public struct DolibarrExtrafield: Hashable, Decodable, Sendable, DolibarrObject { + + // MARK: - Properties + + // Required + + /// Extrafield ID. + public var id: String + + /// Extrafield data type (e.g. `varchar`, `int`, `double`, `date`, `select`). + public var type: String + + /// Display label for the extrafield. + public var label: String + + /// Dolibarr element type this extrafield is attached to (e.g. `societe`, + /// `facture`, `commande`, `product`). + /// + /// Mapped Dolibarr property: **elementtype** + public var elementType: String + + // Optional + + /// Field size or length, when applicable to the ``type``. + public var size: String? + + /// Default value applied when the extrafield is not set. + public var `default`: String? + + /// Computed expression evaluated server-side to derive the field value. + public var computed: String? + + /// Whether this extrafield is required (`"1"`) or not (`"0"`). + public var required: String? + + /// Whether this extrafield enforces uniqueness (`"1"`) or not (`"0"`). + public var unique: String? + + /// Display position used to order extrafields in the UI. + /// + /// Mapped Dolibarr property: **pos** + public var position: String? + + /// List visibility flag controlling whether the extrafield appears in + /// list views. + /// + /// Mapped Dolibarr property: **list** + public var visibility: String? + + // MARK: - Enums + + enum CodingKeys: String, CodingKey { + case id + case type + case label + case elementType = "elementtype" + case size + case `default` + case computed + case required + case unique + case position = "pos" + case visibility = "list" + } + + // MARK: - Inits + + public init( + id: String = "", + type: String = "", + label: String = "", + elementType: String = "", + size: String? = nil, + default: String? = nil, + computed: String? = nil, + required: String? = nil, + unique: String? = nil, + position: String? = nil, + visibility: String? = nil + ) { + self.id = id + self.type = type + self.label = label + self.elementType = elementType + self.size = size + self.default = `default` + self.computed = computed + self.required = required + self.unique = unique + self.position = position + self.visibility = visibility + } + + public init(from decoder: any Decoder) throws { + do { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + Logger.logWithoutSignal("\(Self.self).init.decode", category: .api) + #endif + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.type = try container.decode(String.self, forKey: .type) + self.label = try container.decode(String.self, forKey: .label) + self.elementType = try container.decode(String.self, forKey: .elementType) + self.size = try container.decodeIfPresent(String.self, forKey: .size) + self.default = try container.decodeIfPresent(String.self, forKey: .default) + self.computed = try container.decodeIfPresent(String.self, forKey: .computed) + self.required = try container.decodeIfPresent(String.self, forKey: .required) + self.unique = try container.decodeIfPresent(String.self, forKey: .unique) + self.position = try container.decodeIfPresent(String.self, forKey: .position) + self.visibility = try container.decodeIfPresent(String.self, forKey: .visibility) + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + Logger.logWithoutSignal("\(Self.self).init.decoded", category: .api) + #endif + } catch let error as DecodingError { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + Logger.logDecodingError(error, decodeContext: "\(Self.self).init") + #endif + throw error + } catch { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + Logger.logErrorWithSignal(error, context: "\(Self.self).init", category: .api) + #endif + throw error + } + } + +} + +/// Top-level response wrapper for `/setup/extrafields`. +/// +/// Decodes the nested JSON structure (element type → field name → extrafield) +/// into an ordered array of ``DolibarrExtrafieldGroup`` for convenient +/// iteration. +/// +/// - SeeAlso: ``DolibarrExtrafield``, ``DolibarrExtrafieldGroup`` +public struct DolibarrExtrafields: Hashable, Decodable, Sendable { + + // MARK: - Properties + + /// Extrafield groups, one per Dolibarr element type. + public var elementGroups: [DolibarrExtrafieldGroup] + + // MARK: - Inits + + public init(elementGroups: [DolibarrExtrafieldGroup] = []) { + self.elementGroups = elementGroups + } + + public init(from decoder: any Decoder) throws { + do { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + Logger.logWithoutSignal("\(Self.self).init.decode", category: .api) + #endif + let container = try decoder.singleValueContainer() + let raw = try container.decode([String: [String: DolibarrExtrafield]].self) + self.elementGroups = raw.map { elementType, fields in + DolibarrExtrafieldGroup( + elementType: elementType, + fields: Array(fields.values) + ) + } + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + Logger.logWithoutSignal("\(Self.self).init.decoded", category: .api) + #endif + } catch let error as DecodingError { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + Logger.logDecodingError(error, decodeContext: "\(Self.self).init") + #endif + throw error + } catch { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + Logger.logErrorWithSignal(error, context: "\(Self.self).init", category: .api) + #endif + throw error + } + } + +} + +/// A group of extrafields attached to the same Dolibarr element type. +/// +/// Produced when decoding a ``DolibarrExtrafields`` response — one group is +/// created per top-level JSON key (the element type, e.g. `societe`). +/// +/// - SeeAlso: ``DolibarrExtrafield``, ``DolibarrExtrafields`` +public struct DolibarrExtrafieldGroup: Hashable, Sendable { + + // MARK: - Properties + + /// The Dolibarr element type these extrafields are attached to + /// (e.g. `societe`, `facture`, `commande`, `product`). + public var elementType: String + + /// The extrafields defined for this element type. + public var fields: [DolibarrExtrafield] + + // MARK: - Inits + + public init( + elementType: String = "", + fields: [DolibarrExtrafield] = [] + ) { + self.elementType = elementType + self.fields = fields + } + +}