diff options
Diffstat (limited to 'base_accounting_kit/static/src/js/payment_model.js')
| -rw-r--r-- | base_accounting_kit/static/src/js/payment_model.js | 1881 |
1 files changed, 1881 insertions, 0 deletions
diff --git a/base_accounting_kit/static/src/js/payment_model.js b/base_accounting_kit/static/src/js/payment_model.js new file mode 100644 index 0000000..07785c4 --- /dev/null +++ b/base_accounting_kit/static/src/js/payment_model.js @@ -0,0 +1,1881 @@ +odoo.define('base_accounting_kit.ReconciliationModel', function (require) { +"use strict"; + +var BasicModel = require('web.BasicModel'); +var field_utils = require('web.field_utils'); +var utils = require('web.utils'); +var session = require('web.session'); +var WarningDialog = require('web.CrashManager').WarningDialog; +var core = require('web.core'); +var _t = core._t; + + +/** + * Model use to fetch, format and update 'account.reconciliation.widget', + * datas allowing reconciliation + * + * The statement internal structure:: + * + * { + * valuenow: integer + * valuenow: valuemax + * [bank_statement_line_id]: { + * id: integer + * display_name: string + * } + * reconcileModels: [object] + * accounts: {id: code} + * } + * + * The internal structure of each line is:: + * + * { + * balance: { + * type: number - show/hide action button + * amount: number - real amount + * amount_str: string - formated amount + * account_code: string + * }, + * st_line: { + * partner_id: integer + * partner_name: string + * } + * mode: string ('inactive', 'match_rp', 'match_other', 'create') + * reconciliation_proposition: { + * id: number|string + * partial_amount: number + * invalid: boolean - through the invalid line (without account, label...) + * account_code: string + * date: string + * date_maturity: string + * label: string + * amount: number - real amount + * amount_str: string - formated amount + * [already_paid]: boolean + * [partner_id]: integer + * [partner_name]: string + * [account_code]: string + * [journal_id]: { + * id: integer + * display_name: string + * } + * [ref]: string + * [is_partially_reconciled]: boolean + * [to_check]: boolean + * [amount_currency_str]: string|false (amount in record currency) + * } + * mv_lines_match_rp: object - idem than reconciliation_proposition + * mv_lines_match_other: object - idem than reconciliation_proposition + * limitMoveLines: integer + * filter: string + * [createForm]: { + * account_id: { + * id: integer + * display_name: string + * } + * tax_ids: { + * id: integer + * display_name: string + * } + * analytic_account_id: { + * id: integer + * display_name: string + * } + * analytic_tag_ids: { + * } + * label: string + * amount: number, + * [journal_id]: { + * id: integer + * display_name: string + * } + * } + * } + */ +var StatementModel = BasicModel.extend({ + avoidCreate: false, + quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_ids', 'force_tax_included', 'analytic_tag_ids', 'to_check'], + + // overridden in ManualModel + modes: ['create', 'match_rp', 'match_other'], + + /** + * @override + * + * @param {Widget} parent + * @param {object} options + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.reconcileModels = []; + this.lines = {}; + this.valuenow = 0; + this.valuemax = 0; + this.alreadyDisplayed = []; + this.domain = []; + this.defaultDisplayQty = options && options.defaultDisplayQty || 10; + this.limitMoveLines = options && options.limitMoveLines || 15; + this.display_context = 'init'; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * add a reconciliation proposition from the matched lines + * We also display a warning if the user tries to add 2 line with different + * account type + * + * @param {string} handle + * @param {number} mv_line_id + * @returns {Promise} + */ + addProposition: function (handle, mv_line_id) { + var self = this; + var line = this.getLine(handle); + var prop = _.clone(_.find(line['mv_lines_'+line.mode], {'id': mv_line_id})); + this._addProposition(line, prop); + line['mv_lines_'+line.mode] = _.filter(line['mv_lines_'+line.mode], l => l['id'] != mv_line_id); + + // remove all non valid lines + line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (prop) {return prop && !prop.invalid;}); + + // Onchange the partner if not already set on the statement line. + if(!line.st_line.partner_id && line.reconciliation_proposition + && line.reconciliation_proposition.length == 1 && prop.partner_id && line.type === undefined){ + return this.changePartner(handle, {'id': prop.partner_id, 'display_name': prop.partner_name}, true); + } + + return Promise.all([ + this._computeLine(line), + this._performMoveLine(handle, 'match_rp', line.mode == 'match_rp'? 1 : 0), + this._performMoveLine(handle, 'match_other', line.mode == 'match_other'? 1 : 0) + ]); + }, + /** + * change the filter for the target line and fetch the new matched lines + * + * @param {string} handle + * @param {string} filter + * @returns {Promise} + */ + changeFilter: function (handle, filter) { + var line = this.getLine(handle); + line['filter_'+line.mode] = filter; + line['mv_lines_'+line.mode] = []; + return this._performMoveLine(handle, line.mode); + }, + /** + * change the mode line ('inactive', 'match_rp', 'match_other', 'create'), + * and fetch the new matched lines or prepare to create a new line + * + * ``match_rp`` + * display the matched lines from receivable/payable accounts, the user + * can select the lines to apply there as proposition + * ``match_other`` + * display the other matched lines, the user can select the lines to apply + * there as proposition + * ``create`` + * display fields and quick create button to create a new proposition + * for the reconciliation + * + * @param {string} handle + * @param {'inactive' | 'match_rp' | 'create'} mode + * @returns {Promise} + */ + changeMode: function (handle, mode) { + var self = this; + var line = this.getLine(handle); + if (mode === 'default') { + var match_requests = self.modes.filter(x => x.startsWith('match')).map(x => this._performMoveLine(handle, x)) + return Promise.all(match_requests).then(function() { + return self.changeMode(handle, self._getDefaultMode(handle)); + }); + } + if (mode === 'next') { + var available_modes = self._getAvailableModes(handle) + mode = available_modes[(available_modes.indexOf(line.mode) + 1) % available_modes.length]; + } + line.mode = mode; + if (['match_rp', 'match_other'].includes(line.mode)) { + if (!(line['mv_lines_' + line.mode] && line['mv_lines_' + line.mode].length)) { + return this._performMoveLine(handle, line.mode); + } else { + return this._formatMoveLine(handle, line.mode, []); + } + } + if (line.mode === 'create') { + return this.createProposition(handle); + } + return Promise.resolve(); + }, + /** + * fetch the more matched lines + * + * @param {string} handle + * @returns {Promise} + */ + changeOffset: function (handle) { + var line = this.getLine(handle); + return this._performMoveLine(handle, line.mode); + }, + /** + * change the partner on the line and fetch the new matched lines + * + * @param {string} handle + * @param {bool} preserveMode + * @param {Object} partner + * @param {string} partner.display_name + * @param {number} partner.id + * @returns {Promise} + */ + changePartner: function (handle, partner, preserveMode) { + var self = this; + var line = this.getLine(handle); + line.st_line.partner_id = partner && partner.id; + line.st_line.partner_name = partner && partner.display_name || ''; + line.mv_lines_match_rp = []; + line.mv_lines_match_other = []; + return Promise.resolve(partner && this._changePartner(handle, partner.id)) + .then(function() { + if(line.st_line.partner_id){ + _.each(line.reconciliation_proposition, function(prop){ + if(prop.partner_id != line.st_line.partner_id){ + line.reconciliation_proposition = []; + return false; + } + }); + } + return self._computeLine(line); + }) + .then(function () { + return self.changeMode(handle, preserveMode ? line.mode : 'default', true); + }) + + }, + /** + * close the statement + * @returns {Promise<number>} resolves to the res_id of the closed statements + */ + closeStatement: function () { + var self = this; + return this._rpc({ + model: 'account.bank.statement.line', + method: 'button_confirm_bank', + args: [self.bank_statement_line_id.id], + }) + .then(function () { + return self.bank_statement_line_id.id; + }); + }, + /** + * + * then open the first available line + * + * @param {string} handle + * @returns {Promise} + */ + createProposition: function (handle) { + var line = this.getLine(handle); + var prop = _.filter(line.reconciliation_proposition, '__focus'); + prop = this._formatQuickCreate(line); + line.reconciliation_proposition.push(prop); + line.createForm = _.pick(prop, this.quickCreateFields); + return this._computeLine(line); + }, + /** + * Return context information and journal_id + * @returns {Object} context + */ + getContext: function () { + return this.context; + }, + /** + * Return the lines that needs to be displayed by the widget + * + * @returns {Object} lines that are loaded and not yet displayed + */ + getStatementLines: function () { + var self = this; + var linesToDisplay = _.pick(this.lines, function(value, key, object) { + if (value.visible === true && self.alreadyDisplayed.indexOf(key) === -1) { + self.alreadyDisplayed.push(key); + return object; + } + }); + return linesToDisplay; + }, + /** + * Return a boolean telling if load button needs to be displayed or not + * overridden in ManualModel + * + * @returns {boolean} true if load more button needs to be displayed + */ + hasMoreLines: function () { + var notDisplayed = _.filter(this.lines, function(line) { return !line.visible; }); + if (notDisplayed.length > 0) { + return true; + } + return false; + }, + /** + * get the line data for this handle + * + * @param {Object} handle + * @returns {Object} + */ + getLine: function (handle) { + return this.lines[handle]; + }, + /** + * load data from + * + * - 'account.bank.statement' fetch the line id and bank_statement_id info + * - 'account.reconcile.model' fetch all reconcile model (for quick add) + * - 'account.account' fetch all account code + * - 'account.reconciliation.widget' fetch each line data + * + * overridden in ManualModel + * @param {Object} context + * @param {number[]} context.statement_line_ids + * @returns {Promise} + */ + load: function (context) { + var self = this; + this.context = context; + this.statement_line_ids = context.statement_line_ids; + if (this.statement_line_ids === undefined) { + // This could be undefined if the user pressed F5, take everything as fallback instead of rainbowman + return self._rpc({ + model: 'account.bank.statement.line', + method: 'search_read', + fields: ['id'], + domain: [['journal_id', '=?', context.active_id]], + }).then(function (result) { + self.statement_line_ids = result.map(r => r.id); + return self.reload() + }) + } else { + return self.reload(); + } + + }, + /** + * Load more bank statement line + * + * @param {integer} qty quantity to load + * @returns {Promise} + */ + loadMore: function(qty) { + if (qty === undefined) { + qty = this.defaultDisplayQty; + } + var ids = _.pluck(this.lines, 'id'); + ids = ids.splice(this.pagerIndex, qty); + this.pagerIndex += qty; + return this.loadData(ids, this._getExcludedIds()); + }, + /** + * RPC method to load informations on lines + * overridden in ManualModel + * + * @param {Array} ids ids of bank statement line passed to rpc call + * @param {Array} excluded_ids list of move_line ids that needs to be excluded from search + * @returns {Promise} + */ + loadData: function(ids) { + var self = this; + var excluded_ids = this._getExcludedIds(); + return self._rpc({ + model: 'account.reconciliation.widget', + method: 'get_bank_statement_line_data', + args: [ids, excluded_ids], + context: self.context, + }) + .then(function(res){ + return self._formatLine(res['lines']); + }) + }, + /** + * Reload all data + */ + reload: function() { + var self = this; + self.alreadyDisplayed = []; + self.lines = {}; + self.pagerIndex = 0; + var def_statement = this._rpc({ + model: 'account.reconciliation.widget', + method: 'get_bank_statement_data', + kwargs: {"bank_statement_line_ids":self.statement_line_ids, "srch_domain":self.domain}, + context: self.context, + }) + .then(function (statement) { + self.statement = statement; + self.bank_statement_line_id = self.statement_line_ids.length === 1 ? {id: self.statement_line_ids[0], display_name: statement.statement_name} : false; + self.valuenow = self.valuenow || statement.value_min; + self.valuemax = self.valuemax || statement.value_max; + self.context.journal_id = statement.journal_id; + _.each(statement.lines, function (res) { + var handle = _.uniqueId('rline'); + self.lines[handle] = { + id: res.st_line.id, + partner_id: res.st_line.partner_id, + handle: handle, + reconciled: false, + mode: 'inactive', + mv_lines_match_rp: [], + mv_lines_match_other: [], + filter_match_rp: "", + filter_match_other: "", + reconciliation_proposition: [], + reconcileModels: [], + }; + }); + }); + var domainReconcile = []; + if (self.context && self.context.company_ids) { + domainReconcile.push(['company_id', 'in', self.context.company_ids]); + } + if (self.context && self.context.active_model === 'account.journal' && self.context.active_ids) { + domainReconcile.push('|'); + domainReconcile.push(['match_journal_ids', '=', false]); + domainReconcile.push(['match_journal_ids', 'in', self.context.active_ids]); + } + var def_reconcileModel = this._loadReconciliationModel({domainReconcile: domainReconcile}); + var def_account = this._rpc({ + model: 'account.account', + method: 'search_read', + fields: ['code'], + }) + .then(function (accounts) { + self.accounts = _.object(_.pluck(accounts, 'id'), _.pluck(accounts, 'code')); + }); + var def_taxes = self._loadTaxes(); + return Promise.all([def_statement, def_reconcileModel, def_account, def_taxes]).then(function () { + _.each(self.lines, function (line) { + line.reconcileModels = self.reconcileModels; + }); + var ids = _.pluck(self.lines, 'id'); + ids = ids.splice(0, self.defaultDisplayQty); + self.pagerIndex = ids.length; + return self._formatLine(self.statement.lines); + }); + }, + _readAnalyticTags: function (params) { + var self = this; + this.analyticTags = {}; + if (!params || !params.res_ids || !params.res_ids.length) { + return $.when(); + } + var fields = (params && params.fields || []).concat(['id', 'display_name']); + return this._rpc({ + model: 'account.analytic.tag', + method: 'read', + args: [ + params.res_ids, + fields, + ], + }).then(function (tags) { + for (var i=0; i<tags.length; i++) { + var tag = tags[i]; + self.analyticTags[tag.id] = tag; + } + }); + }, + _loadReconciliationModel: function (params) { + var self = this; + return this._rpc({ + model: 'account.reconcile.model', + method: 'search_read', + domain: params.domainReconcile || [], + }) + .then(function (reconcileModels) { + var analyticTagIds = []; + for (var i=0; i<reconcileModels.length; i++) { + var modelTags = reconcileModels[i].analytic_tag_ids || []; + for (var j=0; j<modelTags.length; j++) { + if (analyticTagIds.indexOf(modelTags[j]) === -1) { + analyticTagIds.push(modelTags[j]); + } + } + } + return self._readAnalyticTags({res_ids: analyticTagIds}).then(function () { + for (var i=0; i<reconcileModels.length; i++) { + var recModel = reconcileModels[i]; + var analyticTagData = []; + var modelTags = reconcileModels[i].analytic_tag_ids || []; + for (var j=0; j<modelTags.length; j++) { + var tagId = modelTags[j]; + analyticTagData.push([tagId, self.analyticTags[tagId].display_name]) + } + recModel.analytic_tag_ids = analyticTagData; + } + self.reconcileModels = reconcileModels; + }); + }); + }, + _loadTaxes: function(){ + var self = this; + self.taxes = {}; + return this._rpc({ + model: 'account.tax', + method: 'search_read', + fields: ['price_include', 'name'], + }).then(function (taxes) { + _.each(taxes, function(tax){ + self.taxes[tax.id] = { + price_include: tax.price_include, + display_name: tax.name, + }; + }); + return taxes; + }); + }, + /** + * Add lines into the propositions from the reconcile model + * Can add 2 lines, and each with its taxes. The second line become editable + * in the create mode. + * + * @see 'updateProposition' method for more informations about the + * 'amount_type' + * + * @param {string} handle + * @param {integer} reconcileModelId + * @returns {Promise} + */ + quickCreateProposition: function (handle, reconcileModelId) { + var self = this; + var line = this.getLine(handle); + var reconcileModel = _.find(this.reconcileModels, function (r) {return r.id === reconcileModelId;}); + var fields = ['account_id', 'amount', 'amount_type', 'analytic_account_id', 'journal_id', 'label', 'force_tax_included', 'tax_ids', 'analytic_tag_ids', 'to_check', 'amount_from_label_regex', 'decimal_separator']; + this._blurProposition(handle); + var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields)); + focus.reconcileModelId = reconcileModelId; + line.reconciliation_proposition.push(focus); + var defs = []; + if (reconcileModel.has_second_line) { + defs.push(self._computeLine(line).then(function() { + var second = {}; + _.each(fields, function (key) { + second[key] = ("second_"+key) in reconcileModel ? reconcileModel["second_"+key] : reconcileModel[key]; + }); + var second_focus = self._formatQuickCreate(line, second); + second_focus.reconcileModelId = reconcileModelId; + line.reconciliation_proposition.push(second_focus); + self._computeReconcileModels(handle, reconcileModelId); + })) + } + return Promise.all(defs).then(function() { + line.createForm = _.pick(focus, self.quickCreateFields); + return self._computeLine(line); + }) + }, + /** + * Remove a proposition and switch to an active mode ('create' or 'match_rp' or 'match_other') + * overridden in ManualModel + * + * @param {string} handle + * @param {number} id (move line id) + * @returns {Promise} + */ + removeProposition: function (handle, id) { + var self = this; + var line = this.getLine(handle); + var defs = []; + var prop = _.find(line.reconciliation_proposition, {'id' : id}); + if (prop) { + line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) { + return p.id !== prop.id && p.id !== prop.link && p.link !== prop.id && (!p.link || p.link !== prop.link); + }); + if (prop['reconcileModelId'] === undefined) { + if (['receivable', 'payable', 'liquidity'].includes(prop.account_type)) { + line.mv_lines_match_rp.unshift(prop); + } else { + line.mv_lines_match_other.unshift(prop); + } + } + + // No proposition left and then, reset the st_line partner. + if(line.reconciliation_proposition.length == 0 && line.st_line.has_no_partner) + defs.push(self.changePartner(line.handle)); + } + line.mode = (id || line.mode !== "create") && isNaN(id) ? 'create' : 'match_rp'; + defs.push(this._computeLine(line)); + return Promise.all(defs).then(function() { + return self.changeMode(handle, line.mode, true); + }) + }, + getPartialReconcileAmount: function(handle, data) { + var line = this.getLine(handle); + var formatOptions = { + currency_id: line.st_line.currency_id, + noSymbol: true, + }; + var prop = _.find(line.reconciliation_proposition, {'id': data.data}); + if (prop) { + var amount = prop.partial_amount || prop.amount; + // Check if we can get a partial amount that would directly set balance to zero + var partial = Math.abs(line.balance.amount + amount); + if (Math.abs(line.balance.amount) >= Math.abs(amount)) { + amount = Math.abs(amount); + } else if (partial <= Math.abs(prop.amount) && partial >= 0) { + amount = partial; + } else { + amount = Math.abs(amount); + } + return field_utils.format.monetary(amount, {}, formatOptions); + } + }, + /** + * Force the partial reconciliation to display the reconciliate button. + * + * @param {string} handle + * @returns {Promise} + */ + partialReconcile: function(handle, data) { + var line = this.getLine(handle); + var prop = _.find(line.reconciliation_proposition, {'id' : data.mvLineId}); + if (prop) { + var amount = data.amount; + try { + amount = field_utils.parse.float(data.amount); + } + catch (err) { + amount = NaN; + } + // Amount can't be greater than line.amount and can not be negative and must be a number + // the amount we receive will be a string, so take sign of previous line amount in consideration in order to put + // the amount in the correct left or right column + if (amount >= Math.abs(prop.amount) || amount <= 0 || isNaN(amount)) { + delete prop.partial_amount_str; + delete prop.partial_amount; + if (isNaN(amount) || amount < 0) { + this.do_warn(_.str.sprintf(_t('The amount %s is not a valid partial amount'), data.amount)); + } + return this._computeLine(line); + } + else { + var format_options = { currency_id: line.st_line.currency_id }; + prop.partial_amount = (prop.amount > 0 ? 1 : -1)*amount; + prop.partial_amount_str = field_utils.format.monetary(Math.abs(prop.partial_amount), {}, format_options); + } + } + return this._computeLine(line); + }, + /** + * Change the value of the editable proposition line or create a new one. + * + * If the editable line comes from a reconcile model with 2 lines + * and their 'amount_type' is "percent" + * and their total equals 100% (this doesn't take into account the taxes + * who can be included or not) + * Then the total is recomputed to have 100%. + * + * @param {string} handle + * @param {*} values + * @returns {Promise} + */ + updateProposition: function (handle, values) { + var self = this; + var line = this.getLine(handle); + var prop = _.last(_.filter(line.reconciliation_proposition, '__focus')); + if ('to_check' in values && values.to_check === false) { + // check if we have another line with to_check and if yes don't change value of this proposition + prop.to_check = line.reconciliation_proposition.some(function(rec_prop, index) { + return rec_prop.id !== prop.id && rec_prop.to_check; + }); + } + if (!prop) { + prop = this._formatQuickCreate(line); + line.reconciliation_proposition.push(prop); + } + _.each(values, function (value, fieldName) { + if (fieldName === 'analytic_tag_ids') { + switch (value.operation) { + case "ADD_M2M": + // handle analytic_tag selection via drop down (single dict) and + // full widget (array of dict) + var vids = _.isArray(value.ids) ? value.ids : [value.ids]; + _.each(vids, function (val) { + if (!_.findWhere(prop.analytic_tag_ids, {id: val.id})) { + prop.analytic_tag_ids.push(val); + } + }); + break; + case "FORGET": + var id = self.localData[value.ids[0]].ref; + prop.analytic_tag_ids = _.filter(prop.analytic_tag_ids, function (val) { + return val.id !== id; + }); + break; + } + } + else if (fieldName === 'tax_ids') { + switch(value.operation) { + case "ADD_M2M": + prop.__tax_to_recompute = true; + var vids = _.isArray(value.ids) ? value.ids : [value.ids]; + _.each(vids, function(val){ + if (!_.findWhere(prop.tax_ids, {id: val.id})) { + value.ids.price_include = self.taxes[val.id] ? self.taxes[val.id].price_include : false; + prop.tax_ids.push(val); + } + }); + break; + case "FORGET": + prop.__tax_to_recompute = true; + var id = self.localData[value.ids[0]].ref; + prop.tax_ids = _.filter(prop.tax_ids, function (val) { + return val.id !== id; + }); + break; + } + } + else { + prop[fieldName] = values[fieldName]; + } + }); + if ('account_id' in values) { + prop.account_code = prop.account_id ? this.accounts[prop.account_id.id] : ''; + } + if ('amount' in values) { + prop.base_amount = values.amount; + if (prop.reconcileModelId) { + this._computeReconcileModels(handle, prop.reconcileModelId); + } + } + if ('force_tax_included' in values || 'amount' in values || 'account_id' in values) { + prop.__tax_to_recompute = true; + } + line.createForm = _.pick(prop, this.quickCreateFields); + // If you check/uncheck the force_tax_included box, reset the createForm amount. + if(prop.base_amount) + line.createForm.amount = prop.base_amount; + if (prop.tax_ids.length !== 1 ) { + // When we have 0 or more than 1 taxes, reset the base_amount and force_tax_included, otherwise weird behavior can happen + prop.amount = prop.base_amount; + line.createForm.force_tax_included = false; + } + return this._computeLine(line); + }, + /** + * Format the value and send it to 'account.reconciliation.widget' model + * Update the number of validated lines + * overridden in ManualModel + * + * @param {(string|string[])} handle + * @returns {Promise<Object>} resolved with an object who contains + * 'handles' key + */ + validate: function (handle) { + var self = this; + this.display_context = 'validate'; + var handles = []; + if (handle) { + handles = [handle]; + } else { + _.each(this.lines, function (line, handle) { + if (!line.reconciled && line.balance && !line.balance.amount && line.reconciliation_proposition.length) { + handles.push(handle); + } + }); + } + var ids = []; + var values = []; + var handlesPromises = []; + _.each(handles, function (handle) { + var line = self.getLine(handle); + var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.invalid;}); + var computeLinePromise; + if (props.length === 0) { + // Usability: if user has not chosen any lines and click validate, it has the same behavior + // as creating a write-off of the same amount. + props.push(self._formatQuickCreate(line, { + account_id: [line.st_line.open_balance_account_id, self.accounts[line.st_line.open_balance_account_id]], + })); + // update balance of line otherwise it won't be to zero and another line will be added + line.reconciliation_proposition.push(props[0]); + computeLinePromise = self._computeLine(line); + } + ids.push(line.id); + handlesPromises.push(Promise.resolve(computeLinePromise).then(function() { + var values_dict = { + "partner_id": line.st_line.partner_id, + "counterpart_aml_dicts": _.map(_.filter(props, function (prop) { + return !isNaN(prop.id) && !prop.already_paid; + }), self._formatToProcessReconciliation.bind(self, line)), + "payment_aml_ids": _.pluck(_.filter(props, function (prop) { + return !isNaN(prop.id) && prop.already_paid; + }), 'id'), + "new_aml_dicts": _.map(_.filter(props, function (prop) { + return isNaN(prop.id) && prop.display; + }), self._formatToProcessReconciliation.bind(self, line)), + "to_check": line.to_check, + }; + + // If the lines are not fully balanced, create an unreconciled amount. + // line.st_line.currency_id is never false here because its equivalent to + // statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side). + // see: get_statement_line_for_reconciliation_widget method in account/models/account_bank_statement.py for more details + var currency = session.get_currency(line.st_line.currency_id); + var balance = line.balance.amount; + if (!utils.float_is_zero(balance, currency.digits[1])) { + var unreconciled_amount_dict = { + 'account_id': line.st_line.open_balance_account_id, + 'credit': balance > 0 ? balance : 0, + 'debit': balance < 0 ? -balance : 0, + 'name': line.st_line.name + ' : ' + _t("Open balance"), + }; + values_dict['new_aml_dicts'].push(unreconciled_amount_dict); + } + values.push(values_dict); + line.reconciled = true; + })); + + _.each(self.lines, function(other_line) { + if (other_line != line) { + var filtered_prop = other_line.reconciliation_proposition.filter(p => !line.reconciliation_proposition.map(l => l.id).includes(p.id)); + if (filtered_prop.length != other_line.reconciliation_proposition.length) { + other_line.need_update = true; + other_line.reconciliation_proposition = filtered_prop; + } + self._computeLine(line); + } + }) + }); + + return Promise.all(handlesPromises).then(function() { + return self._rpc({ + model: 'account.reconciliation.widget', + method: 'process_bank_statement_line', + args: [ids, values], + context: self.context, + }) + .then(self._validatePostProcess.bind(self)) + .then(function () { + self.valuenow += handles.length; + return {handles: handles}; + }); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * add a line proposition after checking receivable and payable accounts constraint + * + * @private + * @param {Object} line + * @param {Object} prop + */ + _addProposition: function (line, prop) { + line.reconciliation_proposition.push(prop); + }, + /** + * stop the editable proposition line and remove it if it's invalid then + * compute the line + * + * See :func:`_computeLine` + * + * @private + * @param {string} handle + * @returns {Promise} + */ + _blurProposition: function (handle) { + var line = this.getLine(handle); + line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (l) { + l.__focus = false; + return !l.invalid; + }); + }, + /** + * When changing partner, read property_account_receivable and payable + * of that partner because the counterpart account might cahnge depending + * on the partner + * + * @private + * @param {string} handle + * @param {integer} partner_id + * @returns {Promise} + */ + _changePartner: function (handle, partner_id) { + var self = this; + return this._rpc({ + model: 'res.partner', + method: 'read', + args: [partner_id, ["property_account_receivable_id", "property_account_payable_id"]], + }).then(function (result) { + if (result.length > 0) { + var line = self.getLine(handle); + self.lines[handle].st_line.open_balance_account_id = line.balance.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0]; + } + }); + }, + /** + * Calculates the balance; format each proposition amount_str and mark as + * invalid the line with empty account_id, amount or label + * Check the taxes server side for each updated propositions with tax_ids + * extended by ManualModel + * + * @private + * @param {Object} line + * @returns {Promise} + */ + _computeLine: function (line) { + //balance_type + var self = this; + + // compute taxes + var tax_defs = []; + var reconciliation_proposition = []; + var formatOptions = { + currency_id: line.st_line.currency_id, + }; + line.to_check = false; + _.each(line.reconciliation_proposition, function (prop) { + if (prop.to_check) { + // If one of the proposition is to_check, set the global to_check flag to true + line.to_check = true; + } + if (prop.tax_repartition_line_id) { + if (!_.find(line.reconciliation_proposition, {'id': prop.link}).__tax_to_recompute) { + reconciliation_proposition.push(prop); + } + return; + } + if (!prop.already_paid && parseInt(prop.id)) { + prop.is_move_line = true; + } + reconciliation_proposition.push(prop); + + if (prop.tax_ids && prop.tax_ids.length && prop.__tax_to_recompute && prop.base_amount) { + reconciliation_proposition = _.filter(reconciliation_proposition, function (p) { + return !p.tax_repartition_line_id || p.link !== prop.id; + }); + var args = [prop.tax_ids.map(function(el){return el.id;}), prop.base_amount, formatOptions.currency_id]; + var add_context = {'round': true}; + if(prop.tax_ids.length === 1 && line.createForm && line.createForm.force_tax_included) + add_context.force_price_include = true; + tax_defs.push(self._rpc({ + model: 'account.tax', + method: 'json_friendly_compute_all', + args: args, + context: $.extend({}, self.context || {}, add_context), + }) + .then(function (result) { + _.each(result.taxes, function(tax){ + var tax_prop = self._formatQuickCreate(line, { + 'link': prop.id, + 'tax_ids': tax.tax_ids, + 'tax_repartition_line_id': tax.tax_repartition_line_id, + 'tag_ids': tax.tag_ids, + 'amount': tax.amount, + 'label': prop.label ? prop.label + " " + tax.name : tax.name, + 'date': prop.date, + 'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id, + 'analytic': tax.analytic, + '__focus': false + }); + + prop.tax_exigible = tax.tax_exigibility === 'on_payment' ? true : undefined; + prop.amount = tax.base; + prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions); + prop.invalid = !self._isValid(prop); + + tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, formatOptions); + tax_prop.invalid = prop.invalid; + + reconciliation_proposition.push(tax_prop); + }); + + prop.tag_ids = result.base_tags; + })); + } else { + prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions); + prop.display = self._isDisplayedProposition(prop); + prop.invalid = !self._isValid(prop); + } + }); + + return Promise.all(tax_defs).then(function () { + _.each(reconciliation_proposition, function (prop) { + prop.__tax_to_recompute = false; + }); + line.reconciliation_proposition = reconciliation_proposition; + + var amount_currency = 0; + var total = line.st_line.amount || 0; + var isOtherCurrencyId = _.uniq(_.pluck(_.reject(reconciliation_proposition, 'invalid'), 'currency_id')); + isOtherCurrencyId = isOtherCurrencyId.length === 1 && !total && isOtherCurrencyId[0] !== formatOptions.currency_id ? isOtherCurrencyId[0] : false; + + _.each(reconciliation_proposition, function (prop) { + if (!prop.invalid) { + total -= prop.partial_amount || prop.amount; + if (isOtherCurrencyId) { + amount_currency -= (prop.amount < 0 ? -1 : 1) * Math.abs(prop.amount_currency); + } + } + }); + var company_currency = session.get_currency(line.st_line.currency_id); + var company_precision = company_currency && company_currency.digits[1] || 2; + total = utils.round_decimals(total, company_precision) || 0; + if(isOtherCurrencyId){ + var other_currency = session.get_currency(isOtherCurrencyId); + var other_precision = other_currency && other_currency.digits[1] || 2; + amount_currency = utils.round_decimals(amount_currency, other_precision); + } + line.balance = { + amount: total, + amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions), + currency_id: isOtherCurrencyId, + amount_currency: isOtherCurrencyId ? amount_currency : total, + amount_currency_str: isOtherCurrencyId ? field_utils.format.monetary(Math.abs(amount_currency), {}, { + currency_id: isOtherCurrencyId + }) : false, + account_code: self.accounts[line.st_line.open_balance_account_id], + }; + line.balance.show_balance = line.balance.amount_currency != 0; + line.balance.type = line.balance.amount_currency ? (line.st_line.partner_id ? 0 : -1) : 1; + }); + }, + /** + * + * + * @private + * @param {string} handle + * @param {integer} reconcileModelId + */ + _computeReconcileModels: function (handle, reconcileModelId) { + var line = this.getLine(handle); + // if quick create with 2 lines who use 100%, change the both values in same time + var props = _.filter(line.reconciliation_proposition, {'reconcileModelId': reconcileModelId, '__focus': true}); + if (props.length === 2 && props[0].percent && props[1].percent) { + if (props[0].percent + props[1].percent === 100) { + props[0].base_amount = props[0].amount = line.st_line.amount - props[1].base_amount; + props[0].__tax_to_recompute = true; + } + } + }, + /** + * format a name_get into an object {id, display_name}, idempotent + * + * @private + * @param {Object|Array} [value] data or name_get + */ + _formatNameGet: function (value) { + return value ? (value.id ? value : {'id': value[0], 'display_name': value[1]}) : false; + }, + _formatMany2ManyTags: function (value) { + var res = []; + for (var i=0, len=value.length; i<len; i++) { + res[i] = {'id': value[i][0], 'display_name': value[i][1]}; + } + return res; + }, + _formatMany2ManyTagsTax: function(value) { + var res = []; + for (var i=0; i<value.length; i++) { + res.push({id: value[i], display_name: this.taxes[value[i]] ? this.taxes[value[i]].display_name : ''}); + } + return res; + }, + /** + * Format each propositions (amount, label, account_id) + * extended in ManualModel + * + * @private + * @param {Object} line + * @param {Object[]} props + */ + _formatLineProposition: function (line, props) { + var self = this; + if (props.length) { + _.each(props, function (prop) { + prop.amount = prop.debit || -prop.credit; + prop.label = prop.name; + prop.account_id = self._formatNameGet(prop.account_id || line.account_id); + prop.is_partially_reconciled = prop.amount_str !== prop.total_amount_str; + prop.to_check = !!prop.to_check; + }); + } + }, + /** + * Format each server lines and propositions and compute all lines + * overridden in ManualModel + * + * @see '_computeLine' + * + * @private + * @param {Object[]} lines + * @returns {Promise} + */ + _formatLine: function (lines) { + var self = this; + var defs = []; + _.each(lines, function (data) { + var line = _.find(self.lines, function (l) { + return l.id === data.st_line.id; + }); + line.visible = true; + line.limitMoveLines = self.limitMoveLines; + _.extend(line, data); + self._formatLineProposition(line, line.reconciliation_proposition); + if (!line.reconciliation_proposition.length) { + delete line.reconciliation_proposition; + } + + // No partner set on st_line and all matching amls have the same one: set it on the st_line. + defs.push( + self._computeLine(line) + .then(function(){ + if(!line.st_line.partner_id && line.reconciliation_proposition.length > 0){ + var hasDifferentPartners = function(prop){ + return !prop.partner_id || prop.partner_id != line.reconciliation_proposition[0].partner_id; + }; + + if(!_.any(line.reconciliation_proposition, hasDifferentPartners)){ + return self.changePartner(line.handle, { + 'id': line.reconciliation_proposition[0].partner_id, + 'display_name': line.reconciliation_proposition[0].partner_name, + }, true); + } + }else if(!line.st_line.partner_id && line.partner_id && line.partner_name){ + return self.changePartner(line.handle, { + 'id': line.partner_id, + 'display_name': line.partner_name, + }, true); + } + return true; + }) + .then(function(){ + return data.write_off ? self.quickCreateProposition(line.handle, data.model_id) : true; + }) + .then(function() { + // If still no partner set, take the one from context, if it exists + if (!line.st_line.partner_id && self.context.partner_id && self.context.partner_name) { + return self.changePartner(line.handle, { + 'id': self.context.partner_id, + 'display_name': self.context.partner_name, + }, true); + } + return true; + }) + ); + }); + return Promise.all(defs); + }, + /** + * Format the server value then compute the line + * overridden in ManualModel + * + * @see '_computeLine' + * + * @private + * @param {string} handle + * @param {Object[]} mv_lines + * @returns {Promise} + */ + _formatMoveLine: function (handle, mode, mv_lines) { + var self = this; + var line = this.getLine(handle); + line['mv_lines_'+mode] = _.uniq(line['mv_lines_'+mode].concat(mv_lines), l => l.id); + if (mv_lines[0]){ + line['remaining_'+mode] = mv_lines[0].recs_count - mv_lines.length; + } else if (line['mv_lines_'+mode].lenght == 0) { + line['remaining_'+mode] = 0; + } + this._formatLineProposition(line, mv_lines); + + if ((line.mode == 'match_other' || line.mode == "match_rp") && !line['mv_lines_'+mode].length && !line['filter_'+mode].length) { + line.mode = self._getDefaultMode(handle); + if (line.mode !== 'match_rp' && line.mode !== 'match_other' && line.mode !== 'inactive') { + return this._computeLine(line).then(function () { + return self.createProposition(handle); + }); + } + } else { + return this._computeLine(line); + } + }, + /** + * overridden in ManualModel + */ + _getDefaultMode: function(handle) { + var line = this.getLine(handle); + if (line.balance.amount === 0 + && (!line.st_line.mv_lines_match_rp || line.st_line.mv_lines_match_rp.length === 0) + && (!line.st_line.mv_lines_match_other || line.st_line.mv_lines_match_other.length === 0)) { + return 'inactive'; + } + if (line.mv_lines_match_rp && line.mv_lines_match_rp.length) { + return 'match_rp'; + } + if (line.mv_lines_match_other && line.mv_lines_match_other.length) { + return 'match_other'; + } + return 'create'; + }, + _getAvailableModes: function(handle) { + var line = this.getLine(handle); + var modes = [] + if (line.mv_lines_match_rp && line.mv_lines_match_rp.length) { + modes.push('match_rp') + } + if (line.mv_lines_match_other && line.mv_lines_match_other.length) { + modes.push('match_other') + } + modes.push('create') + return modes + }, + /** + * Apply default values for the proposition, format datas and format the + * base_amount with the decimal number from the currency + * extended in ManualModel + * + * @private + * @param {Object} line + * @param {Object} values + * @returns {Object} + */ + _formatQuickCreate: function (line, values) { + values = values || {}; + var today = new moment().utc().format(); + var account = this._formatNameGet(values.account_id); + var formatOptions = { + currency_id: line.st_line.currency_id, + }; + var amount; + switch(values.amount_type) { + case 'percentage': + amount = line.balance.amount * values.amount / 100; + break; + case 'regex': + var matching = line.st_line.name.match(new RegExp(values.amount_from_label_regex)) + amount = 0; + if (matching && matching.length == 2) { + matching = matching[1].replace(new RegExp('\\D' + values.decimal_separator, 'g'), ''); + matching = matching.replace(values.decimal_separator, '.'); + amount = parseFloat(matching) || 0; + amount = line.balance.amount > 0 ? amount : -amount; + } + break; + case 'fixed': + amount = values.amount; + break; + default: + amount = values.amount !== undefined ? values.amount : line.balance.amount; + } + + + var prop = { + 'id': _.uniqueId('createLine'), + 'label': values.label || line.st_line.name, + 'account_id': account, + 'account_code': account ? this.accounts[account.id] : '', + 'analytic_account_id': this._formatNameGet(values.analytic_account_id), + 'analytic_tag_ids': this._formatMany2ManyTags(values.analytic_tag_ids || []), + 'journal_id': this._formatNameGet(values.journal_id), + 'tax_ids': this._formatMany2ManyTagsTax(values.tax_ids || []), + 'tag_ids': values.tag_ids, + 'tax_repartition_line_id': values.tax_repartition_line_id, + 'debit': 0, + 'credit': 0, + 'date': values.date ? values.date : field_utils.parse.date(today, {}, {isUTC: true}), + 'force_tax_included': values.force_tax_included || false, + 'base_amount': amount, + 'percent': values.amount_type === "percentage" ? values.amount : null, + 'link': values.link, + 'display': true, + 'invalid': true, + 'to_check': !!values.to_check, + '__tax_to_recompute': true, + '__focus': '__focus' in values ? values.__focus : true, + }; + if (prop.base_amount) { + // Call to format and parse needed to round the value to the currency precision + var sign = prop.base_amount < 0 ? -1 : 1; + var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, formatOptions); + prop.base_amount = sign * field_utils.parse.monetary(amount, {}, formatOptions); + } + + prop.amount = prop.base_amount; + return prop; + }, + /** + * Return list of account_move_line that has been selected and needs to be removed + * from other calls. + * + * @private + * @returns {Array} list of excluded ids + */ + _getExcludedIds: function () { + var excludedIds = []; + _.each(this.lines, function(line) { + if (line.reconciliation_proposition) { + _.each(line.reconciliation_proposition, function(prop) { + if (parseInt(prop['id'])) { + excludedIds.push(prop['id']); + } + }); + } + }); + return excludedIds; + }, + /** + * Defined whether the line is to be displayed or not. Here, we only display + * the line if it comes from the server or if an account is defined when it + * is created + * extended in ManualModel + * + * @private + * @param {object} prop + * @returns {Boolean} + */ + _isDisplayedProposition: function (prop) { + return !isNaN(prop.id) || !!prop.account_id; + }, + /** + * extended in ManualModel + * @private + * @param {object} prop + * @returns {Boolean} + */ + _isValid: function (prop) { + return !isNaN(prop.id) || prop.account_id && prop.amount && prop.label && !!prop.label.length; + }, + /** + * Fetch 'account.reconciliation.widget' propositions. + * overridden in ManualModel + * + * @see '_formatMoveLine' + * + * @private + * @param {string} handle + * @returns {Promise} + */ + _performMoveLine: function (handle, mode, limit) { + limit = limit || this.limitMoveLines; + var line = this.getLine(handle); + var excluded_ids = _.map(_.union(line.reconciliation_proposition, line.mv_lines_match_rp, line.mv_lines_match_other), function (prop) { + return _.isNumber(prop.id) ? prop.id : null; + }).filter(id => id != null); + var filter = line['filter_'+mode] || ""; + return this._rpc({ + model: 'account.reconciliation.widget', + method: 'get_move_lines_for_bank_statement_line', + args: [line.id, line.st_line.partner_id, excluded_ids, filter, 0, limit, mode === 'match_rp' ? 'rp' : 'other'], + context: this.context, + }) + .then(this._formatMoveLine.bind(this, handle, mode)); + }, + /** + * format the proposition to send information server side + * extended in ManualModel + * + * @private + * @param {object} line + * @param {object} prop + * @returns {object} + */ + _formatToProcessReconciliation: function (line, prop) { + var amount = -prop.amount; + if (prop.partial_amount) { + amount = -prop.partial_amount; + } + + var result = { + name : prop.label, + debit : amount > 0 ? amount : 0, + credit : amount < 0 ? -amount : 0, + tax_exigible: prop.tax_exigible, + analytic_tag_ids: [[6, null, _.pluck(prop.analytic_tag_ids, 'id')]] + }; + if (!isNaN(prop.id)) { + result.counterpart_aml_id = prop.id; + } else { + result.account_id = prop.account_id.id; + if (prop.journal_id) { + result.journal_id = prop.journal_id.id; + } + } + if (!isNaN(prop.id)) result.counterpart_aml_id = prop.id; + if (prop.analytic_account_id) result.analytic_account_id = prop.analytic_account_id.id; + if (prop.tax_ids && prop.tax_ids.length) result.tax_ids = [[6, null, _.pluck(prop.tax_ids, 'id')]]; + + if (prop.tag_ids && prop.tag_ids.length) result.tag_ids = [[6, null, prop.tag_ids]]; + if (prop.tax_repartition_line_id) result.tax_repartition_line_id = prop.tax_repartition_line_id; + if (prop.reconcileModelId) result.reconcile_model_id = prop.reconcileModelId + return result; + }, + /** + * Hook to handle return values of the validate's line process. + * + * @private + * @param {Object} data + * @param {Object[]} data.moves list of processed account.move + * @returns {Deferred} + */ + _validatePostProcess: function (data) { + var self = this; + return Promise.resolve(); + }, +}); + + +/** + * Model use to fetch, format and update 'account.move.line' and 'res.partner' + * datas allowing manual reconciliation + */ +var ManualModel = StatementModel.extend({ + quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_ids', 'force_tax_included', 'analytic_tag_ids', 'date', 'to_check'], + + modes: ['create', 'match'], + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Return a boolean telling if load button needs to be displayed or not + * + * @returns {boolean} true if load more button needs to be displayed + */ + hasMoreLines: function () { + if (this.manualLines.length > this.pagerIndex) { + return true; + } + return false; + }, + /** + * load data from + * - 'account.reconciliation.widget' fetch the lines to reconciliate + * - 'account.account' fetch all account code + * + * @param {Object} context + * @param {string} [context.mode] 'customers', 'suppliers' or 'accounts' + * @param {integer[]} [context.company_ids] + * @param {integer[]} [context.partner_ids] used for 'customers' and + * 'suppliers' mode + * @returns {Promise} + */ + load: function (context) { + var self = this; + this.context = context; + + var domain_account_id = []; + if (context && context.company_ids) { + domain_account_id.push(['company_id', 'in', context.company_ids]); + } + + var def_account = this._rpc({ + model: 'account.account', + method: 'search_read', + domain: domain_account_id, + fields: ['code'], + }) + .then(function (accounts) { + self.account_ids = _.pluck(accounts, 'id'); + self.accounts = _.object(self.account_ids, _.pluck(accounts, 'code')); + }); + + var domainReconcile = []; + var session_allowed_company_ids = session.user_context.allowed_company_ids || [] + var company_ids = context && context.company_ids || session_allowed_company_ids.slice(0, 1); + + if (company_ids) { + domainReconcile.push(['company_id', 'in', company_ids]); + } + var def_reconcileModel = this._loadReconciliationModel({domainReconcile: domainReconcile}); + var def_taxes = this._loadTaxes(); + + return Promise.all([def_reconcileModel, def_account, def_taxes]).then(function () { + switch(context.mode) { + case 'customers': + case 'suppliers': + var mode = context.mode === 'customers' ? 'receivable' : 'payable'; + var args = ['partner', context.partner_ids || null, mode]; + return self._rpc({ + model: 'account.reconciliation.widget', + method: 'get_data_for_manual_reconciliation', + args: args, + context: context, + }) + .then(function (result) { + self.manualLines = result; + self.valuenow = 0; + self.valuemax = Object.keys(self.manualLines).length; + var lines = self.manualLines.slice(0, self.defaultDisplayQty); + self.pagerIndex = lines.length; + return self.loadData(lines); + }); + case 'accounts': + return self._rpc({ + model: 'account.reconciliation.widget', + method: 'get_data_for_manual_reconciliation', + args: ['account', context.account_ids || self.account_ids], + context: context, + }) + .then(function (result) { + self.manualLines = result; + self.valuenow = 0; + self.valuemax = Object.keys(self.manualLines).length; + var lines = self.manualLines.slice(0, self.defaultDisplayQty); + self.pagerIndex = lines.length; + return self.loadData(lines); + }); + default: + var partner_ids = context.partner_ids || null; + var account_ids = context.account_ids || self.account_ids || null; + return self._rpc({ + model: 'account.reconciliation.widget', + method: 'get_all_data_for_manual_reconciliation', + args: [partner_ids, account_ids], + context: context, + }) + .then(function (result) { + // Flatten the result + self.manualLines = [].concat(result.accounts, result.customers, result.suppliers); + self.valuenow = 0; + self.valuemax = Object.keys(self.manualLines).length; + var lines = self.manualLines.slice(0, self.defaultDisplayQty); + self.pagerIndex = lines.length; + return self.loadData(lines); + }); + } + }); + }, + + /** + * Reload data by calling load + * It overrides super.reload() because + * it is not adapted for this model. + * + * Use case: coming back to manual reconcilation + * in breadcrumb + */ + reload: function () { + this.lines = {}; + return this.load(this.context); + }, + + /** + * Load more partners/accounts + * overridden in ManualModel + * + * @param {integer} qty quantity to load + * @returns {Promise} + */ + loadMore: function(qty) { + if (qty === undefined) { + qty = this.defaultDisplayQty; + } + var lines = this.manualLines.slice(this.pagerIndex, this.pagerIndex + qty); + this.pagerIndex += qty; + return this.loadData(lines); + }, + /** + * Method to load informations on lines + * + * @param {Array} lines manualLines to load + * @returns {Promise} + */ + loadData: function(lines) { + var self = this; + var defs = []; + _.each(lines, function (l) { + defs.push(self._formatLine(l.mode, l)); + }); + return Promise.all(defs); + + }, + /** + * Mark the account or the partner as reconciled + * + * @param {(string|string[])} handle + * @returns {Promise<Array>} resolved with the handle array + */ + validate: function (handle) { + var self = this; + var handles = []; + if (handle) { + handles = [handle]; + } else { + _.each(this.lines, function (line, handle) { + if (!line.reconciled && !line.balance.amount && line.reconciliation_proposition.length) { + handles.push(handle); + } + }); + } + + var def = Promise.resolve(); + var process_reconciliations = []; + var reconciled = []; + _.each(handles, function (handle) { + var line = self.getLine(handle); + if(line.reconciled) { + return; + } + var props = line.reconciliation_proposition; + if (!props.length) { + self.valuenow++; + reconciled.push(handle); + line.reconciled = true; + process_reconciliations.push({ + id: line.type === 'accounts' ? line.account_id : line.partner_id, + type: line.type, + mv_line_ids: [], + new_mv_line_dicts: [], + }); + } else { + var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id'); + var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id) && prop.display;}), self._formatToProcessReconciliation.bind(self, line)); + process_reconciliations.push({ + id: null, + type: null, + mv_line_ids: mv_line_ids, + new_mv_line_dicts: new_mv_line_dicts + }); + } + line.reconciliation_proposition = []; + }); + if (process_reconciliations.length) { + def = self._rpc({ + model: 'account.reconciliation.widget', + method: 'process_move_lines', + args: [process_reconciliations], + }); + } + + return def.then(function() { + var defs = []; + var account_ids = []; + var partner_ids = []; + _.each(handles, function (handle) { + var line = self.getLine(handle); + if (line.reconciled) { + return; + } + line.filter_match = ""; + defs.push(self._performMoveLine(handle, 'match').then(function () { + if(!line.mv_lines_match.length) { + self.valuenow++; + reconciled.push(handle); + line.reconciled = true; + if (line.type === 'accounts') { + account_ids.push(line.account_id.id); + } else { + partner_ids.push(line.partner_id); + } + } + })); + }); + return Promise.all(defs).then(function () { + if (partner_ids.length) { + self._rpc({ + model: 'res.partner', + method: 'mark_as_reconciled', + args: [partner_ids], + }); + } + return {reconciled: reconciled, updated: _.difference(handles, reconciled)}; + }); + }); + }, + removeProposition: function (handle, id) { + var self = this; + var line = this.getLine(handle); + var defs = []; + var prop = _.find(line.reconciliation_proposition, {'id' : id}); + if (prop) { + line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) { + return p.id !== prop.id && p.id !== prop.link && p.link !== prop.id && (!p.link || p.link !== prop.link); + }); + line.mv_lines_match = line.mv_lines_match || []; + line.mv_lines_match.unshift(prop); + + // No proposition left and then, reset the st_line partner. + if(line.reconciliation_proposition.length == 0 && line.st_line.has_no_partner) + defs.push(self.changePartner(line.handle)); + } + line.mode = (id || line.mode !== "create") && isNaN(id) ? 'create' : 'match'; + defs.push(this._computeLine(line)); + return Promise.all(defs).then(function() { + return self.changeMode(handle, line.mode, true); + }) + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * override change the balance type to display or not the reconcile button + * + * @override + * @private + * @param {Object} line + * @returns {Promise} + */ + _computeLine: function (line) { + return this._super(line).then(function () { + var props = _.reject(line.reconciliation_proposition, 'invalid'); + _.each(line.reconciliation_proposition, function(p) { + delete p.is_move_line; + }); + line.balance.type = -1; + if (!line.balance.amount_currency && props.length) { + line.balance.type = 1; + } else if(_.any(props, function (prop) {return prop.amount > 0;}) && + _.any(props, function (prop) {return prop.amount < 0;})) { + line.balance.type = 0; + } + }); + }, + /** + * Format each server lines and propositions and compute all lines + * + * @see '_computeLine' + * + * @private + * @param {'customers' | 'suppliers' | 'accounts'} type + * @param {Object} data + * @returns {Promise} + */ + _formatLine: function (type, data) { + var line = this.lines[_.uniqueId('rline')] = _.extend(data, { + type: type, + reconciled: false, + mode: 'inactive', + limitMoveLines: this.limitMoveLines, + filter_match: "", + reconcileModels: this.reconcileModels, + account_id: this._formatNameGet([data.account_id, data.account_name]), + st_line: data, + visible: true + }); + this._formatLineProposition(line, line.reconciliation_proposition); + if (!line.reconciliation_proposition.length) { + delete line.reconciliation_proposition; + } + return this._computeLine(line); + }, + /** + * override to add journal_id + * + * @override + * @private + * @param {Object} line + * @param {Object} props + */ + _formatLineProposition: function (line, props) { + var self = this; + this._super(line, props); + if (props.length) { + _.each(props, function (prop) { + var tmp_value = prop.debit || prop.credit; + prop.credit = prop.credit !== 0 ? 0 : tmp_value; + prop.debit = prop.debit !== 0 ? 0 : tmp_value; + prop.amount = -prop.amount; + prop.journal_id = self._formatNameGet(prop.journal_id || line.journal_id); + prop.to_check = !!prop.to_check; + }); + } + }, + /** + * override to add journal_id on tax_created_line + * + * @private + * @param {Object} line + * @param {Object} values + * @returns {Object} + */ + _formatQuickCreate: function (line, values) { + // Add journal to created line + if (values && values.journal_id === undefined && line && line.createForm && line.createForm.journal_id) { + values.journal_id = line.createForm.journal_id; + } + return this._super(line, values); + }, + /** + * @override + * @param {object} prop + * @returns {Boolean} + */ + _isDisplayedProposition: function (prop) { + return !!prop.journal_id && this._super(prop); + }, + /** + * @override + * @param {object} prop + * @returns {Boolean} + */ + _isValid: function (prop) { + return prop.journal_id && this._super(prop); + }, + /** + * Fetch 'account.move.line' propositions. + * + * @see '_formatMoveLine' + * + * @override + * @private + * @param {string} handle + * @returns {Promise} + */ + _performMoveLine: function (handle, mode, limit) { + limit = limit || this.limitMoveLines; + var line = this.getLine(handle); + var excluded_ids = _.map(_.union(line.reconciliation_proposition, line.mv_lines_match), function (prop) { + return _.isNumber(prop.id) ? prop.id : null; + }).filter(id => id != null); + var filter = line.filter_match || ""; + var args = [line.account_id.id, line.partner_id, excluded_ids, filter, 0, limit]; + return this._rpc({ + model: 'account.reconciliation.widget', + method: 'get_move_lines_for_manual_reconciliation', + args: args, + context: this.context, + }) + .then(this._formatMoveLine.bind(this, handle, '')); + }, + + _formatToProcessReconciliation: function (line, prop) { + var result = this._super(line, prop); + result['date'] = prop.date; + return result; + }, + _getDefaultMode: function(handle) { + var line = this.getLine(handle); + if (line.balance.amount === 0 && (!line.st_line.mv_lines_match || line.st_line.mv_lines_match.length === 0)) { + return 'inactive'; + } + return line.mv_lines_match.length > 0 ? 'match' : 'create'; + }, + _formatMoveLine: function (handle, mode, mv_lines) { + var self = this; + var line = this.getLine(handle); + line.mv_lines_match = _.uniq((line.mv_lines_match || []).concat(mv_lines), l => l.id); + this._formatLineProposition(line, mv_lines); + + if (line.mode !== 'create' && !line.mv_lines_match.length && !line.filter_match.length) { + line.mode = this.avoidCreate || !line.balance.amount ? 'inactive' : 'create'; + if (line.mode === 'create') { + return this._computeLine(line).then(function () { + return self.createProposition(handle); + }); + } + } else { + return this._computeLine(line); + } + }, +}); + +return { + StatementModel: StatementModel, + ManualModel: ManualModel, +}; +}); |
