rodoo 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4659d7f4bf1d0156dc09a4b417310c208ecf83c901b80dbdf99ac84ef80ae703
4
- data.tar.gz: b9c048a9136da50585032eed8a1e267bdb6174f5623252b603ba1c188e2f8004
3
+ metadata.gz: 87923ea6d949a5082fdf435534be6e9785c655b2bc37533784c8389c3288661b
4
+ data.tar.gz: 33009601846b3655494da2ee9f03c1e1ec87896d964094b03491f9ce692e07d8
5
5
  SHA512:
6
- metadata.gz: de08af6d3ce14f9848b10598eb788b488e2b9dc5e6021a334cea754b4b50e6a5f00ea6370bc5e35ddb926f663e9b8f9ac57825acca78032d0b737e2854f80231
7
- data.tar.gz: 4dfb5a9c3cfe0e281abed50567aeb3cac61a7e605f09ec29a0bbd6471d2b16c5da801521d1d6c1833de44f81c05c7195adddd9923d174a6b28ef7efa14c61b68
6
+ metadata.gz: 50bf69658ddddcf3f3b8a82f1998ebeda996ec656cfb5647c6a6c9d9e1ef2ba60349b585e861ceb44b4af69ab8b7c01c6050b5f47e58f796c89ffe5ea2074e1a
7
+ data.tar.gz: 7c0e9f254bc0a0c71474e29b8568fef331f78855cfac1a1956400dfc200914a5dcced7b1f440b167abb73cf646244628238c8a260ed6cf61183b5a6400751dd0
data/.rubocop.yml CHANGED
@@ -11,6 +11,10 @@ Metrics/MethodLength:
11
11
  Exclude:
12
12
  - "test/**/*"
13
13
 
14
+ Metrics/AbcSize:
15
+ Exclude:
16
+ - "test/**/*"
17
+
14
18
  Metrics/ClassLength:
15
19
  Max: 105
16
20
  Exclude:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-01-20
4
+
5
+ - Add PDF attachment support for accounting entries
6
+ - Add Attachment model for ir.attachment API
7
+
3
8
  ## [0.1.0] - 2025-12-24
4
9
 
5
10
  - Initial release
data/README.md CHANGED
@@ -149,6 +149,73 @@ contact.destroy
149
149
  contact.destroyed? # => true
