Odoo - load a form view from new button in tree header - javascript

I created two new buttons in JS and replaced the original Add button in tree view of a model:
ListView.include({
render_buttons: function() {
var self = this;
this._super.apply(this, arguments)
if (this.model=='account.cash'){
if (this.$buttons) {
this.$buttons.find('.o_list_button_income').on('click', this.proxy('do_new_income'))
this.$buttons.find('.o_list_button_add').css({"display":"none"})
}
}
},
do_new_income: function () {
var model = new Model('account.cash');
var self = this;
model.call('new_income', [[]])
With this button I´m trying to call a form view by following method:
#api.model
def new_income(self):
view_id = self.env.ref('account.view_cash_statement_form').id
context = self._context.copy()
return {
'name': 'New income',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'res_model': 'account.cash',
'context': context,
'target': 'current',
'type': 'ir.actions.act_window'
}
However this is not working. When I try to call this method as a test in other view from button defined in standard XML view definition, it is behaving as expected.

model.call('new_income', [[]]).then(function (res) { self.do_action(res)})
where res is the returned value from python did the trick

Related

Can't open records from list view in js action odoo16

I have a custom button near list view and opened the action from javascript with target:new and it worked when record is single as i have passed the id of the record and it opened the form view on button click but when there are multiple records, I have opened the tree view and displayed all records in case of multiple and clicked on record but doesn't opened the form view of the particular record.
here is my code:-
odoo.define('module_name.file_name', function (require) {
"use strict";
var ListController = require('web.ListController');
var ListView = require('web.ListView');
var viewRegistry = require('web.view_registry');
var rpc = require('web.rpc');
var test = [];
var count_doc = 0;
var TreeButton = ListController.extend({
buttons_template: 'button_template.buttons',
events: _.extend({}, ListController.prototype.events, {
'click .button_class': '_OpenWizardAttendance',
}),
_OpenWizardAttendance: function () {
var self = this;
rpc.query({
model: 'hr.attendance',
method: 'get_attendance_id',
args: [],
}).then (function(res){
alert(res)
test = res['id'];
count_doc = res['count']
if (count_doc ==1){
self.do_action({ //action for single record
type: 'ir.actions.act_window',
res_model: 'res.model.model',
name :'Help',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
res_id: test,
})
}
else{
self.do_action({ //action for multiple records
name: _("Help"),
type: 'ir.actions.act_window',
res_model: 'res.model.model',
view_mode: 'form,tree',
view_type: 'form',
views: [[false, 'list'],[false, 'form']],
target: 'new',
domain: [],
},{on_reverse_breadcrumb: function(){ return self.reload();}})
}
});
}
});
var SaleOrderListView = ListView.extend({
config: _.extend({}, ListView.prototype.config, {
Controller: TreeButton,
}),
});
viewRegistry.add('button_in_tree', SaleOrderListView);
});
How can i open the particular record on click event?
I have tried to pass current_target event but doesn't work for me.
Also when i am passing target: current it opens the form view in that case.
Do am I missing something? Can anyone suggest me a path to solve this?

Uncaught Type Error: View is not a constructor

I have Uncaught Type Error : UserRegisterView is not a constructor.I dont understand this error.I looked all code but i dont find it.
Sorry of my bad english.Please help me
Thanks for answer
UPDATED
UserRegisterView is here
var UserRegisterView = Backbone.View.extend({
model: User,
el: '#form',
events: {
'click input[id="infoWeek"]': 'infoWeek',
'click input[id="infoMonth"]': 'infoMonth'
},
infoWeek: function() {
this.$el.find("#dayOfMonth").hide();
this.render();
},
infoMonth: function() {
this.$el.find("#dayOfWeek").hide();
this.render();
}
});
var AddUserView = Backbone.View.extend({
el: $(".page"),
events: {
'click #saveUser': 'saveUser'
},
saveUser: function() {
var user = new User();
user.set({
username: $("#username").val(),
lastName: $("#lastName").val(),
regNumber: $("#regNumber").val(),
password: $("#password").val(),
departmentName: $("#departmentName").val(),
email: $("#email").val(),
role: $("#role").val()
});
user.save();
if (document.getElementById('isOpen').checked) {
user.set("isOpen", $("#isOpen").val("1"));
user.save();
} else {
user.set("isOpen", $("#isOpen").val("0"));
user.save();
}
if (document.getElementById('dayOfWeek').checked) {
user.set("dayOfWeek", $("#dayOfWeek").val());
user.save();
} else if (document.getElementById('dayOfMonth').checked) {
user.set("dayOfMonth", $("#dayOfMonth").val());
user.save();
}
$("#username").val("");
$("#firstName").val("");
$("#lastName").val("");
$("#regNumber").val("");
$("#password").val("");
$("#deparmentName").val("");
$("#email").val("");
$("#isOpen").val("");
$("#dayOfWeek").val("");
$("#dayOfMonth").val("");
},
render: function() {
var that = this;
var template = Handlebars.compile(UserRegister);
var myHtml = template(that.model.toJSON());
that.$el.html(myHtml);
return this;
}
});
return {
AddUserView: AddUserView,
UserRegisterView: UserRegisterView
};
});
router user func.
define([
'jquery',
'underscore',
'backbone',
'handlebars',
'spin',
'app/models/LoginModel',
'app/views/LoginView',
'app/views/UserRegisterView'
], function($,
_,
Backbone,
Handlebars,
Spinner,
Login,
LoginView,
UserRegisterView
) {
var Router = Backbone.Router.extend({
routes: {
'search': 'search',
'login': 'login',
'travels': 'travels',
'user': 'user',
'menu': 'menu',
'': 'home'
},
user: function() {
disposeView(new UserRegisterView().render());
}
dispose.view on util.js
function disposeView(view) {
Backbone.View.prototype.close = function() {
this.unbind();
this.undelegateEvents();
};
/* Şu anki viewi yok et */
if (this.currentView !== undefined) {
this.currentView.close();
}
/* Yeni view oluştur. */
this.currentView = view;
this.currentView.delegateEvents();
return this.currentView;
}
What's happening
Your UserRegisterView module returns an object which contains two constructors.
return {
AddUserView: AddUserView,
UserRegisterView: UserRegisterView
};
When using this module, what you're getting is the object above.
define([
// ...
'app/views/UserRegisterView'
], function(
// ...
UserRegisterView // value of the return in the module
) {
So you're kind of misleading yourself by calling it UserRegisterView as it's not the constructor, but the object containing the constructor.
To get a new UserRegisterView view instance with the current way your module is setup, you'd need to call it like so:
var userView = new UserRegisterView.UserRegisterView();
Or to create a AddUserView instance:
var addView = new UserRegisterView.AddUserView();
Solutions
Split up the module, one for each view constructor.
Change the name so at least it's not misleading (like UserViewsModule)
Other improvements
That being said, there are other improvements that could be made to your Backbone code.
var UserRegisterView = Backbone.View.extend({
// that's useless (if not used) and not a view property.
// model: User,
// don't use `el` like that, especially when using the view as a shared Constructor
el: '#form',
events: {
'click input[id="infoWeek"]': 'onInfoWeekClick',
'click input[id="infoMonth"]': 'onInfoMonthClick'
},
initialize: function() {
// Cache jQuery object of the view's element
this.$dayOfMonth = this.$("#dayOfMonth");
this.$dayOfMonth = this.$("#dayOfMonth");
// also use the shortcut function instead of `this.$el.find()`
}
onInfoWeekClick: function(e) {
this.$dayOfMonth.hide();
// calling render here is useless unless your using it as a parent
// view, where the child view overrides the render function.
},
onInfoMonthClick: function(e) {
this.$dayOfMonth.hide();
}
});
The disposeView function could be simplified:
function disposeView(view) {
var current = this.currentView;
if (current) current.close();
current = this.currentView = view;
current.delegateEvents();
return current;
}
Don't change the default Backbone view prototype each time the function is called. Instead, add the function once.
_.extend(Backbone.View.prototype, {
close: function() {
this.unbind();
this.undelegateEvents();
},
// any other function you want to add can go here.
});
In another answer, I go into details on how to extend Backbone's core classes with requirejs transparently.
You're already using jQuery, so don't use JavaScript DOM API document.getElementById('isOpen') interspersed with jQuery selectors $('#isOpen').
I made some improvements to the following view. Take the time to create yourself some utility functions (like reset and getValues) to simplify the flow of the code and encapsulate the complexity.
var AddUserView = Backbone.View.extend({
el: $(".page"),
events: {
'click #saveUser': 'saveUser'
},
// compile the template once while creating the view class
template: Handlebars.compile(UserRegister),
// get the selector string out of the code and place them in one place
// easy to change and maintain.
fields: {
username: "#username",
firstName: "#firstName",
lastName: "#lastName",
regNumber: "#regNumber",
password: "#password",
deparmentName: "#deparmentName",
email: "#email",
isOpen: "#isOpen",
dayOfWeek: "#dayOfWeek",
dayOfMonth: "#dayOfMonth",
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
// cache jQuery object of every field once after a render
this.field = _.reduce(this.fields, function(fields, selector, key) {
fields['$' + key] = this.$(selector);
return fields;
}, {}, this);
return this;
},
reset: function() {
// reset all the fields once without repeating code.
_.each(this.field, function($field) {
$field.val("");
});
return this;
},
getValues: function(keys) {
// get the value of multiple fields returned in a nice object
// ready to be sent to a Backbone model.
return _.reduce(keys, function(data, key) {
data[key] = this.field[key].val();
return data;
}, {}, this);
},
saveUser: function() {
var field = this.field,
user = new User(this.getValues([
'username',
'lastName',
'regNumber',
'password',
'departmentName',
'email',
'role',
]));
user.set({ isOpen: field.$isOpen.is(':checked') });
if (field.$dayOfWeek.is(':checked')) {
user.set("dayOfWeek", field.$dayOfWeek.val());
} else if (field.$dayOfMonth.is(':checked')) {
user.set("dayOfMonth", field.$dayOfMonth.val());
}
user.save();
this.reset();
},
});
In the following snippet, you're putting the context (this) into a local variable. I see that a lot and I could say that 90% of the times I see it on Stack Overflow questions, it makes no sense. It clearly screams copy-pasted.
render: function() {
var that = this;
// ...
that.$el.html(myHtml);
return this;
}
Please tell me you see that you're putting this into that, then using that throughout the function, then you still return this?!
Putting the context into a local variable is useful when the object is needed in a dynamically created callback.
render: function() {
var that = this; // this is available here
setTimeout(function() {
// here this is not available.
that.handleCallback();
}, 10);
// here we are in the same context as the first line.
return this;
}

How to update part of a template when a collection is updated?

I'm trying to update only a part of a template used by a Backbone view when a collection is updated. The code below successfully executes the search() and render_list() methods. Furthermore, console.log(html) shows the full template's html. But when I execute the replaceWith, it replaces the selector with empty. If I replace $(selector,html) with a string (ie: 'test'), it successfully replaces with 'test'.
So, for some reason, the $(selector, html) selector isn't doing what it's meant to. What is further weird is that the images within the updated html selector are requested by the browser even though none of the updated html is inserted on into the document.
define([
'jquery',
'underscore',
'backbone',
'collections/tracks',
'collections/genres',
'text!templates/search_view_title.html',
'text!templates/search_view.html'
],function($,_,Backbone,Tracks_collection,Genres_collection,Search_view_title_template,Search_view_template){
var Search_view = Backbone.View.extend({
el: $('#app'),
events: {
'change #genre_select': function(){this.search('genre')},
'click #search_btn': function(){this.search('search')}
},
template: _.template(Search_view_template),
initialize: function(){
// SET SOME IMPORTANT LAYOUT SETTINGS
$('#pagetitle').html(_.template(Search_view_title_template));
$('body').css('padding-top','124px');
this.genre_collection = new Genres_collection();
this.listenTo(this.genre_collection,'update',this.render);
this.genre_collection.fetch();
this.collection = new Tracks_collection();
this.listenTo(this.collection,'update',this.render_list);
},
search: function(searchtype){
switch(searchtype){
case 'genre':
console.log('genre changed');
this.collection.fetch({
data: {limit: 30, type:'genre',genre_id:$('#genre_select').val()}
});
break;
case 'search':
console.log('search changed');
this.collection.fetch({
data: {limit: 30, type:'search',keyword:$('#keyword').val()}
});
break;
}
console.log(this.collection);
},
render_list: function(){
var that = this;
console.log('render list');
var html = that.template({genres: this.genre_collection.models,tracks: this.collection.models});
console.log(html);
var selector = '#tracklist';
console.log($(selector,html));
that.$el.find(selector).replaceWith($(selector,html));
return this;
},
render: function(){
// MAKE 'THIS' ACCESSIBLE
var that = this;
console.log('render');
that.$el.find('#container').html(that.template({genres: this.genre_collection.models}));
return this;
}
});
return Search_view;
});
Without the HTML templates in hand, I can just assume things.
This is closer to how I would do it:
var Search_view = Backbone.View.extend({
el: $('#app'),
events: {
'change #genre_select': 'onGenreChange',
'click #search_btn': 'onSearchClick'
},
template: _.template(Search_view_template),
initialize: function() {
// SET SOME IMPORTANT LAYOUT SETTINGS
$('#pagetitle').html(Search_view_title_template);
// Do this in css
$('body').css('padding-top', '124px');
this.genre_collection = new Genres_collection();
this.genre_collection.fetch();
this.collection = new Tracks_collection();
this.listenTo(this.genre_collection, 'update', this.render);
this.listenTo(this.collection, 'update', this.render_list);
},
render: function() {
console.log('render');
this.$('#container').html(this.template({
genres: this.genre_collection.models
}));
return this;
},
render_list: function() {
console.log('render list');
var html = this.template({
genres: this.genre_collection.models,
tracks: this.collection.models
});
console.log(html);
var $selector = this.$('#tracklist');
console.log($selector);
$selector.replaceWith($(html));
return this;
},
////////////
// events //
////////////
onSearchClick: function() {
console.log('search changed');
this.collection.fetch({
data: { limit: 30, type: 'search', keyword: $('#keyword').val() }
});
},
onGenreChange: function() {
console.log('genre changed');
this.collection.fetch({
data: { limit: 30, type: 'genre', genre_id: $('#genre_select').val() }
});
},
});
$('#pagetitle').html(_.template(Search_view_title_template));
The _.template function returns a function, which itself returns the rendered template when called.
It can be confused with this.template which often contains the result of _.template and is ready to be called (this.template(data)).
Split your callbacks, functions are cheap and unnecessary switch are ugly.
I made your search into onGenreChange and onSearchClick.
$('body').css('padding-top','124px');
Try to avoid that, it can be easily done with CSS, or even inline <style> tag or inline style="" attribute. If it's necessary for you as it's related to a dynamic behavior, create a class (e.g. search-class) in a css file, then toggle the class with jQuery, moving the "design" responsability out of the js:
$('body').toggleClass('search-class');
var that = this;
This is only necessary when dealing with callbacks where the context (this) is different in the callback. In Backbone, most of the time, it's avoidable as the context option is often available and automatically set on most (like the events callbacks).
this.$el.find(selector)
This is equivalent to this.$(selector). Just a little shortcut.
.replaceWith($(selector,html));
replaceWith expects a htmlString or Element or Array or jQuery.
$(selector, html) expects a selector and a context. You want $(html) to transform your html string into a jQuery element.

Rendering the view returns undefined

I've got a collection view with two filter methods, and a render method which takes a parameter. The problem I'm stuck with is that when rendering the view for the first time it returns me an error. Here's my collection:
var ResumeCollection = Backbone.Collection.extend({
url: 'http://localhost:3000',
filterActive: function () {
var active = this.where({interviewed: false});
return new ResumeCollection(active);
},
filterInterviewed: function () {
var interviewed = this.where({interviewed: true});
return new ResumeCollection(interviewed);
}
});
And my view:
var ResumeList = Backbone.View.extend({
events { // hash array of filter events },
initialize: function () {
this.collection.fetch();
},
render: function (filtered) {
var self = this;
var data;
if (!filtered) {
data = this.collection.toArray();
} else {
data = filtered.toArray();
}
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
},
showActive: function (ev) {
var filtered = this.collection.filterActive();
this.render(filtered);
},
showInterviewed: function (ev) {
var filtered = this.collection.filterInterviewed();
this.render(filtered);
},
showAll: function (ev) {
this.render(this.collection);
}
});
This view gets rendered for the first time in my router by passing a collection:
var AppRouter = Backbone.Router.extend({
routes: {
'': 'home'
},
initialize: function () {
this.layout = new LayoutView();
}
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
});
And this is the layout view within which all the other views are rendered:
var LayoutView = Backbone.View.extend({
el: $('#outlet'),
render: function (view) {
if (this.child && this.child !== view) {
this.child.undelegateEvents();
}
this.child = view;
this.child.setElement(this.$el).render();
return this;
}
});
When I just refresh my page, I get filtered.toArray is not a function error and nothing is rendered respectively. After inspecting everything in the debugger, I found out that when the view gets rendered for the first time, the filtered attribute receives an empty collection, assigns it to data variable, which becomes an empty array and goes to the body of render function, becoming undefined after that. The mysteries go here: whenever I click items, that are bound to my show* events, they act exactly as expected and render either models where interviewed === false, or true or the whole collection. This looks kinda magic to me and I haven't got the faintest idea what can I do with that.
ADDED: GitHub repo with this project
Your home function on the AppRouter has a typo. You have an extra semi-colon.
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection();
}));
}
Should be
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
I needed to remove it to get the JSFiddle working: https://jsfiddle.net/4gyne5ev/1/
I'd recommend adding some kind of linting tool into your IDE or Build process (http://eslint.org/)
You need to add home url content to your db.json file like this
"" : [
{
'somthing': 'somthing'
}
]
After a piece of advice from my mentor I realized that the core of the problem was in asynchronous origin of fetch method -- as I passed this.collection.fetch in my initialize function, it executed after my render method, not before it, so my render method had just nothing to render when the view was called for the first time. So, this fix worked:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection();
// removed .fetch() method from here
},
render: function (filtered) {
var self = this;
var data;
// and added it here:
this.collection.fetch({
success: function (collection) {
if (!filtered) {
data = collection.toArray();
} else {
data = filtered.toArray();
}
self.$el.html(self.template(collection.toJSON()));
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
})
}
});
}
});
And this worked perfectly and exactly as I needed.

