summaryrefslogtreecommitdiff
path: root/base_accounting_kit/static/src/js/payment_matching.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 17:14:58 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 17:14:58 +0700
commit1ca3b3df3421961caec3b747a364071c80f5c7da (patch)
tree6778a1f0f3f9b4c6e26d6d87ccde16e24da6c9d6 /base_accounting_kit/static/src/js/payment_matching.js
parentb57188be371d36d96caac4b8d65a40745c0e972c (diff)
initial commit
Diffstat (limited to 'base_accounting_kit/static/src/js/payment_matching.js')
-rw-r--r--base_accounting_kit/static/src/js/payment_matching.js505
1 files changed, 505 insertions, 0 deletions
diff --git a/base_accounting_kit/static/src/js/payment_matching.js b/base_accounting_kit/static/src/js/payment_matching.js
new file mode 100644
index 0000000..80a1732
--- /dev/null
+++ b/base_accounting_kit/static/src/js/payment_matching.js
@@ -0,0 +1,505 @@
+odoo.define('base_accounting_kit.ReconciliationClientAction', function (require) {
+"use strict";
+
+var AbstractAction = require('web.AbstractAction');
+var ReconciliationModel = require('base_accounting_kit.ReconciliationModel');
+var ReconciliationRenderer = require('base_accounting_kit.ReconciliationRenderer');
+var core = require('web.core');
+var QWeb = core.qweb;
+
+
+/**
+ * Widget used as action for 'account.bank.statement' reconciliation
+ */
+var StatementAction = AbstractAction.extend({
+ hasControlPanel: true,
+ withSearchBar: true,
+ loadControlPanel: true,
+ title: core._t('Bank Reconciliation'),
+ contentTemplate: 'reconciliation',
+ custom_events: {
+ change_mode: '_onAction',
+ change_filter: '_onAction',
+ change_offset: '_onAction',
+ change_partner: '_onAction',
+ add_proposition: '_onAction',
+ remove_proposition: '_onAction',
+ update_proposition: '_onAction',
+ create_proposition: '_onAction',
+ getPartialAmount: '_onActionPartialAmount',
+ quick_create_proposition: '_onAction',
+ partial_reconcile: '_onAction',
+ validate: '_onValidate',
+ close_statement: '_onCloseStatement',
+ load_more: '_onLoadMore',
+ reload: 'reload',
+ search: '_onSearch',
+ navigation_move:'_onNavigationMove',
+ },
+ config: _.extend({}, AbstractAction.prototype.config, {
+ // used to instantiate the model
+ Model: ReconciliationModel.StatementModel,
+ // used to instantiate the action interface
+ ActionRenderer: ReconciliationRenderer.StatementRenderer,
+ // used to instantiate each widget line
+ LineRenderer: ReconciliationRenderer.LineRenderer,
+ // used context params
+ params: ['statement_line_ids'],
+ // number of statements/partners/accounts to display
+ defaultDisplayQty: 10,
+ // number of moves lines displayed in 'match' mode
+ limitMoveLines: 15,
+ }),
+
+ _onNavigationMove: function (ev) {
+ var non_reconciled_keys = _.keys(_.pick(this.model.lines, function(value, key, object) {return !value.reconciled}));
+ var currentIndex = _.indexOf(non_reconciled_keys, ev.data.handle);
+ var widget = false;
+ switch (ev.data.direction) {
+ case 'up':
+ ev.stopPropagation();
+ widget = this._getWidget(non_reconciled_keys[currentIndex-1]);
+ break;
+ case 'down':
+ ev.stopPropagation();
+ widget = this._getWidget(non_reconciled_keys[currentIndex+1]);
+ break;
+ case 'validate':
+ ev.stopPropagation();
+ widget = this._getWidget(non_reconciled_keys[currentIndex]);
+ widget.$('caption .o_buttons button:visible').click();
+ break;
+ }
+ if (widget) widget.$el.focus();
+ },
+
+ /**
+ * @override
+ * @param {Object} params
+ * @param {Object} params.context
+ *
+ */
+ init: function (parent, params) {
+ this._super.apply(this, arguments);
+ this.action_manager = parent;
+ this.params = params;
+ this.searchModelConfig.modelName = 'account.bank.statement.line';
+ this.controlPanelProps.cp_content = {};
+// this.controlPanelParams.modelName = 'account.bank.statement.line';
+ this.model = new this.config.Model(this, {
+ modelName: "account.reconciliation.widget",
+ defaultDisplayQty: params.params && params.params.defaultDisplayQty || this.config.defaultDisplayQty,
+ limitMoveLines: params.params && params.params.limitMoveLines || this.config.limitMoveLines,
+ });
+ this.widgets = [];
+ // Adding values from the context is necessary to put this information in the url via the action manager so that
+ // you can retrieve it if the person shares his url or presses f5
+ _.each(params.params, function (value, name) {
+ params.context[name] = name.indexOf('_ids') !== -1 ? _.map((value+'').split(','), parseFloat) : value;
+ });
+ params.params = {};
+ _.each(this.config.params, function (name) {
+ if (params.context[name]) {
+ params.params[name] = params.context[name];
+ }
+ });
+ },
+
+ /**
+ * instantiate the action renderer
+ *
+ * @override
+ */
+ willStart: function () {
+ var self = this;
+ var def = this.model.load(this.params.context).then(this._super.bind(this));
+ return def.then(function () {
+ if (!self.model.context || !self.model.context.active_id) {
+ self.model.context = {'active_id': self.params.context.active_id,
+ 'active_model': self.params.context.active_model};
+ }
+ var journal_id = self.params.context.journal_id;
+ if (self.model.context.active_id && self.model.context.active_model === 'account.journal') {
+ journal_id = journal_id || self.model.context.active_id;
+ }
+ if (journal_id) {
+ var promise = self._rpc({
+ model: 'account.journal',
+ method: 'read',
+ args: [journal_id, ['display_name']],
+ });
+ } else {
+ var promise = Promise.resolve();
+ }
+ return promise.then(function (result) {
+ var title = result && result[0] ? result[0]['display_name'] : self.params.display_name || ''
+ self._setTitle(title);
+ self.renderer = new self.config.ActionRenderer(self, self.model, {
+ 'bank_statement_line_id': self.model.bank_statement_line_id,
+ 'valuenow': self.model.valuenow,
+ 'valuemax': self.model.valuemax,
+ 'defaultDisplayQty': self.model.defaultDisplayQty,
+ 'title': title,
+ });
+ });
+ });
+ },
+
+ reload: function() {
+ // On reload destroy all rendered line widget, reload data and then rerender widget
+ var self = this;
+
+ self.$('.o_reconciliation_lines').addClass('d-none'); // prevent the browser from recomputing css after each destroy for HUGE perf improvement on a lot of lines
+ _.each(this.widgets, function(widget) {
+ widget.destroy();
+ });
+ this.widgets = [];
+ self.$('.o_reconciliation_lines').removeClass('d-none');
+ return this.model.reload().then(function() {
+ return self._renderLinesOrRainbow();
+ });
+ },
+
+ _renderLinesOrRainbow: function() {
+ var self = this;
+ return self._renderLines().then(function() {
+ var initialState = self.renderer._initialState;
+ var valuenow = self.model.statement ? self.model.statement.value_min : initialState.valuenow;
+ var valuemax = self.model.statement ? self.model.statement.value_max : initialState.valuemax;
+ // No more lines to reconcile, trigger the rainbowman.
+ if(valuenow === valuemax){
+ initialState.valuenow = valuenow;
+ initialState.context = self.model.getContext();
+ self.renderer.showRainbowMan(initialState);
+ self.remove_cp();
+ }else{
+ // Create a notification if some lines have been reconciled automatically.
+ if(initialState.valuenow > 0)
+ self.renderer._renderNotifications(self.model.statement.notifications);
+ self._openFirstLine();
+ self.renderer.$('[data-toggle="tooltip"]').tooltip();
+ self.do_show();
+ }
+ });
+ },
+
+ /**
+ * append the renderer and instantiate the line renderers
+ *
+ * @override
+ */
+ start: function () {
+ var self = this;
+ var args = arguments;
+ var sup = this._super;
+
+ return this.renderer.prependTo(self.$('.o_form_sheet')).then(function() {
+ return self._renderLinesOrRainbow().then(function() {
+ self.do_show();
+ return sup.apply(self, args);
+ });
+ });
+ },
+
+ /**
+ * update the control panel and breadcrumbs
+ *
+ * @override
+ */
+ do_show: function () {
+ this._super.apply(this, arguments);
+ if (this.action_manager) {
+ this.$pager = $(QWeb.render('reconciliation.control.pager', {widget: this.renderer}));
+ this.controlPanelProps.cp_content = {$pager: this.$pager};
+// this.updateControlPanel({
+// clear: true,
+// cp_content: {
+// $pager: this.$pager,
+// },
+// });
+ this.renderer.$progress = this.$pager;
+ $(this.renderer.$progress).parent().css('width', '100%').css('padding-left', '0');
+ }
+ },
+
+ remove_cp: function() {
+ this.controlPanelProps.cp_content = {};
+// this.updateControlPanel({
+// clear: true,
+// });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {string} handle
+ * @returns {Widget} widget line
+ */
+ _getWidget: function (handle) {
+ return _.find(this.widgets, function (widget) {return widget.handle===handle;});
+ },
+
+ /**
+ *
+ */
+ _loadMore: function(qty) {
+ var self = this;
+ return this.model.loadMore(qty).then(function () {
+ return self._renderLines();
+ });
+ },
+ /**
+ * sitch to 'match' the first available line
+ *
+ * @private
+ */
+ _openFirstLine: function (previous_handle) {
+ var self = this;
+ previous_handle = previous_handle || 'rline0';
+ var handle = _.compact(_.map(this.model.lines, function (line, handle) {
+ return (line.reconciled || (parseInt(handle.substr(5)) < parseInt(previous_handle.substr(5)))) ? null : handle;
+ }))[0];
+ if (handle) {
+ var line = this.model.getLine(handle);
+ this.model.changeMode(handle, 'default').then(function () {
+ self._getWidget(handle).update(line);
+ }).guardedCatch(function(){
+ self._getWidget(handle).update(line);
+ }).then(function() {
+ self._getWidget(handle).$el.focus();
+ }
+ );
+ }
+ return handle;
+ },
+
+ _forceUpdate: function() {
+ var self = this;
+ _.each(this.model.lines, function(handle) {
+ var widget = self._getWidget(handle['handle']);
+ if (widget && handle.need_update) {
+ widget.update(handle);
+ widget.need_update = false;
+ }
+ })
+ },
+ /**
+ * render line widget and append to view
+ *
+ * @private
+ */
+ _renderLines: function () {
+ var self = this;
+ var linesToDisplay = this.model.getStatementLines();
+ var linePromises = [];
+ _.each(linesToDisplay, function (line, handle) {
+ var widget = new self.config.LineRenderer(self, self.model, line);
+ widget.handle = handle;
+ self.widgets.push(widget);
+ linePromises.push(widget.appendTo(self.$('.o_reconciliation_lines')));
+ });
+ if (this.model.hasMoreLines() === false) {
+ this.renderer.hideLoadMoreButton(true);
+ }
+ else {
+ this.renderer.hideLoadMoreButton(false);
+ }
+ return Promise.all(linePromises);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * dispatch on the camelcased event name to model method then update the
+ * line renderer with the new state. If the mode was switched from 'inactive'
+ * to 'create' or 'match_rp' or 'match_other', the other lines switch to
+ * 'inactive' mode
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onAction: function (event) {
+ var self = this;
+ var handle = event.target.handle;
+ var current_line = this.model.getLine(handle);
+ this.model[_.str.camelize(event.name)](handle, event.data.data).then(function () {
+ var widget = self._getWidget(handle);
+ if (widget) {
+ widget.update(current_line);
+ }
+ if (current_line.mode !== 'inactive') {
+ _.each(self.model.lines, function (line, _handle) {
+ if (line.mode !== 'inactive' && _handle !== handle) {
+ self.model.changeMode(_handle, 'inactive');
+ var widget = self._getWidget(_handle);
+ if (widget) {
+ widget.update(line);
+ }
+ }
+ });
+ }
+ });
+ },
+
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onSearch: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+ this.model.domain = ev.data.domain;
+ this.model.display_context = 'search';
+ self.reload().then(function() {
+ self.renderer._updateProgressBar({
+ 'valuenow': self.model.valuenow,
+ 'valuemax': self.model.valuemax,
+ });
+ });
+ },
+
+ _onActionPartialAmount: function(event) {
+ var self = this;
+ var handle = event.target.handle;
+ var line = this.model.getLine(handle);
+ var amount = this.model.getPartialReconcileAmount(handle, event.data);
+ self._getWidget(handle).updatePartialAmount(event.data.data, amount);
+ },
+
+ /**
+ * call 'closeStatement' model method
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onCloseStatement: function (event) {
+ var self = this;
+ return this.model.closeStatement().then(function (result) {
+ self.do_action({
+ name: 'Bank Statements',
+ res_model: 'account.bank.statement.line',
+ res_id: result,
+ views: [[false, 'form']],
+ type: 'ir.actions.act_window',
+ view_mode: 'form',
+ });
+ $('.o_reward').remove();
+ });
+ },
+ /**
+ * Load more statement and render them
+ *
+ * @param {OdooEvent} event
+ */
+ _onLoadMore: function (event) {
+ return this._loadMore(this.model.defaultDisplayQty);
+ },
+ /**
+ * call 'validate' model method then destroy the
+ * validated lines and update the action renderer with the new status bar
+ * values and notifications then open the first available line
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onValidate: function (event) {
+ var self = this;
+ var handle = event.target.handle;
+ this.model.validate(handle).then(function (result) {
+ self.renderer.update({
+ 'valuenow': self.model.valuenow,
+ 'valuemax': self.model.valuemax,
+ 'title': self.title,
+ 'time': Date.now()-self.time,
+ 'notifications': result.notifications,
+ 'context': self.model.getContext(),
+ });
+ self._forceUpdate();
+ _.each(result.handles, function (handle) {
+ var widget = self._getWidget(handle);
+ if (widget) {
+ widget.destroy();
+ var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
+ self.widgets.splice(index, 1);
+ }
+ });
+ // Get number of widget and if less than constant and if there are more to laod, load until constant
+ if (self.widgets.length < self.model.defaultDisplayQty
+ && self.model.valuemax - self.model.valuenow >= self.model.defaultDisplayQty) {
+ var toLoad = self.model.defaultDisplayQty - self.widgets.length;
+ self._loadMore(toLoad);
+ }
+ self._openFirstLine(handle);
+ });
+ },
+});
+
+
+/**
+ * Widget used as action for 'account.move.line' and 'res.partner' for the
+ * manual reconciliation and mark data as reconciliate
+ */
+var ManualAction = StatementAction.extend({
+ title: core._t('Journal Items to Reconcile'),
+ withSearchBar: false,
+ config: _.extend({}, StatementAction.prototype.config, {
+ Model: ReconciliationModel.ManualModel,
+ ActionRenderer: ReconciliationRenderer.ManualRenderer,
+ LineRenderer: ReconciliationRenderer.ManualLineRenderer,
+ params: ['company_ids', 'mode', 'partner_ids', 'account_ids'],
+ defaultDisplayQty: 30,
+ limitMoveLines: 15,
+ }),
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * call 'validate' model method then destroy the
+ * reconcilied lines, update the not reconcilied and update the action
+ * renderer with the new status bar values and notifications then open the
+ * first available line
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onValidate: function (event) {
+ var self = this;
+ var handle = event.target.handle;
+ var method = 'validate';
+ this.model[method](handle).then(function (result) {
+ _.each(result.reconciled, function (handle) {
+ self._getWidget(handle).destroy();
+ });
+ _.each(result.updated, function (handle) {
+ self._getWidget(handle).update(self.model.getLine(handle));
+ });
+ self.renderer.update({
+ valuenow: _.compact(_.invoke(self.widgets, 'isDestroyed')).length,
+ valuemax: self.widgets.length,
+ title: self.title,
+ time: Date.now()-self.time,
+ });
+ if(!_.any(result.updated, function (handle) {
+ return self.model.getLine(handle).mode !== 'inactive';
+ })) {
+ self._openFirstLine(handle);
+ }
+ });
+ },
+});
+
+core.action_registry.add('bank_statement_reconciliation_view', StatementAction);
+core.action_registry.add('manual_reconciliation_view', ManualAction);
+
+return {
+ StatementAction: StatementAction,
+ ManualAction: ManualAction,
+};
+});