Reputation: 5229
I have this models
class Book < ActiveRecord::Base
has_many :accountings
has_many :commodities, through: :accountings
end
class Accounting < ActiveRecord::Base
belongs_to :book
belongs_to :commodity
end
class Commodity < ActiveRecord::Base
has_many :accountings
end
My book form is:
<%= form_for(book) do |f| %>
<%= render 'shared/error_messages', object: book %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :commodity_ids %>
<%= f.collection_select :commodity_ids, Commodity.all, :id, :name, {}, {multiple: true} %>
<br />
<%= f.submit class: 'btn btn-primary' %>
<% end %>
At the BooksController:
def new
@book = Book.new
end
def create
@book = Book.new(book_params)
if @book.save
redirect_to @book
else
render 'new'
end
end
def edit
@book = Book.find(params[:id])
end
def update
@book = Book.find(params[:id])
if @book.update_attributes(book_params)
redirect_to @book
else
render 'edit'
end
end
private
def book_params
params.require(:book).permit(:name, commodity_ids: [])
end
All this works fine. And Accounting
records are added and deleted when commodity_ids
are updated. (1)
Now I need to add a new model: Company
, since Book
and Commodity
are shared by all companies, Accounting
must belongs to company
, and also not shared at all the system. And Accounting
becomes:
class Accounting < ActiveRecord::Base
belongs_to :book
belongs_to :commodity
belongs_to :company
end
The Company
model:
class Company < ActiveRecord::Base
has_many :accountings
end
Accounting
is more than a relation between commodities and books (and companies), it represent a business model too.
The constraint here is: When a new Commodity
is added to a Book
, then a new Accounting
must be created for each Company
. (2)
I did try to relate Book
with companies
, through accountings
. But, it does not work. Even it does not represent the business model and Book don't care about Company, I think Book is a good candidate for link the models. (3)
Now I'm thinking add a new model BookCommodity
, that relate books and commodities through this model, and on save this new model generate the Accounting
records needed for all companies
. (4)
Before add this fifth model I want ask you if there are a better way to manage this stuff?
Edited
At Github you can find a demo_finance project, with only the code of this post. It has 4 branchs:
The key task here is add or remove commodities to a book, and get accountings updated, just like it works at version (1) (master branch).
Edit #2
I tried to do this:
class Book < ActiveRecord::Base
has_many :accountings
has_many :commodity_companies, through: :accountings
has_many :commodities, through: :commodity_companies
end
but it doesn't work. On update it raises:
ActiveRecord::HasManyThroughNestedAssociationsAreReadonly
Cannot modify association 'Book#commodities' because it goes through more than one other association.
I also try to do this:
class Book < ActiveRecord::Base
has_many :accountings do
def build(attributes = {}, &block)
Company.all.each do |company|
@association.build(attributes.merge(company_id: company.id), &block)
end
end
end
has_many :commodities, through: :accountings
end
But, this build action is not called on update books with commodity_ids=
.
Upvotes: 9
Views: 795
Reputation: 4820
This is a perfect application for the acts_as_tenant gem. I have this gem running in a production application and it is an excellent solution. You can basically take your original working solution and replicate it for each company within your system just by using this gem. Each company's data would be isolated to its own system within a single database.
The documentation is excellent but I would be happy to answer specific questions about the implementation. I combine this gem with Sorcery and Rolify for authentication and authorization, but that's up to you. I can have many companies on one image. In your case, your firm could easily switch to different companies but you could allow individual companies access to only their data.
It's an elegant but simple solution to such a complex issue.
Upvotes: 0
Reputation: 5229
Answering my own question:
I did not select this answer as the right one yet. Because, although it works, I think there could be better answers, which would deserve the bounty.
I made it work overwriting the method commodity_ids=
, also is needed to add dependent: :destroy
option to accountings, and scope -> {uniq}
to commodities.
class Book < ActiveRecord::Base
has_many :accountings, dependent: :destroy
has_many :commodities, ->{ uniq }, through: :accountings
def commodity_ids=(ids)
self.accountings = Commodity.where(id: ids).map do |commodity|
Company.all.map do |company|
accountings.find_by(company_id: company.id, commodity_id: commodity.id) ||
accountings.build(company: company, commodity: commodity)
end
end.flatten
end
def commodities=(records)
self.commodity_ids = records.map(&:id)
end
end
class Commodity < ActiveRecord::Base
end
class Company < ActiveRecord::Base
end
class Accounting < ActiveRecord::Base
belongs_to :book
belongs_to :commodity
belongs_to :company
def to_s
"#{book.name} - #{company.name} - #{commodity.name}"
end
end
Running at the console from scratch:
~/ (main) > Book.create name: 'Book 1'
=> #<Book id: 1, name: "Book 1", created_at: "2015-09-30 11:47:23", updated_at: "2015-09-30 11:47:23">
~/ (main) > Commodity.create name: 'Commodity 1'
=> #<Commodity id: 1, name: "Commodity 1", created_at: "2015-09-30 11:47:37", updated_at: "2015-09-30 11:47:37">
~/ (main) > Commodity.create name: 'Commodity 2'
=> #<Commodity id: 2, name: "Commodity 2", created_at: "2015-09-30 11:47:40", updated_at: "2015-09-30 11:47:40">
~/ (main) > Commodity.create name: 'Commodity 3'
=> #<Commodity id: 3, name: "Commodity 3", created_at: "2015-09-30 11:47:42", updated_at: "2015-09-30 11:47:42">
~/ (main) > Company.create name: 'Company 1'
=> #<Company id: 1, name: "Company 1", created_at: "2015-09-30 11:47:51", updated_at: "2015-09-30 11:47:51">
~/ (main) > Company.create name: 'Company 2'
=> #<Company id: 2, name: "Company 2", created_at: "2015-09-30 11:47:54", updated_at: "2015-09-30 11:47:54">
~/ (main) > bb = Book.first
=> #<Book id: 1, name: "Book 1", created_at: "2015-09-30 11:47:23", updated_at: "2015-09-30 11:47:23">
~/ (main) > bb.commodity_ids = ['', nil, 1, '3']
=> [
[0] "",
[1] nil,
[2] 1,
[3] "3"
]
~/ (main) > bb.save
=> true
~/ (main) > bb.reload
=> #<Book id: 1, name: "Book 1", created_at: "2015-09-30 11:47:23", updated_at: "2015-09-30 11:47:23">
~/ (main) > bb.accountings.map(&:to_s)
=> [
[0] "Book 1 - Company 1 - Commodity 1",
[1] "Book 1 - Company 2 - Commodity 1",
[2] "Book 1 - Company 1 - Commodity 3",
[3] "Book 1 - Company 2 - Commodity 3"
]
~/ (main) > bb.commodities.map(&:name)
=> [
[0] "Commodity 1",
[1] "Commodity 3"
]
~/ (main) > bb.commodity_ids = ['']
=> [
[0] ""
]
~/ (main) > bb.save
=> true
~/ (main) > bb.reload
=> #<Book id: 1, name: "Book 1", created_at: "2015-09-30 11:47:23", updated_at: "2015-09-30 11:47:23">
~/ (main) > bb.commodities.map(&:name)
=> []
~/ (main) > bb.accountings.map(&:to_s)
=> []
~/ (main) >
Upvotes: 0
Reputation: 5105
It's only a solution. After, I've learned deeply your questions. I'd say you have to add all companies in your book form as multy-select options. I knew perhaps it will make it out of your idea now but you have to follow about rails flow. You've said that you want to update commodities through accounting based on companies and then you want to update data automatically but your code don't provide to do these.
OPTION 1
This is the suggestion:
Controller
Add new strong parameter
def book_params
params.require(:book).permit(:name, commodity_ids: [], company_ids: [])
end
Model
You need to revised some associations
class Book < ActiveRecord::Base
has_many :accountings
has_many :commodities, through: :accountings
has_many :companies, through: :accountings
end
class Accounting < ActiveRecord::Base
belongs_to :book
belongs_to :commodity
belongs_to :company
end
class Commodity < ActiveRecord::Base
has_many :accountings
has_many :companies, through: :accountings
has_many :books, through: :accountings
end
class Company < ActiveRecord::Base
has_many :accountings
has_many :books through: :accountings
has_many :commodites through: :accountings
end
View
You can select all companies as selected in order to user don't need to select options (optional)
<%= form_for(book) do |f| %>
<%= render 'shared/error_messages', object: book %>
<%= f.label :company_ids %>
<%= f.collection_select :company_ids, Company.all, :id, :name, {:selected: Company.all.map(&:id)}, {multiple: true} %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :commodity_ids %>
<%= f.collection_select :commodity_ids, Commodity.all, :id, :name, {}, {multiple: true} %>
<br />
<%= f.submit class: 'btn btn-primary' %>
<% end %>
As addition, try not to create BookCommodity
to create Accounting
because BookCommodity
is equal to Accounting
OPTION 2
For another option if you want to not show in your form you can add in your model in before_save callback. This is only
before_save :set_companies
def set_companies
self.company_ids = Company.all.map(&:id)
end
Conclusion:
Those are having pros and cons. In first options, user can select the company but the user need to choose the company first. Last one, the user don't choose the companies but he will get difficulties when the user want to update the company of the book or commodity. As always, I would prefer to you to use the first one way because the user will easy to edit. That's all. I hope this can help you.
Upvotes: 0