What is the best way to add model in a collection from view?

I have a Backbone Marionette app with Router and a Controller. In my app you can view a collection of texts (index route with collection fetching from server), can view existing collection of texts (indexPage route without fetching from server) and can create a new text (form route). Views of list texts and create form are different from each other and changes in region.
I want to add a successully saved model to a collection and then redirect to indexPage route, but what is the best way to get a texts collection from _FormView success callback? Or how to restruct an app to do it simple?
I can send event to a controller with Backbone.Radio but want to deal without it.
Routes
router.processAppRoutes(controller, {
'': 'index',
'index': 'indexPage',
'create': 'form'
});
Controller
_Controller = Marionette.Controller.extend({
initialize: function () {
this.list = new _MainTexts();
},
index: function () {
if (!_.size(this.list)) {
var
self = this;
this.list.fetch({
success: function (collection, response, options) {
self.indexPage();
return;
}
});
}
this.indexPage();
},
indexPage: function () {
var
textsView = new _TextsView({
collection: this.list
});
application.getRegion('contentRegion').show(textsView);
},
form: function () {
var
formView = new _FormView({
model: new _MainText()
});
application.getRegion('contentRegion').show(formView);
}
});
Views
_TextView = Marionette.ItemView.extend({
className: 'item text',
template: function (serialized_model) {
return _.template('<p><%= texts[0].text %></p>')(serialized_model);
}
});
_TextsView = Marionette.CollectionView.extend({
className: 'clearfix',
childView: _TextView
});
Form view
_FormView = Marionette.ItemView.extend({
template: '#form-template',
ui: {
text: 'textarea[name="text"]',
submit: 'button[type="submit"]'
},
events: {
'click #ui.submit': 'submitForm'
},
submitForm: function (event) {
event.preventDefault();
this.model.set({
text: this.ui.text.val()
});
this.model.save({}, {
success: function (model, response, options) {
???
}
});
}
});
Ok, my problem solution is here. In controller action "form" I create event listener
var
formView = new _FormView({
model: model
});
formView.on('formSave', function (model) {
if (id == null) {
self.list.add(model);
}
...
});
Then in form view I trigger event
this.model.save({}, {
success: function (model, response, options) {
if (response.state.success) {
self.trigger('formSave', model);
}
}
});
That's all:)

Categories

Resources