150
150
  ```
151
151
 
152
+ ### Attachments
153
+
154
+ Attach PDF files to accounting entries (invoices, credit notes, journal entries):
155
+
156
+ ```ruby
157
+ invoice = Rodoo::ProviderInvoice.find(42)
158
+
159
+ # Attach from file path (sets as main attachment by default)
160
+ invoice.attach_pdf("/path/to/invoice.pdf")
161
+
162
+ # Attach with custom filename
163
+ invoice.attach_pdf("/path/to/document.pdf", filename: "vendor_invoice.pdf")
164
+
165
+ # Attach without setting as main attachment
166
+ invoice.attach_pdf("/path/to/supporting.pdf", set_as_main: false)
167
+
168
+ # Attach from base64 data
169
+ invoice.attach_pdf_from_base64(base64_content, filename: "invoice.pdf")
170
+
171
+ # List attachments
172
+ invoice.attachments
173
+ invoice.attachments(mimetype: "application/pdf")
174
+
175
+ # Get the main attachment
176
+ invoice.main_attachment
177
+
178
+ # Set a different attachment as main
179
+ invoice.set_main_attachment(attachment_id)
180
+ ```
181
+
182
+ #### With Rails ActiveStorage
183
+
184
+ When using ActiveStorage in a Rails application:
185
+
186
+ ```ruby
187
+ # Rails model with ActiveStorage attachment
188
+ class Invoice < ApplicationRecord
189
+ has_one_attached :pdf_document
190
+ end
191
+
192
+ # Sync to Odoo
193
+ rails_invoice = Invoice.find(123)
194
+ odoo_invoice = Rodoo::ProviderInvoice.find(42)
195
+
196
+ rails_invoice.pdf_document.open do |tempfile|
197
+ odoo_invoice.attach_pdf(tempfile, filename: rails_invoice.pdf_document.filename.to_s)
198
+ end
199
+ ```
200
+
201
+ #### Direct Attachment model usage
202
+
203
+ ```ruby
204
+ # Create attachment for any record
205
+ Rodoo::Attachment.create_for(
206
+ record, "/path/to/file.pdf", filename: "doc.pdf", mimetype: "application/pdf"
207
+ )
208
+
209
+ # Create from base64
210
+ Rodoo::Attachment.create_from_base64(
211
+ record, base64_data, filename: "doc.pdf", mimetype: "application/pdf"
212
+ )
213
+
214
+ # Find attachments for a record
215
+ Rodoo::Attachment.for_record(record)
216
+ Rodoo::Attachment.for_record(record, mimetype: "application/pdf")
217
+ ```
218
+
152
219
  ### Available models
153
220
 
154
221
  Rodoo includes pre-built models for common Odoo objects:
@@ -159,6 +226,7 @@ Rodoo includes pre-built models for common Odoo objects:
159
226
  | `Rodoo::Project` | `project.project` |
160
227
  | `Rodoo::AnalyticAccount` | `account.analytic.account` |
161
228
  | `Rodoo::AnalyticPlan` | `account.analytic.plan` |
229
+ | `Rodoo::Attachment` | `ir.attachment` |
162
230
  | `Rodoo::AccountingEntry` | `account.move` (all types) |
163
231
  | `Rodoo::CustomerInvoice` | `account.move` (move_type: out_invoice) |
164
232
  | `Rodoo::ProviderInvoice` | `account.move` (move_type: in_invoice) |
@@ -16,9 +16,16 @@ module Rodoo
16
16
  # @example Using the base class to query all types
17
17
  # all_entries = Rodoo::AccountingEntry.where([["date", ">", "2025-01-01"]])
18
18
  #
19
+ # @example Attaching a PDF
20
+ # invoice = Rodoo::ProviderInvoice.find(42)
21
+ # invoice.attach_pdf("/path/to/invoice.pdf")
22
+ #
19
23
  class AccountingEntry < Model
20
24
  model_name "account.move"
21
25
 
26
+ PDF_MIMETYPE = "application/pdf"
27
+ private_constant :PDF_MIMETYPE
28
+
22
29
  # Subclasses override this to specify their move_type
23
30
  #
24
31
  # @return [String, nil] The move_type value for this class
@@ -49,5 +56,108 @@ module Rodoo
49
56
  end
50
57
  super(scoped_attrs)
51
58
  end
59
+
60
+ # Attach a PDF file to this record
61
+ #
62
+ # @param file_path_or_io [String, IO, #read] File path or IO-like object
63
+ # @param filename [String, nil] The filename (derived from path if not provided)
64
+ # @param set_as_main [Boolean] Whether to set this as the main attachment (default: true)
65
+ # @return [Attachment] The created attachment
66
+ #
67
+ # @example Attach from file path (sets as main by default)
68
+ # invoice.attach_pdf("/path/to/invoice.pdf")
69
+ #
70
+ # @example Attach without setting as main
71
+ # invoice.attach_pdf("/path/to/supporting.pdf", set_as_main: false)
72
+ #
73
+ def attach_pdf(file_path_or_io, filename: nil, set_as_main: true)
74
+ resolved_filename = filename || derive_filename(file_path_or_io)
75
+ attachment = Attachment.create_for(
76
+ self, file_path_or_io, filename: resolved_filename, mimetype: PDF_MIMETYPE
77
+ )
78
+ set_main_attachment(attachment) if set_as_main
79
+ attachment
80
+ end
81
+
82
+ # Attach a PDF from base64-encoded data
83
+ #
84
+ # @param base64_data [String] The base64-encoded PDF data
85
+ # @param filename [String] The filename for the attachment
86
+ # @param set_as_main [Boolean] Whether to set this as the main attachment (default: true)
87
+ # @return [Attachment] The created attachment
88
+ #
89
+ # @example
90
+ # invoice.attach_pdf_from_base64(base64_content, filename: "invoice.pdf")
91
+ #
92
+ def attach_pdf_from_base64(base64_data, filename:, set_as_main: true)
93
+ attachment = Attachment.create_from_base64(
94
+ self, base64_data, filename: filename, mimetype: PDF_MIMETYPE
95
+ )
96
+ set_main_attachment(attachment) if set_as_main
97
+ attachment
98
+ end
99
+
100
+ # Set the main attachment for this record (visible in Odoo's side panel)
101
+ #
102
+ # @param attachment_or_id [Attachment, Integer] The attachment or its ID
103
+ # @return [self]
104
+ #
105
+ # @example With an Attachment object
106
+ # invoice.set_main_attachment(attachment)
107
+ #
108
+ # @example With an attachment ID
109
+ # invoice.set_main_attachment(123)
110
+ #
111
+ # rubocop:disable Naming/AccessorMethodName
112
+ def set_main_attachment(attachment_or_id)
113
+ attachment_id = attachment_or_id.is_a?(Attachment) ? attachment_or_id.id : attachment_or_id
114
+ update(message_main_attachment_id: attachment_id)
115
+ end
116
+ # rubocop:enable Naming/AccessorMethodName
117
+
118
+ # List attachments for this record
119
+ #
120
+ # @param mimetype [String, nil] Optional MIME type filter
121
+ # @return [Array<Attachment>] Array of attachments
122
+ #
123
+ # @example Get all attachments
124
+ # invoice.attachments
125
+ #
126
+ # @example Get only PDF attachments
127
+ # invoice.attachments(mimetype: "application/pdf")
128
+ #
129
+ def attachments(mimetype: nil)
130
+ Attachment.for_record(self, mimetype: mimetype)
131
+ end
132
+
133
+ # Get the main attachment for this record
134
+ #
135
+ # @return [Attachment, nil] The main attachment or nil if not set
136
+ #
137
+ # @example
138
+ # main = invoice.main_attachment
139
+ #
140
+ def main_attachment
141
+ main_id = self[:message_main_attachment_id]
142
+ return nil unless main_id
143
+
144
+ # Odoo returns [id, name] for many2one fields
145
+ attachment_id = main_id.is_a?(Array) ? main_id.first : main_id
146
+ return nil unless attachment_id
147
+
148
+ Attachment.find(attachment_id)
149
+ end
150
+
151
+ private
152
+
153
+ def derive_filename(file_path_or_io)
154
+ if file_path_or_io.respond_to?(:path)
155
+ File.basename(file_path_or_io.path)
156
+ elsif file_path_or_io.is_a?(String)
157
+ File.basename(file_path_or_io)
158
+ else
159
+ "attachment.pdf"
160
+ end
161
+ end
52
162
  end
53
163
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Rodoo
6
+ # Wrapper for Odoo's ir.attachment model.
7
+ #
8
+ # Provides methods for creating and querying file attachments linked to Odoo records.
9
+ #
10
+ # @example Create an attachment from a file path
11
+ # invoice = Rodoo::ProviderInvoice.find(42)
12
+ # Rodoo::Attachment.create_for(
13
+ # invoice, "/path/to/file.pdf", filename: "invoice.pdf", mimetype: "application/pdf"
14
+ # )
15
+ #
16
+ # @example Create an attachment from base64 data
17
+ # Rodoo::Attachment.create_from_base64(
18
+ # invoice, base64_data, filename: "doc.pdf", mimetype: "application/pdf"
19
+ # )
20
+ #
21
+ # @example List attachments for a record
22
+ # Rodoo::Attachment.for_record(invoice)
23
+ # Rodoo::Attachment.for_record(invoice, mimetype: "application/pdf")
24
+ #
25
+ class Attachment < Model
26
+ model_name "ir.attachment"
27
+
28
+ # Create an attachment for a record from a file path or IO object
29
+ #
30
+ # @param record [Model] The Odoo record to attach the file to
31
+ # @param file_path_or_io [String, IO, #read] File path or IO-like object
32
+ # @param filename [String] The filename for the attachment
33
+ # @param mimetype [String] The MIME type of the file
34
+ # @return [Attachment] The created attachment
35
+ #
36
+ # @example From file path
37
+ # Rodoo::Attachment.create_for(
38
+ # invoice, "/path/to/file.pdf", filename: "invoice.pdf", mimetype: "application/pdf"
39
+ # )
40
+ #
41
+ # @example From IO object
42
+ # File.open("/path/to/file.pdf", "rb") do |f|
43
+ # Rodoo::Attachment.create_for(
44
+ # invoice, f, filename: "invoice.pdf", mimetype: "application/pdf"
45
+ # )
46
+ # end
47
+ #
48
+ def self.create_for(record, file_path_or_io, filename:, mimetype:)
49
+ data = read_file_data(file_path_or_io)
50
+ base64_data = Base64.strict_encode64(data)
51
+ create_from_base64(record, base64_data, filename: filename, mimetype: mimetype)
52
+ end
53
+
54
+ # Create an attachment for a record from base64-encoded data
55
+ #
56
+ # @param record [Model] The Odoo record to attach the file to
57
+ # @param base64_data [String] The base64-encoded file data
58
+ # @param filename [String] The filename for the attachment
59
+ # @param mimetype [String] The MIME type of the file
60
+ # @return [Attachment] The created attachment
61
+ #
62
+ # @example
63
+ # Rodoo::Attachment.create_from_base64(
64
+ # invoice, base64_content, filename: "doc.pdf", mimetype: "application/pdf"
65
+ # )
66
+ #
67
+ def self.create_from_base64(record, base64_data, filename:, mimetype:)
68
+ attrs = {
69
+ name: filename,
70
+ type: "binary",
71
+ datas: base64_data,
72
+ res_model: record.class.model_name,
73
+ res_id: record.id,
74
+ mimetype: mimetype
75
+ }
76
+ ids = execute("create", vals_list: [attrs])
77
+ # Don't call find() - reading ir.attachment fails when Odoo tries to return
78
+ # the binary datas field. Return an instance with the known attributes.
79
+ new(attrs.except(:datas).merge(id: ids.first))
80
+ end
81
+
82
+ # Find all attachments for a record
83
+ #
84
+ # @param record [Model] The Odoo record to find attachments for
85
+ # @param mimetype [String, nil] Optional MIME type filter
86
+ # @return [Array<Attachment>] Array of attachments
87
+ #
88
+ # @example Get all attachments
89
+ # Rodoo::Attachment.for_record(invoice)
90
+ #
91
+ # @example Get only PDF attachments
92
+ # Rodoo::Attachment.for_record(invoice, mimetype: "application/pdf")
93
+ #
94
+ def self.for_record(record, mimetype: nil)
95
+ domain = [
96
+ ["res_model", "=", record.class.model_name],
97
+ ["res_id", "=", record.id]
98
+ ]
99
+ domain << ["mimetype", "=", mimetype] if mimetype
100
+ where(domain)
101
+ end
102
+
103
+ # Read file data from a path or IO object
104
+ #
105
+ # @param file_path_or_io [String, IO, #read] File path or IO-like object
106
+ # @return [String] Binary file data
107
+ # @api private
108
+ def self.read_file_data(file_path_or_io)
109
+ if file_path_or_io.respond_to?(:read)
110
+ file_path_or_io.read
111
+ else
112
+ File.binread(file_path_or_io)
113
+ end
114
+ end
115
+ private_class_method :read_file_data
116
+ end
117
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rodoo
4
+ class Tax < Model
5
+ model_name "account.tax"
6
+ end
7
+ end
data/lib/rodoo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rodoo
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/rodoo.rb CHANGED
@@ -12,6 +12,7 @@ module Rodoo
12
12
  autoload :AccountingEntryLine, "rodoo/models/accounting_entry_line"
13
13
  autoload :AnalyticAccount, "rodoo/models/analytic_account"
14
14
  autoload :AnalyticPlan, "rodoo/models/analytic_plan"
15
+ autoload :Attachment, "rodoo/models/attachment"
15
16
  autoload :CustomerCreditNote, "rodoo/models/customer_credit_note"
16
17
  autoload :CustomerInvoice, "rodoo/models/customer_invoice"
17
18
  autoload :JournalEntry, "rodoo/models/journal_entry"
@@ -20,6 +21,7 @@ module Rodoo
20
21
  autoload :Project, "rodoo/models/project"
21
22
  autoload :ProviderCreditNote, "rodoo/models/provider_credit_note"
22
23
  autoload :ProviderInvoice, "rodoo/models/provider_invoice"
24
+ autoload :Tax, "rodoo/models/tax"
23
25
 
24
26
  @configuration = nil
25
27
  @connection = nil
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodoo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serrano
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
12
26
  description: This gem implements a wrapper to interact with Odoo's API in Ruby. The
13
27
  API used is Odoo's 'external JSON-2 API' introduced in Odoo v19.
14
28
  email:
@@ -35,6 +49,7 @@ files:
35
49
  - lib/rodoo/models/accounting_entry_line.rb
36
50
  - lib/rodoo/models/analytic_account.rb
37
51
  - lib/rodoo/models/analytic_plan.rb
52
+ - lib/rodoo/models/attachment.rb
38
53
  - lib/rodoo/models/contact.rb
39
54
  - lib/rodoo/models/customer_credit_note.rb
40
55
  - lib/rodoo/models/customer_invoice.rb
@@ -43,6 +58,7 @@ files:
43
58
  - lib/rodoo/models/project.rb
44
59
  - lib/rodoo/models/provider_credit_note.rb
45
60
  - lib/rodoo/models/provider_invoice.rb
61
+ - lib/rodoo/models/tax.rb
46
62
  - lib/rodoo/version.rb
47
63
  homepage: https://github.com/dekuple/rodoo
48
64
  licenses: