Best Python code snippet using gherkin-python
payment_model.js
Source:payment_model.js
1odoo.define('base_accounting_kit.ReconciliationModel', function (require) {2"use strict";3var BasicModel = require('web.BasicModel');4var field_utils = require('web.field_utils');5var utils = require('web.utils');6var session = require('web.session');7var WarningDialog = require('web.CrashManager').WarningDialog;8var core = require('web.core');9var _t = core._t;10/**11 * Model use to fetch, format and update 'account.reconciliation.widget',12 * datas allowing reconciliation13 *14 * The statement internal structure::15 *16 * {17 * valuenow: integer18 * valuenow: valuemax19 * [bank_statement_line_id]: {20 * id: integer21 * display_name: string22 * }23 * reconcileModels: [object]24 * accounts: {id: code}25 * }26 *27 * The internal structure of each line is::28 *29 * {30 * balance: {31 * type: number - show/hide action button32 * amount: number - real amount33 * amount_str: string - formated amount34 * account_code: string35 * },36 * st_line: {37 * partner_id: integer38 * partner_name: string39 * }40 * mode: string ('inactive', 'match_rp', 'match_other', 'create')41 * reconciliation_proposition: {42 * id: number|string43 * partial_amount: number44 * invalid: boolean - through the invalid line (without account, label...)45 * account_code: string46 * date: string47 * date_maturity: string48 * label: string49 * amount: number - real amount50 * amount_str: string - formated amount51 * [already_paid]: boolean52 * [partner_id]: integer53 * [partner_name]: string54 * [account_code]: string55 * [journal_id]: {56 * id: integer57 * display_name: string58 * }59 * [ref]: string60 * [is_partially_reconciled]: boolean61 * [to_check]: boolean62 * [amount_currency_str]: string|false (amount in record currency)63 * }64 * mv_lines_match_rp: object - idem than reconciliation_proposition65 * mv_lines_match_other: object - idem than reconciliation_proposition66 * limitMoveLines: integer67 * filter: string68 * [createForm]: {69 * account_id: {70 * id: integer71 * display_name: string72 * }73 * tax_ids: {74 * id: integer75 * display_name: string76 * }77 * analytic_account_id: {78 * id: integer79 * display_name: string80 * }81 * analytic_tag_ids: {82 * }83 * label: string84 * amount: number,85 * [journal_id]: {86 * id: integer87 * display_name: string88 * }89 * }90 * }91 */92var StatementModel = BasicModel.extend({93 avoidCreate: false,94 quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_ids', 'force_tax_included', 'analytic_tag_ids', 'to_check'],95 // overridden in ManualModel96 modes: ['create', 'match_rp', 'match_other'],97 /**98 * @override99 *100 * @param {Widget} parent101 * @param {object} options102 */103 init: function (parent, options) {104 this._super.apply(this, arguments);105 this.reconcileModels = [];106 this.lines = {};107 this.valuenow = 0;108 this.valuemax = 0;109 this.alreadyDisplayed = [];110 this.domain = [];111 this.defaultDisplayQty = options && options.defaultDisplayQty || 10;112 this.limitMoveLines = options && options.limitMoveLines || 15;113 this.display_context = 'init';114 },115 //--------------------------------------------------------------------------116 // Public117 //--------------------------------------------------------------------------118 /**119 * add a reconciliation proposition from the matched lines120 * We also display a warning if the user tries to add 2 line with different121 * account type122 *123 * @param {string} handle124 * @param {number} mv_line_id125 * @returns {Promise}126 */127 addProposition: function (handle, mv_line_id) {128 var self = this;129 var line = this.getLine(handle);130 var prop = _.clone(_.find(line['mv_lines_'+line.mode], {'id': mv_line_id}));131 this._addProposition(line, prop);132 line['mv_lines_'+line.mode] = _.filter(line['mv_lines_'+line.mode], l => l['id'] != mv_line_id);133 // remove all non valid lines134 line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (prop) {return prop && !prop.invalid;});135 // Onchange the partner if not already set on the statement line.136 if(!line.st_line.partner_id && line.reconciliation_proposition137 && line.reconciliation_proposition.length == 1 && prop.partner_id && line.type === undefined){138 return this.changePartner(handle, {'id': prop.partner_id, 'display_name': prop.partner_name}, true);139 }140 return Promise.all([141 this._computeLine(line),142 this._performMoveLine(handle, 'match_rp', line.mode == 'match_rp'? 1 : 0),143 this._performMoveLine(handle, 'match_other', line.mode == 'match_other'? 1 : 0)144 ]);145 },146 /**147 * change the filter for the target line and fetch the new matched lines148 *149 * @param {string} handle150 * @param {string} filter151 * @returns {Promise}152 */153 changeFilter: function (handle, filter) {154 var line = this.getLine(handle);155 line['filter_'+line.mode] = filter;156 line['mv_lines_'+line.mode] = [];157 return this._performMoveLine(handle, line.mode);158 },159 /**160 * change the mode line ('inactive', 'match_rp', 'match_other', 'create'),161 * and fetch the new matched lines or prepare to create a new line162 *163 * ``match_rp``164 * display the matched lines from receivable/payable accounts, the user165 * can select the lines to apply there as proposition166 * ``match_other``167 * display the other matched lines, the user can select the lines to apply168 * there as proposition169 * ``create``170 * display fields and quick create button to create a new proposition171 * for the reconciliation172 *173 * @param {string} handle174 * @param {'inactive' | 'match_rp' | 'create'} mode175 * @returns {Promise}176 */177 changeMode: function (handle, mode) {178 var self = this;179 var line = this.getLine(handle);180 if (mode === 'default') {181 var match_requests = self.modes.filter(x => x.startsWith('match')).map(x => this._performMoveLine(handle, x))182 return Promise.all(match_requests).then(function() {183 return self.changeMode(handle, self._getDefaultMode(handle));184 });185 }186 if (mode === 'next') {187 var available_modes = self._getAvailableModes(handle)188 mode = available_modes[(available_modes.indexOf(line.mode) + 1) % available_modes.length];189 }190 line.mode = mode;191 if (['match_rp', 'match_other'].includes(line.mode)) {192 if (!(line['mv_lines_' + line.mode] && line['mv_lines_' + line.mode].length)) {193 return this._performMoveLine(handle, line.mode);194 } else {195 return this._formatMoveLine(handle, line.mode, []);196 }197 }198 if (line.mode === 'create') {199 return this.createProposition(handle);200 }201 return Promise.resolve();202 },203 /**204 * fetch the more matched lines205 *206 * @param {string} handle207 * @returns {Promise}208 */209 changeOffset: function (handle) {210 var line = this.getLine(handle);211 return this._performMoveLine(handle, line.mode);212 },213 /**214 * change the partner on the line and fetch the new matched lines215 *216 * @param {string} handle217 * @param {bool} preserveMode218 * @param {Object} partner219 * @param {string} partner.display_name220 * @param {number} partner.id221 * @returns {Promise}222 */223 changePartner: function (handle, partner, preserveMode) {224 var self = this;225 var line = this.getLine(handle);226 line.st_line.partner_id = partner && partner.id;227 line.st_line.partner_name = partner && partner.display_name || '';228 line.mv_lines_match_rp = [];229 line.mv_lines_match_other = [];230 return Promise.resolve(partner && this._changePartner(handle, partner.id))231 .then(function() {232 if(line.st_line.partner_id){233 _.each(line.reconciliation_proposition, function(prop){234 if(prop.partner_id != line.st_line.partner_id){235 line.reconciliation_proposition = [];236 return false;237 }238 });239 }240 return self._computeLine(line);241 })242 .then(function () {243 return self.changeMode(handle, preserveMode ? line.mode : 'default', true);244 })245 },246 /**247 * close the statement248 * @returns {Promise<number>} resolves to the res_id of the closed statements249 */250 closeStatement: function () {251 var self = this;252 return this._rpc({253 model: 'account.bank.statement.line',254 method: 'button_confirm_bank',255 args: [self.bank_statement_line_id.id],256 })257 .then(function () {258 return self.bank_statement_line_id.id;259 });260 },261 /**262 *263 * then open the first available line264 *265 * @param {string} handle266 * @returns {Promise}267 */268 createProposition: function (handle) {269 var line = this.getLine(handle);270 var prop = _.filter(line.reconciliation_proposition, '__focus');271 prop = this._formatQuickCreate(line);272 line.reconciliation_proposition.push(prop);273 line.createForm = _.pick(prop, this.quickCreateFields);274 return this._computeLine(line);275 },276 /**277 * Return context information and journal_id278 * @returns {Object} context279 */280 getContext: function () {281 return this.context;282 },283 /**284 * Return the lines that needs to be displayed by the widget285 *286 * @returns {Object} lines that are loaded and not yet displayed287 */288 getStatementLines: function () {289 var self = this;290 var linesToDisplay = _.pick(this.lines, function(value, key, object) {291 if (value.visible === true && self.alreadyDisplayed.indexOf(key) === -1) {292 self.alreadyDisplayed.push(key);293 return object;294 }295 });296 return linesToDisplay;297 },298 /**299 * Return a boolean telling if load button needs to be displayed or not300 * overridden in ManualModel301 *302 * @returns {boolean} true if load more button needs to be displayed303 */304 hasMoreLines: function () {305 var notDisplayed = _.filter(this.lines, function(line) { return !line.visible; });306 if (notDisplayed.length > 0) {307 return true;308 }309 return false;310 },311 /**312 * get the line data for this handle313 *314 * @param {Object} handle315 * @returns {Object}316 */317 getLine: function (handle) {318 return this.lines[handle];319 },320 /**321 * load data from322 *323 * - 'account.bank.statement' fetch the line id and bank_statement_id info324 * - 'account.reconcile.model' fetch all reconcile model (for quick add)325 * - 'account.account' fetch all account code326 * - 'account.reconciliation.widget' fetch each line data327 *328 * overridden in ManualModel329 * @param {Object} context330 * @param {number[]} context.statement_line_ids331 * @returns {Promise}332 */333 load: function (context) {334 var self = this;335 this.context = context;336 this.statement_line_ids = context.statement_line_ids;337 if (this.statement_line_ids === undefined) {338 // This could be undefined if the user pressed F5, take everything as fallback instead of rainbowman339 return self._rpc({340 model: 'account.bank.statement.line',341 method: 'search_read',342 fields: ['id'],343 domain: [['journal_id', '=?', context.active_id]],344 }).then(function (result) {345 self.statement_line_ids = result.map(r => r.id);346 return self.reload()347 })348 } else {349 return self.reload();350 }351 },352 /**353 * Load more bank statement line354 *355 * @param {integer} qty quantity to load356 * @returns {Promise}357 */358 loadMore: function(qty) {359 if (qty === undefined) {360 qty = this.defaultDisplayQty;361 }362 var ids = _.pluck(this.lines, 'id');363 ids = ids.splice(this.pagerIndex, qty);364 this.pagerIndex += qty;365 return this.loadData(ids, this._getExcludedIds());366 },367 /**368 * RPC method to load informations on lines369 * overridden in ManualModel370 *371 * @param {Array} ids ids of bank statement line passed to rpc call372 * @param {Array} excluded_ids list of move_line ids that needs to be excluded from search373 * @returns {Promise}374 */375 loadData: function(ids) {376 var self = this;377 var excluded_ids = this._getExcludedIds();378 return self._rpc({379 model: 'account.reconciliation.widget',380 method: 'get_bank_statement_line_data',381 args: [ids, excluded_ids],382 context: self.context,383 })384 .then(function(res){385 return self._formatLine(res['lines']);386 })387 },388 /**389 * Reload all data390 */391 reload: function() {392 var self = this;393 self.alreadyDisplayed = [];394 self.lines = {};395 self.pagerIndex = 0;396 var def_statement = this._rpc({397 model: 'account.reconciliation.widget',398 method: 'get_bank_statement_data',399 kwargs: {"bank_statement_line_ids":self.statement_line_ids, "srch_domain":self.domain},400 context: self.context,401 })402 .then(function (statement) {403 self.statement = statement;404 self.bank_statement_line_id = self.statement_line_ids.length === 1 ? {id: self.statement_line_ids[0], display_name: statement.statement_name} : false;405 self.valuenow = self.valuenow || statement.value_min;406 self.valuemax = self.valuemax || statement.value_max;407 self.context.journal_id = statement.journal_id;408 _.each(statement.lines, function (res) {409 var handle = _.uniqueId('rline');410 self.lines[handle] = {411 id: res.st_line.id,412 partner_id: res.st_line.partner_id,413 handle: handle,414 reconciled: false,415 mode: 'inactive',416 mv_lines_match_rp: [],417 mv_lines_match_other: [],418 filter_match_rp: "",419 filter_match_other: "",420 reconciliation_proposition: [],421 reconcileModels: [],422 };423 });424 });425 var domainReconcile = [];426 if (self.context && self.context.company_ids) {427 domainReconcile.push(['company_id', 'in', self.context.company_ids]);428 }429 if (self.context && self.context.active_model === 'account.journal' && self.context.active_ids) {430 domainReconcile.push('|');431 domainReconcile.push(['match_journal_ids', '=', false]);432 domainReconcile.push(['match_journal_ids', 'in', self.context.active_ids]);433 }434 var def_reconcileModel = this._loadReconciliationModel({domainReconcile: domainReconcile});435 var def_account = this._rpc({436 model: 'account.account',437 method: 'search_read',438 fields: ['code'],439 })440 .then(function (accounts) {441 self.accounts = _.object(_.pluck(accounts, 'id'), _.pluck(accounts, 'code'));442 });443 var def_taxes = self._loadTaxes();444 return Promise.all([def_statement, def_reconcileModel, def_account, def_taxes]).then(function () {445 _.each(self.lines, function (line) {446 line.reconcileModels = self.reconcileModels;447 });448 var ids = _.pluck(self.lines, 'id');449 ids = ids.splice(0, self.defaultDisplayQty);450 self.pagerIndex = ids.length;451 return self._formatLine(self.statement.lines);452 });453 },454 _readAnalyticTags: function (params) {455 var self = this;456 this.analyticTags = {};457 if (!params || !params.res_ids || !params.res_ids.length) {458 return $.when();459 }460 var fields = (params && params.fields || []).concat(['id', 'display_name']);461 return this._rpc({462 model: 'account.analytic.tag',463 method: 'read',464 args: [465 params.res_ids,466 fields,467 ],468 }).then(function (tags) {469 for (var i=0; i<tags.length; i++) {470 var tag = tags[i];471 self.analyticTags[tag.id] = tag;472 }473 });474 },475 _loadReconciliationModel: function (params) {476 var self = this;477 return this._rpc({478 model: 'account.reconcile.model',479 method: 'search_read',480 domain: params.domainReconcile || [],481 })482 .then(function (reconcileModels) {483 var analyticTagIds = [];484 for (var i=0; i<reconcileModels.length; i++) {485 var modelTags = reconcileModels[i].analytic_tag_ids || [];486 for (var j=0; j<modelTags.length; j++) {487 if (analyticTagIds.indexOf(modelTags[j]) === -1) {488 analyticTagIds.push(modelTags[j]);489 }490 }491 }492 return self._readAnalyticTags({res_ids: analyticTagIds}).then(function () {493 for (var i=0; i<reconcileModels.length; i++) {494 var recModel = reconcileModels[i];495 var analyticTagData = [];496 var modelTags = reconcileModels[i].analytic_tag_ids || [];497 for (var j=0; j<modelTags.length; j++) {498 var tagId = modelTags[j];499 analyticTagData.push([tagId, self.analyticTags[tagId].display_name])500 }501 recModel.analytic_tag_ids = analyticTagData;502 }503 self.reconcileModels = reconcileModels;504 });505 });506 },507 _loadTaxes: function(){508 var self = this;509 self.taxes = {};510 return this._rpc({511 model: 'account.tax',512 method: 'search_read',513 fields: ['price_include', 'name'],514 }).then(function (taxes) {515 _.each(taxes, function(tax){516 self.taxes[tax.id] = {517 price_include: tax.price_include,518 display_name: tax.name,519 };520 });521 return taxes;522 });523 },524 /**525 * Add lines into the propositions from the reconcile model526 * Can add 2 lines, and each with its taxes. The second line become editable527 * in the create mode.528 *529 * @see 'updateProposition' method for more informations about the530 * 'amount_type'531 *532 * @param {string} handle533 * @param {integer} reconcileModelId534 * @returns {Promise}535 */536 quickCreateProposition: function (handle, reconcileModelId) {537 var self = this;538 var line = this.getLine(handle);539 var reconcileModel = _.find(this.reconcileModels, function (r) {return r.id === reconcileModelId;});540 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'];541 this._blurProposition(handle);542 var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields));543 focus.reconcileModelId = reconcileModelId;544 line.reconciliation_proposition.push(focus);545 var defs = [];546 if (reconcileModel.has_second_line) {547 defs.push(self._computeLine(line).then(function() {548 var second = {};549 _.each(fields, function (key) {550 second[key] = ("second_"+key) in reconcileModel ? reconcileModel["second_"+key] : reconcileModel[key];551 });552 var second_focus = self._formatQuickCreate(line, second);553 second_focus.reconcileModelId = reconcileModelId;554 line.reconciliation_proposition.push(second_focus);555 self._computeReconcileModels(handle, reconcileModelId);556 }))557 }558 return Promise.all(defs).then(function() {559 line.createForm = _.pick(focus, self.quickCreateFields);560 return self._computeLine(line);561 })562 },563 /**564 * Remove a proposition and switch to an active mode ('create' or 'match_rp' or 'match_other')565 * overridden in ManualModel566 *567 * @param {string} handle568 * @param {number} id (move line id)569 * @returns {Promise}570 */571 removeProposition: function (handle, id) {572 var self = this;573 var line = this.getLine(handle);574 var defs = [];575 var prop = _.find(line.reconciliation_proposition, {'id' : id});576 if (prop) {577 line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) {578 return p.id !== prop.id && p.id !== prop.link && p.link !== prop.id && (!p.link || p.link !== prop.link);579 });580 if (prop['reconcileModelId'] === undefined) {581 if (['receivable', 'payable', 'liquidity'].includes(prop.account_type)) {582 line.mv_lines_match_rp.unshift(prop);583 } else {584 line.mv_lines_match_other.unshift(prop);585 }586 }587 // No proposition left and then, reset the st_line partner.588 if(line.reconciliation_proposition.length == 0 && line.st_line.has_no_partner)589 defs.push(self.changePartner(line.handle));590 }591 line.mode = (id || line.mode !== "create") && isNaN(id) ? 'create' : 'match_rp';592 defs.push(this._computeLine(line));593 return Promise.all(defs).then(function() {594 return self.changeMode(handle, line.mode, true);595 })596 },597 getPartialReconcileAmount: function(handle, data) {598 var line = this.getLine(handle);599 var formatOptions = {600 currency_id: line.st_line.currency_id,601 noSymbol: true,602 };603 var prop = _.find(line.reconciliation_proposition, {'id': data.data});604 if (prop) {605 var amount = prop.partial_amount || prop.amount;606 // Check if we can get a partial amount that would directly set balance to zero607 var partial = Math.abs(line.balance.amount + amount);608 if (Math.abs(line.balance.amount) >= Math.abs(amount)) {609 amount = Math.abs(amount);610 } else if (partial <= Math.abs(prop.amount) && partial >= 0) {611 amount = partial;612 } else {613 amount = Math.abs(amount);614 }615 return field_utils.format.monetary(amount, {}, formatOptions);616 }617 },618 /**619 * Force the partial reconciliation to display the reconciliate button.620 *621 * @param {string} handle622 * @returns {Promise}623 */624 partialReconcile: function(handle, data) {625 var line = this.getLine(handle);626 var prop = _.find(line.reconciliation_proposition, {'id' : data.mvLineId});627 if (prop) {628 var amount = data.amount;629 try {630 amount = field_utils.parse.float(data.amount);631 }632 catch (err) {633 amount = NaN;634 }635 // Amount can't be greater than line.amount and can not be negative and must be a number636 // the amount we receive will be a string, so take sign of previous line amount in consideration in order to put637 // the amount in the correct left or right column638 if (amount >= Math.abs(prop.amount) || amount <= 0 || isNaN(amount)) {639 delete prop.partial_amount_str;640 delete prop.partial_amount;641 if (isNaN(amount) || amount < 0) {642 this.do_warn(_.str.sprintf(_t('The amount %s is not a valid partial amount'), data.amount));643 }644 return this._computeLine(line);645 }646 else {647 var format_options = { currency_id: line.st_line.currency_id };648 prop.partial_amount = (prop.amount > 0 ? 1 : -1)*amount;649 prop.partial_amount_str = field_utils.format.monetary(Math.abs(prop.partial_amount), {}, format_options);650 }651 }652 return this._computeLine(line);653 },654 /**655 * Change the value of the editable proposition line or create a new one.656 *657 * If the editable line comes from a reconcile model with 2 lines658 * and their 'amount_type' is "percent"659 * and their total equals 100% (this doesn't take into account the taxes660 * who can be included or not)661 * Then the total is recomputed to have 100%.662 *663 * @param {string} handle664 * @param {*} values665 * @returns {Promise}666 */667 updateProposition: function (handle, values) {668 var self = this;669 var line = this.getLine(handle);670 var prop = _.last(_.filter(line.reconciliation_proposition, '__focus'));671 if ('to_check' in values && values.to_check === false) {672 // check if we have another line with to_check and if yes don't change value of this proposition673 prop.to_check = line.reconciliation_proposition.some(function(rec_prop, index) {674 return rec_prop.id !== prop.id && rec_prop.to_check;675 });676 }677 if (!prop) {678 prop = this._formatQuickCreate(line);679 line.reconciliation_proposition.push(prop);680 }681 _.each(values, function (value, fieldName) {682 if (fieldName === 'analytic_tag_ids') {683 switch (value.operation) {684 case "ADD_M2M":685 // handle analytic_tag selection via drop down (single dict) and686 // full widget (array of dict)687 var vids = _.isArray(value.ids) ? value.ids : [value.ids];688 _.each(vids, function (val) {689 if (!_.findWhere(prop.analytic_tag_ids, {id: val.id})) {690 prop.analytic_tag_ids.push(val);691 }692 });693 break;694 case "FORGET":695 var id = self.localData[value.ids[0]].ref;696 prop.analytic_tag_ids = _.filter(prop.analytic_tag_ids, function (val) {697 return val.id !== id;698 });699 break;700 }701 }702 else if (fieldName === 'tax_ids') {703 switch(value.operation) {704 case "ADD_M2M":705 prop.__tax_to_recompute = true;706 var vids = _.isArray(value.ids) ? value.ids : [value.ids];707 _.each(vids, function(val){708 if (!_.findWhere(prop.tax_ids, {id: val.id})) {709 value.ids.price_include = self.taxes[val.id] ? self.taxes[val.id].price_include : false;710 prop.tax_ids.push(val);711 }712 });713 break;714 case "FORGET":715 prop.__tax_to_recompute = true;716 var id = self.localData[value.ids[0]].ref;717 prop.tax_ids = _.filter(prop.tax_ids, function (val) {718 return val.id !== id;719 });720 break;721 }722 }723 else {724 prop[fieldName] = values[fieldName];725 }726 });727 if ('account_id' in values) {728 prop.account_code = prop.account_id ? this.accounts[prop.account_id.id] : '';729 }730 if ('amount' in values) {731 prop.base_amount = values.amount;732 if (prop.reconcileModelId) {733 this._computeReconcileModels(handle, prop.reconcileModelId);734 }735 }736 if ('force_tax_included' in values || 'amount' in values || 'account_id' in values) {737 prop.__tax_to_recompute = true;738 }739 line.createForm = _.pick(prop, this.quickCreateFields);740 // If you check/uncheck the force_tax_included box, reset the createForm amount.741 if(prop.base_amount)742 line.createForm.amount = prop.base_amount;743 if (prop.tax_ids.length !== 1 ) {744 // When we have 0 or more than 1 taxes, reset the base_amount and force_tax_included, otherwise weird behavior can happen745 prop.amount = prop.base_amount;746 line.createForm.force_tax_included = false;747 }748 return this._computeLine(line);749 },750 /**751 * Format the value and send it to 'account.reconciliation.widget' model752 * Update the number of validated lines753 * overridden in ManualModel754 *755 * @param {(string|string[])} handle756 * @returns {Promise<Object>} resolved with an object who contains757 * 'handles' key758 */759 validate: function (handle) {760 var self = this;761 this.display_context = 'validate';762 var handles = [];763 if (handle) {764 handles = [handle];765 } else {766 _.each(this.lines, function (line, handle) {767 if (!line.reconciled && line.balance && !line.balance.amount && line.reconciliation_proposition.length) {768 handles.push(handle);769 }770 });771 }772 var ids = [];773 var values = [];774 var handlesPromises = [];775 _.each(handles, function (handle) {776 var line = self.getLine(handle);777 var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.invalid;});778 var computeLinePromise;779 if (props.length === 0) {780 // Usability: if user has not chosen any lines and click validate, it has the same behavior781 // as creating a write-off of the same amount.782 props.push(self._formatQuickCreate(line, {783 account_id: [line.st_line.open_balance_account_id, self.accounts[line.st_line.open_balance_account_id]],784 }));785 // update balance of line otherwise it won't be to zero and another line will be added786 line.reconciliation_proposition.push(props[0]);787 computeLinePromise = self._computeLine(line);788 }789 ids.push(line.id);790 handlesPromises.push(Promise.resolve(computeLinePromise).then(function() {791 var values_dict = {792 "partner_id": line.st_line.partner_id,793 "counterpart_aml_dicts": _.map(_.filter(props, function (prop) {794 return !isNaN(prop.id) && !prop.already_paid;795 }), self._formatToProcessReconciliation.bind(self, line)),796 "payment_aml_ids": _.pluck(_.filter(props, function (prop) {797 return !isNaN(prop.id) && prop.already_paid;798 }), 'id'),799 "new_aml_dicts": _.map(_.filter(props, function (prop) {800 return isNaN(prop.id) && prop.display;801 }), self._formatToProcessReconciliation.bind(self, line)),802 "to_check": line.to_check,803 };804 // If the lines are not fully balanced, create an unreconciled amount.805 // line.st_line.currency_id is never false here because its equivalent to806 // statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side).807 // see: get_statement_line_for_reconciliation_widget method in account/models/account_bank_statement.py for more details808 var currency = session.get_currency(line.st_line.currency_id);809 var balance = line.balance.amount;810 if (!utils.float_is_zero(balance, currency.digits[1])) {811 var unreconciled_amount_dict = {812 'account_id': line.st_line.open_balance_account_id,813 'credit': balance > 0 ? balance : 0,814 'debit': balance < 0 ? -balance : 0,815 'name': line.st_line.name + ' : ' + _t("Open balance"),816 };817 values_dict['new_aml_dicts'].push(unreconciled_amount_dict);818 }819 values.push(values_dict);820 line.reconciled = true;821 }));822 _.each(self.lines, function(other_line) {823 if (other_line != line) {824 var filtered_prop = other_line.reconciliation_proposition.filter(p => !line.reconciliation_proposition.map(l => l.id).includes(p.id));825 if (filtered_prop.length != other_line.reconciliation_proposition.length) {826 other_line.need_update = true;827 other_line.reconciliation_proposition = filtered_prop;828 }829 self._computeLine(line);830 }831 })832 });833 return Promise.all(handlesPromises).then(function() {834 return self._rpc({835 model: 'account.reconciliation.widget',836 method: 'process_bank_statement_line',837 args: [ids, values],838 context: self.context,839 })840 .then(self._validatePostProcess.bind(self))841 .then(function () {842 self.valuenow += handles.length;843 return {handles: handles};844 });845 });846 },847 //--------------------------------------------------------------------------848 // Private849 //--------------------------------------------------------------------------850 /**851 * add a line proposition after checking receivable and payable accounts constraint852 *853 * @private854 * @param {Object} line855 * @param {Object} prop856 */857 _addProposition: function (line, prop) {858 line.reconciliation_proposition.push(prop);859 },860 /**861 * stop the editable proposition line and remove it if it's invalid then862 * compute the line863 *864 * See :func:`_computeLine`865 *866 * @private867 * @param {string} handle868 * @returns {Promise}869 */870 _blurProposition: function (handle) {871 var line = this.getLine(handle);872 line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (l) {873 l.__focus = false;874 return !l.invalid;875 });876 },877 /**878 * When changing partner, read property_account_receivable and payable879 * of that partner because the counterpart account might cahnge depending880 * on the partner881 *882 * @private883 * @param {string} handle884 * @param {integer} partner_id885 * @returns {Promise}886 */887 _changePartner: function (handle, partner_id) {888 var self = this;889 return this._rpc({890 model: 'res.partner',891 method: 'read',892 args: [partner_id, ["property_account_receivable_id", "property_account_payable_id"]],893 }).then(function (result) {894 if (result.length > 0) {895 var line = self.getLine(handle);896 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];897 }898 });899 },900 /**901 * Calculates the balance; format each proposition amount_str and mark as902 * invalid the line with empty account_id, amount or label903 * Check the taxes server side for each updated propositions with tax_ids904 * extended by ManualModel905 *906 * @private907 * @param {Object} line908 * @returns {Promise}909 */910 _computeLine: function (line) {911 //balance_type912 var self = this;913 // compute taxes914 var tax_defs = [];915 var reconciliation_proposition = [];916 var formatOptions = {917 currency_id: line.st_line.currency_id,918 };919 line.to_check = false;920 _.each(line.reconciliation_proposition, function (prop) {921 if (prop.to_check) {922 // If one of the proposition is to_check, set the global to_check flag to true923 line.to_check = true;924 }925 if (prop.tax_repartition_line_id) {926 if (!_.find(line.reconciliation_proposition, {'id': prop.link}).__tax_to_recompute) {927 reconciliation_proposition.push(prop);928 }929 return;930 }931 if (!prop.already_paid && parseInt(prop.id)) {932 prop.is_move_line = true;933 }934 reconciliation_proposition.push(prop);935 if (prop.tax_ids && prop.tax_ids.length && prop.__tax_to_recompute && prop.base_amount) {936 reconciliation_proposition = _.filter(reconciliation_proposition, function (p) {937 return !p.tax_repartition_line_id || p.link !== prop.id;938 });939 var args = [prop.tax_ids.map(function(el){return el.id;}), prop.base_amount, formatOptions.currency_id];940 var add_context = {'round': true};941 if(prop.tax_ids.length === 1 && line.createForm && line.createForm.force_tax_included)942 add_context.force_price_include = true;943 tax_defs.push(self._rpc({944 model: 'account.tax',945 method: 'json_friendly_compute_all',946 args: args,947 context: $.extend({}, self.context || {}, add_context),948 })949 .then(function (result) {950 _.each(result.taxes, function(tax){951 var tax_prop = self._formatQuickCreate(line, {952 'link': prop.id,953 'tax_ids': tax.tax_ids,954 'tax_repartition_line_id': tax.tax_repartition_line_id,955 'tag_ids': tax.tag_ids,956 'amount': tax.amount,957 'label': prop.label ? prop.label + " " + tax.name : tax.name,958 'date': prop.date,959 'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id,960 'analytic': tax.analytic,961 '__focus': false962 });963 prop.tax_exigible = tax.tax_exigibility === 'on_payment' ? true : undefined;964 prop.amount = tax.base;965 prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);966 prop.invalid = !self._isValid(prop);967 tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, formatOptions);968 tax_prop.invalid = prop.invalid;969 reconciliation_proposition.push(tax_prop);970 });971 prop.tag_ids = result.base_tags;972 }));973 } else {974 prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);975 prop.display = self._isDisplayedProposition(prop);976 prop.invalid = !self._isValid(prop);977 }978 });979 return Promise.all(tax_defs).then(function () {980 _.each(reconciliation_proposition, function (prop) {981 prop.__tax_to_recompute = false;982 });983 line.reconciliation_proposition = reconciliation_proposition;984 var amount_currency = 0;985 var total = line.st_line.amount || 0;986 var isOtherCurrencyId = _.uniq(_.pluck(_.reject(reconciliation_proposition, 'invalid'), 'currency_id'));987 isOtherCurrencyId = isOtherCurrencyId.length === 1 && !total && isOtherCurrencyId[0] !== formatOptions.currency_id ? isOtherCurrencyId[0] : false;988 _.each(reconciliation_proposition, function (prop) {989 if (!prop.invalid) {990 total -= prop.partial_amount || prop.amount;991 if (isOtherCurrencyId) {992 amount_currency -= (prop.amount < 0 ? -1 : 1) * Math.abs(prop.amount_currency);993 }994 }995 });996 var company_currency = session.get_currency(line.st_line.currency_id);997 var company_precision = company_currency && company_currency.digits[1] || 2;998 total = utils.round_decimals(total, company_precision) || 0;999 if(isOtherCurrencyId){1000 var other_currency = session.get_currency(isOtherCurrencyId);1001 var other_precision = other_currency && other_currency.digits[1] || 2;1002 amount_currency = utils.round_decimals(amount_currency, other_precision);1003 }1004 line.balance = {1005 amount: total,1006 amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions),1007 currency_id: isOtherCurrencyId,1008 amount_currency: isOtherCurrencyId ? amount_currency : total,1009 amount_currency_str: isOtherCurrencyId ? field_utils.format.monetary(Math.abs(amount_currency), {}, {1010 currency_id: isOtherCurrencyId1011 }) : false,1012 account_code: self.accounts[line.st_line.open_balance_account_id],1013 };1014 line.balance.show_balance = line.balance.amount_currency != 0;1015 line.balance.type = line.balance.amount_currency ? (line.st_line.partner_id ? 0 : -1) : 1;1016 });1017 },1018 /**1019 *1020 *1021 * @private1022 * @param {string} handle1023 * @param {integer} reconcileModelId1024 */1025 _computeReconcileModels: function (handle, reconcileModelId) {1026 var line = this.getLine(handle);1027 // if quick create with 2 lines who use 100%, change the both values in same time1028 var props = _.filter(line.reconciliation_proposition, {'reconcileModelId': reconcileModelId, '__focus': true});1029 if (props.length === 2 && props[0].percent && props[1].percent) {1030 if (props[0].percent + props[1].percent === 100) {1031 props[0].base_amount = props[0].amount = line.st_line.amount - props[1].base_amount;1032 props[0].__tax_to_recompute = true;1033 }1034 }1035 },1036 /**1037 * format a name_get into an object {id, display_name}, idempotent1038 *1039 * @private1040 * @param {Object|Array} [value] data or name_get1041 */1042 _formatNameGet: function (value) {1043 return value ? (value.id ? value : {'id': value[0], 'display_name': value[1]}) : false;1044 },1045 _formatMany2ManyTags: function (value) {1046 var res = [];1047 for (var i=0, len=value.length; i<len; i++) {1048 res[i] = {'id': value[i][0], 'display_name': value[i][1]};1049 }1050 return res;1051 },1052 _formatMany2ManyTagsTax: function(value) {1053 var res = [];1054 for (var i=0; i<value.length; i++) {1055 res.push({id: value[i], display_name: this.taxes[value[i]] ? this.taxes[value[i]].display_name : ''});1056 }1057 return res;1058 },1059 /**1060 * Format each propositions (amount, label, account_id)1061 * extended in ManualModel1062 *1063 * @private1064 * @param {Object} line1065 * @param {Object[]} props1066 */1067 _formatLineProposition: function (line, props) {1068 var self = this;1069 if (props.length) {1070 _.each(props, function (prop) {1071 prop.amount = prop.debit || -prop.credit;1072 prop.label = prop.name;1073 prop.account_id = self._formatNameGet(prop.account_id || line.account_id);1074 prop.is_partially_reconciled = prop.amount_str !== prop.total_amount_str;1075 prop.to_check = !!prop.to_check;1076 });1077 }1078 },1079 /**1080 * Format each server lines and propositions and compute all lines1081 * overridden in ManualModel1082 *1083 * @see '_computeLine'1084 *1085 * @private1086 * @param {Object[]} lines1087 * @returns {Promise}1088 */1089 _formatLine: function (lines) {1090 var self = this;1091 var defs = [];1092 _.each(lines, function (data) {1093 var line = _.find(self.lines, function (l) {1094 return l.id === data.st_line.id;1095 });1096 line.visible = true;1097 line.limitMoveLines = self.limitMoveLines;1098 _.extend(line, data);1099 self._formatLineProposition(line, line.reconciliation_proposition);1100 if (!line.reconciliation_proposition.length) {1101 delete line.reconciliation_proposition;1102 }1103 // No partner set on st_line and all matching amls have the same one: set it on the st_line.1104 defs.push(1105 self._computeLine(line)1106 .then(function(){1107 if(!line.st_line.partner_id && line.reconciliation_proposition.length > 0){1108 var hasDifferentPartners = function(prop){1109 return !prop.partner_id || prop.partner_id != line.reconciliation_proposition[0].partner_id;1110 };1111 if(!_.any(line.reconciliation_proposition, hasDifferentPartners)){1112 return self.changePartner(line.handle, {1113 'id': line.reconciliation_proposition[0].partner_id,1114 'display_name': line.reconciliation_proposition[0].partner_name,1115 }, true);1116 }1117 }else if(!line.st_line.partner_id && line.partner_id && line.partner_name){1118 return self.changePartner(line.handle, {1119 'id': line.partner_id,1120 'display_name': line.partner_name,1121 }, true);1122 }1123 return true;1124 })1125 .then(function(){1126 return data.write_off ? self.quickCreateProposition(line.handle, data.model_id) : true;1127 })1128 .then(function() {1129 // If still no partner set, take the one from context, if it exists1130 if (!line.st_line.partner_id && self.context.partner_id && self.context.partner_name) {1131 return self.changePartner(line.handle, {1132 'id': self.context.partner_id,1133 'display_name': self.context.partner_name,1134 }, true);1135 }1136 return true;1137 })1138 );1139 });1140 return Promise.all(defs);1141 },1142 /**1143 * Format the server value then compute the line1144 * overridden in ManualModel1145 *1146 * @see '_computeLine'1147 *1148 * @private1149 * @param {string} handle1150 * @param {Object[]} mv_lines1151 * @returns {Promise}1152 */1153 _formatMoveLine: function (handle, mode, mv_lines) {1154 var self = this;1155 var line = this.getLine(handle);1156 line['mv_lines_'+mode] = _.uniq(line['mv_lines_'+mode].concat(mv_lines), l => l.id);1157 if (mv_lines[0]){1158 line['remaining_'+mode] = mv_lines[0].recs_count - mv_lines.length;1159 } else if (line['mv_lines_'+mode].lenght == 0) {1160 line['remaining_'+mode] = 0;1161 }1162 this._formatLineProposition(line, mv_lines);1163 if ((line.mode == 'match_other' || line.mode == "match_rp") && !line['mv_lines_'+mode].length && !line['filter_'+mode].length) {1164 line.mode = self._getDefaultMode(handle);1165 if (line.mode !== 'match_rp' && line.mode !== 'match_other' && line.mode !== 'inactive') {1166 return this._computeLine(line).then(function () {1167 return self.createProposition(handle);1168 });1169 }1170 } else {1171 return this._computeLine(line);1172 }1173 },1174 /**1175 * overridden in ManualModel1176 */1177 _getDefaultMode: function(handle) {1178 var line = this.getLine(handle);1179 if (line.balance.amount === 01180 && (!line.st_line.mv_lines_match_rp || line.st_line.mv_lines_match_rp.length === 0)1181 && (!line.st_line.mv_lines_match_other || line.st_line.mv_lines_match_other.length === 0)) {1182 return 'inactive';1183 }1184 if (line.mv_lines_match_rp && line.mv_lines_match_rp.length) {1185 return 'match_rp';1186 }1187 if (line.mv_lines_match_other && line.mv_lines_match_other.length) {1188 return 'match_other';1189 }1190 return 'create';1191 },1192 _getAvailableModes: function(handle) {1193 var line = this.getLine(handle);1194 var modes = []1195 if (line.mv_lines_match_rp && line.mv_lines_match_rp.length) {1196 modes.push('match_rp')1197 }1198 if (line.mv_lines_match_other && line.mv_lines_match_other.length) {1199 modes.push('match_other')1200 }1201 modes.push('create')1202 return modes1203 },1204 /**1205 * Apply default values for the proposition, format datas and format the1206 * base_amount with the decimal number from the currency1207 * extended in ManualModel1208 *1209 * @private1210 * @param {Object} line1211 * @param {Object} values1212 * @returns {Object}1213 */1214 _formatQuickCreate: function (line, values) {1215 values = values || {};1216 var today = new moment().utc().format();1217 var account = this._formatNameGet(values.account_id);1218 var formatOptions = {1219 currency_id: line.st_line.currency_id,1220 };1221 var amount;1222 switch(values.amount_type) {1223 case 'percentage':1224 amount = line.balance.amount * values.amount / 100;1225 break;1226 case 'regex':1227 var matching = line.st_line.name.match(new RegExp(values.amount_from_label_regex))1228 amount = 0;1229 if (matching && matching.length == 2) {1230 matching = matching[1].replace(new RegExp('\\D' + values.decimal_separator, 'g'), '');1231 matching = matching.replace(values.decimal_separator, '.');1232 amount = parseFloat(matching) || 0;1233 amount = line.balance.amount > 0 ? amount : -amount;1234 }1235 break;1236 case 'fixed':1237 amount = values.amount;1238 break;1239 default:1240 amount = values.amount !== undefined ? values.amount : line.balance.amount;1241 }1242 var prop = {1243 'id': _.uniqueId('createLine'),1244 'label': values.label || line.st_line.name,1245 'account_id': account,1246 'account_code': account ? this.accounts[account.id] : '',1247 'analytic_account_id': this._formatNameGet(values.analytic_account_id),1248 'analytic_tag_ids': this._formatMany2ManyTags(values.analytic_tag_ids || []),1249 'journal_id': this._formatNameGet(values.journal_id),1250 'tax_ids': this._formatMany2ManyTagsTax(values.tax_ids || []),1251 'tag_ids': values.tag_ids,1252 'tax_repartition_line_id': values.tax_repartition_line_id,1253 'debit': 0,1254 'credit': 0,1255 'date': values.date ? values.date : field_utils.parse.date(today, {}, {isUTC: true}),1256 'force_tax_included': values.force_tax_included || false,1257 'base_amount': amount,1258 'percent': values.amount_type === "percentage" ? values.amount : null,1259 'link': values.link,1260 'display': true,1261 'invalid': true,1262 'to_check': !!values.to_check,1263 '__tax_to_recompute': true,1264 '__focus': '__focus' in values ? values.__focus : true,1265 };1266 if (prop.base_amount) {1267 // Call to format and parse needed to round the value to the currency precision1268 var sign = prop.base_amount < 0 ? -1 : 1;1269 var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, formatOptions);1270 prop.base_amount = sign * field_utils.parse.monetary(amount, {}, formatOptions);1271 }1272 prop.amount = prop.base_amount;1273 return prop;1274 },1275 /**1276 * Return list of account_move_line that has been selected and needs to be removed1277 * from other calls.1278 *1279 * @private1280 * @returns {Array} list of excluded ids1281 */1282 _getExcludedIds: function () {1283 var excludedIds = [];1284 _.each(this.lines, function(line) {1285 if (line.reconciliation_proposition) {1286 _.each(line.reconciliation_proposition, function(prop) {1287 if (parseInt(prop['id'])) {1288 excludedIds.push(prop['id']);1289 }1290 });1291 }1292 });1293 return excludedIds;1294 },1295 /**1296 * Defined whether the line is to be displayed or not. Here, we only display1297 * the line if it comes from the server or if an account is defined when it1298 * is created1299 * extended in ManualModel1300 *1301 * @private1302 * @param {object} prop1303 * @returns {Boolean}1304 */1305 _isDisplayedProposition: function (prop) {1306 return !isNaN(prop.id) || !!prop.account_id;1307 },1308 /**1309 * extended in ManualModel1310 * @private1311 * @param {object} prop1312 * @returns {Boolean}1313 */1314 _isValid: function (prop) {1315 return !isNaN(prop.id) || prop.account_id && prop.amount && prop.label && !!prop.label.length;1316 },1317 /**1318 * Fetch 'account.reconciliation.widget' propositions.1319 * overridden in ManualModel1320 *1321 * @see '_formatMoveLine'1322 *1323 * @private1324 * @param {string} handle1325 * @returns {Promise}1326 */1327 _performMoveLine: function (handle, mode, limit) {1328 limit = limit || this.limitMoveLines;1329 var line = this.getLine(handle);1330 var excluded_ids = _.map(_.union(line.reconciliation_proposition, line.mv_lines_match_rp, line.mv_lines_match_other), function (prop) {1331 return _.isNumber(prop.id) ? prop.id : null;1332 }).filter(id => id != null);1333 var filter = line['filter_'+mode] || "";1334 return this._rpc({1335 model: 'account.reconciliation.widget',1336 method: 'get_move_lines_for_bank_statement_line',1337 args: [line.id, line.st_line.partner_id, excluded_ids, filter, 0, limit, mode === 'match_rp' ? 'rp' : 'other'],1338 context: this.context,1339 })1340 .then(this._formatMoveLine.bind(this, handle, mode));1341 },1342 /**1343 * format the proposition to send information server side1344 * extended in ManualModel1345 *1346 * @private1347 * @param {object} line1348 * @param {object} prop1349 * @returns {object}1350 */1351 _formatToProcessReconciliation: function (line, prop) {1352 var amount = -prop.amount;1353 if (prop.partial_amount) {1354 amount = -prop.partial_amount;1355 }1356 var result = {1357 name : prop.label,1358 debit : amount > 0 ? amount : 0,1359 credit : amount < 0 ? -amount : 0,1360 tax_exigible: prop.tax_exigible,1361 analytic_tag_ids: [[6, null, _.pluck(prop.analytic_tag_ids, 'id')]]1362 };1363 if (!isNaN(prop.id)) {1364 result.counterpart_aml_id = prop.id;1365 } else {1366 result.account_id = prop.account_id.id;1367 if (prop.journal_id) {1368 result.journal_id = prop.journal_id.id;1369 }1370 }1371 if (!isNaN(prop.id)) result.counterpart_aml_id = prop.id;1372 if (prop.analytic_account_id) result.analytic_account_id = prop.analytic_account_id.id;1373 if (prop.tax_ids && prop.tax_ids.length) result.tax_ids = [[6, null, _.pluck(prop.tax_ids, 'id')]];1374 if (prop.tag_ids && prop.tag_ids.length) result.tag_ids = [[6, null, prop.tag_ids]];1375 if (prop.tax_repartition_line_id) result.tax_repartition_line_id = prop.tax_repartition_line_id;1376 if (prop.reconcileModelId) result.reconcile_model_id = prop.reconcileModelId1377 return result;1378 },1379 /**1380 * Hook to handle return values of the validate's line process.1381 *1382 * @private1383 * @param {Object} data1384 * @param {Object[]} data.moves list of processed account.move1385 * @returns {Deferred}1386 */1387 _validatePostProcess: function (data) {1388 var self = this;1389 return Promise.resolve();1390 },1391});1392/**1393 * Model use to fetch, format and update 'account.move.line' and 'res.partner'1394 * datas allowing manual reconciliation1395 */1396var ManualModel = StatementModel.extend({1397 quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_ids', 'force_tax_included', 'analytic_tag_ids', 'date', 'to_check'],1398 modes: ['create', 'match'],1399 //--------------------------------------------------------------------------1400 // Public1401 //--------------------------------------------------------------------------1402 /**1403 * Return a boolean telling if load button needs to be displayed or not1404 *1405 * @returns {boolean} true if load more button needs to be displayed1406 */1407 hasMoreLines: function () {1408 if (this.manualLines.length > this.pagerIndex) {1409 return true;1410 }1411 return false;1412 },1413 /**1414 * load data from1415 * - 'account.reconciliation.widget' fetch the lines to reconciliate1416 * - 'account.account' fetch all account code1417 *1418 * @param {Object} context1419 * @param {string} [context.mode] 'customers', 'suppliers' or 'accounts'1420 * @param {integer[]} [context.company_ids]1421 * @param {integer[]} [context.partner_ids] used for 'customers' and1422 * 'suppliers' mode1423 * @returns {Promise}1424 */1425 load: function (context) {1426 var self = this;1427 this.context = context;1428 var domain_account_id = [];1429 if (context && context.company_ids) {1430 domain_account_id.push(['company_id', 'in', context.company_ids]);1431 }1432 var def_account = this._rpc({1433 model: 'account.account',1434 method: 'search_read',1435 domain: domain_account_id,1436 fields: ['code'],1437 })1438 .then(function (accounts) {1439 self.account_ids = _.pluck(accounts, 'id');1440 self.accounts = _.object(self.account_ids, _.pluck(accounts, 'code'));1441 });1442 var domainReconcile = [];1443 var session_allowed_company_ids = session.user_context.allowed_company_ids || []1444 var company_ids = context && context.company_ids || session_allowed_company_ids.slice(0, 1);1445 if (company_ids) {1446 domainReconcile.push(['company_id', 'in', company_ids]);1447 }1448 var def_reconcileModel = this._loadReconciliationModel({domainReconcile: domainReconcile});1449 var def_taxes = this._loadTaxes();1450 return Promise.all([def_reconcileModel, def_account, def_taxes]).then(function () {1451 switch(context.mode) {1452 case 'customers':1453 case 'suppliers':1454 var mode = context.mode === 'customers' ? 'receivable' : 'payable';1455 var args = ['partner', context.partner_ids || null, mode];1456 return self._rpc({1457 model: 'account.reconciliation.widget',1458 method: 'get_data_for_manual_reconciliation',1459 args: args,1460 context: context,1461 })1462 .then(function (result) {1463 self.manualLines = result;1464 self.valuenow = 0;1465 self.valuemax = Object.keys(self.manualLines).length;1466 var lines = self.manualLines.slice(0, self.defaultDisplayQty);1467 self.pagerIndex = lines.length;1468 return self.loadData(lines);1469 });1470 case 'accounts':1471 return self._rpc({1472 model: 'account.reconciliation.widget',1473 method: 'get_data_for_manual_reconciliation',1474 args: ['account', context.account_ids || self.account_ids],1475 context: context,1476 })1477 .then(function (result) {1478 self.manualLines = result;1479 self.valuenow = 0;1480 self.valuemax = Object.keys(self.manualLines).length;1481 var lines = self.manualLines.slice(0, self.defaultDisplayQty);1482 self.pagerIndex = lines.length;1483 return self.loadData(lines);1484 });1485 default:1486 var partner_ids = context.partner_ids || null;1487 var account_ids = context.account_ids || self.account_ids || null;1488 return self._rpc({1489 model: 'account.reconciliation.widget',1490 method: 'get_all_data_for_manual_reconciliation',1491 args: [partner_ids, account_ids],1492 context: context,1493 })1494 .then(function (result) {1495 // Flatten the result1496 self.manualLines = [].concat(result.accounts, result.customers, result.suppliers);1497 self.valuenow = 0;1498 self.valuemax = Object.keys(self.manualLines).length;1499 var lines = self.manualLines.slice(0, self.defaultDisplayQty);1500 self.pagerIndex = lines.length;1501 return self.loadData(lines);1502 });1503 }1504 });1505 },1506 /**1507 * Reload data by calling load1508 * It overrides super.reload() because1509 * it is not adapted for this model.1510 *1511 * Use case: coming back to manual reconcilation1512 * in breadcrumb1513 */1514 reload: function () {1515 this.lines = {};1516 return this.load(this.context);1517 },1518 /**1519 * Load more partners/accounts1520 * overridden in ManualModel1521 *1522 * @param {integer} qty quantity to load1523 * @returns {Promise}1524 */1525 loadMore: function(qty) {1526 if (qty === undefined) {1527 qty = this.defaultDisplayQty;1528 }1529 var lines = this.manualLines.slice(this.pagerIndex, this.pagerIndex + qty);1530 this.pagerIndex += qty;1531 return this.loadData(lines);1532 },1533 /**1534 * Method to load informations on lines1535 *1536 * @param {Array} lines manualLines to load1537 * @returns {Promise}1538 */1539 loadData: function(lines) {1540 var self = this;1541 var defs = [];1542 _.each(lines, function (l) {1543 defs.push(self._formatLine(l.mode, l));1544 });1545 return Promise.all(defs);1546 },1547 /**1548 * Mark the account or the partner as reconciled1549 *1550 * @param {(string|string[])} handle1551 * @returns {Promise<Array>} resolved with the handle array1552 */1553 validate: function (handle) {1554 var self = this;1555 var handles = [];1556 if (handle) {1557 handles = [handle];1558 } else {1559 _.each(this.lines, function (line, handle) {1560 if (!line.reconciled && !line.balance.amount && line.reconciliation_proposition.length) {1561 handles.push(handle);1562 }1563 });1564 }1565 var def = Promise.resolve();1566 var process_reconciliations = [];1567 var reconciled = [];1568 _.each(handles, function (handle) {1569 var line = self.getLine(handle);1570 if(line.reconciled) {1571 return;1572 }1573 var props = line.reconciliation_proposition;1574 if (!props.length) {1575 self.valuenow++;1576 reconciled.push(handle);1577 line.reconciled = true;1578 process_reconciliations.push({1579 id: line.type === 'accounts' ? line.account_id : line.partner_id,1580 type: line.type,1581 mv_line_ids: [],1582 new_mv_line_dicts: [],1583 });1584 } else {1585 var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id');1586 var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id) && prop.display;}), self._formatToProcessReconciliation.bind(self, line));1587 process_reconciliations.push({1588 id: null,1589 type: null,1590 mv_line_ids: mv_line_ids,1591 new_mv_line_dicts: new_mv_line_dicts1592 });1593 }1594 line.reconciliation_proposition = [];1595 });1596 if (process_reconciliations.length) {1597 def = self._rpc({1598 model: 'account.reconciliation.widget',1599 method: 'process_move_lines',1600 args: [process_reconciliations],1601 });1602 }1603 return def.then(function() {1604 var defs = [];1605 var account_ids = [];1606 var partner_ids = [];1607 _.each(handles, function (handle) {1608 var line = self.getLine(handle);1609 if (line.reconciled) {1610 return;1611 }1612 line.filter_match = "";1613 defs.push(self._performMoveLine(handle, 'match').then(function () {1614 if(!line.mv_lines_match.length) {1615 self.valuenow++;1616 reconciled.push(handle);1617 line.reconciled = true;1618 if (line.type === 'accounts') {1619 account_ids.push(line.account_id.id);1620 } else {1621 partner_ids.push(line.partner_id);1622 }1623 }1624 }));1625 });1626 return Promise.all(defs).then(function () {1627 if (partner_ids.length) {1628 self._rpc({1629 model: 'res.partner',1630 method: 'mark_as_reconciled',1631 args: [partner_ids],1632 });1633 }1634 return {reconciled: reconciled, updated: _.difference(handles, reconciled)};1635 });1636 });1637 },1638 removeProposition: function (handle, id) {1639 var self = this;1640 var line = this.getLine(handle);1641 var defs = [];1642 var prop = _.find(line.reconciliation_proposition, {'id' : id});1643 if (prop) {1644 line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) {1645 return p.id !== prop.id && p.id !== prop.link && p.link !== prop.id && (!p.link || p.link !== prop.link);1646 });1647 line.mv_lines_match = line.mv_lines_match || [];1648 line.mv_lines_match.unshift(prop);1649 // No proposition left and then, reset the st_line partner.1650 if(line.reconciliation_proposition.length == 0 && line.st_line.has_no_partner)1651 defs.push(self.changePartner(line.handle));1652 }1653 line.mode = (id || line.mode !== "create") && isNaN(id) ? 'create' : 'match';1654 defs.push(this._computeLine(line));1655 return Promise.all(defs).then(function() {1656 return self.changeMode(handle, line.mode, true);1657 })1658 },1659 //--------------------------------------------------------------------------1660 // Private1661 //--------------------------------------------------------------------------1662 /**1663 * override change the balance type to display or not the reconcile button1664 *1665 * @override1666 * @private1667 * @param {Object} line1668 * @returns {Promise}1669 */1670 _computeLine: function (line) {1671 return this._super(line).then(function () {1672 var props = _.reject(line.reconciliation_proposition, 'invalid');1673 _.each(line.reconciliation_proposition, function(p) {1674 delete p.is_move_line;1675 });1676 line.balance.type = -1;1677 if (!line.balance.amount_currency && props.length) {1678 line.balance.type = 1;1679 } else if(_.any(props, function (prop) {return prop.amount > 0;}) &&1680 _.any(props, function (prop) {return prop.amount < 0;})) {1681 line.balance.type = 0;1682 }1683 });1684 },1685 /**1686 * Format each server lines and propositions and compute all lines1687 *1688 * @see '_computeLine'1689 *1690 * @private1691 * @param {'customers' | 'suppliers' | 'accounts'} type1692 * @param {Object} data1693 * @returns {Promise}1694 */1695 _formatLine: function (type, data) {1696 var line = this.lines[_.uniqueId('rline')] = _.extend(data, {1697 type: type,1698 reconciled: false,1699 mode: 'inactive',1700 limitMoveLines: this.limitMoveLines,1701 filter_match: "",1702 reconcileModels: this.reconcileModels,1703 account_id: this._formatNameGet([data.account_id, data.account_name]),1704 st_line: data,1705 visible: true1706 });1707 this._formatLineProposition(line, line.reconciliation_proposition);1708 if (!line.reconciliation_proposition.length) {1709 delete line.reconciliation_proposition;1710 }1711 return this._computeLine(line);1712 },1713 /**1714 * override to add journal_id1715 *1716 * @override1717 * @private1718 * @param {Object} line1719 * @param {Object} props1720 */1721 _formatLineProposition: function (line, props) {1722 var self = this;1723 this._super(line, props);1724 if (props.length) {1725 _.each(props, function (prop) {1726 var tmp_value = prop.debit || prop.credit;1727 prop.credit = prop.credit !== 0 ? 0 : tmp_value;1728 prop.debit = prop.debit !== 0 ? 0 : tmp_value;1729 prop.amount = -prop.amount;1730 prop.journal_id = self._formatNameGet(prop.journal_id || line.journal_id);1731 prop.to_check = !!prop.to_check;1732 });1733 }1734 },1735 /**1736 * override to add journal_id on tax_created_line1737 *1738 * @private1739 * @param {Object} line1740 * @param {Object} values1741 * @returns {Object}1742 */1743 _formatQuickCreate: function (line, values) {1744 // Add journal to created line1745 if (values && values.journal_id === undefined && line && line.createForm && line.createForm.journal_id) {1746 values.journal_id = line.createForm.journal_id;1747 }1748 return this._super(line, values);1749 },1750 /**1751 * @override1752 * @param {object} prop1753 * @returns {Boolean}1754 */1755 _isDisplayedProposition: function (prop) {1756 return !!prop.journal_id && this._super(prop);1757 },1758 /**1759 * @override1760 * @param {object} prop1761 * @returns {Boolean}1762 */1763 _isValid: function (prop) {1764 return prop.journal_id && this._super(prop);1765 },1766 /**1767 * Fetch 'account.move.line' propositions.1768 *1769 * @see '_formatMoveLine'1770 *1771 * @override1772 * @private1773 * @param {string} handle1774 * @returns {Promise}1775 */1776 _performMoveLine: function (handle, mode, limit) {1777 limit = limit || this.limitMoveLines;1778 var line = this.getLine(handle);1779 var excluded_ids = _.map(_.union(line.reconciliation_proposition, line.mv_lines_match), function (prop) {1780 return _.isNumber(prop.id) ? prop.id : null;1781 }).filter(id => id != null);1782 var filter = line.filter_match || "";1783 var args = [line.account_id.id, line.partner_id, excluded_ids, filter, 0, limit];1784 return this._rpc({1785 model: 'account.reconciliation.widget',1786 method: 'get_move_lines_for_manual_reconciliation',1787 args: args,1788 context: this.context,1789 })1790 .then(this._formatMoveLine.bind(this, handle, ''));1791 },1792 _formatToProcessReconciliation: function (line, prop) {1793 var result = this._super(line, prop);1794 result['date'] = prop.date;1795 return result;1796 },1797 _getDefaultMode: function(handle) {1798 var line = this.getLine(handle);1799 if (line.balance.amount === 0 && (!line.st_line.mv_lines_match || line.st_line.mv_lines_match.length === 0)) {1800 return 'inactive';1801 }1802 return line.mv_lines_match.length > 0 ? 'match' : 'create';1803 },1804 _formatMoveLine: function (handle, mode, mv_lines) {1805 var self = this;1806 var line = this.getLine(handle);1807 line.mv_lines_match = _.uniq((line.mv_lines_match || []).concat(mv_lines), l => l.id);1808 this._formatLineProposition(line, mv_lines);1809 if (line.mode !== 'create' && !line.mv_lines_match.length && !line.filter_match.length) {1810 line.mode = this.avoidCreate || !line.balance.amount ? 'inactive' : 'create';1811 if (line.mode === 'create') {1812 return this._computeLine(line).then(function () {1813 return self.createProposition(handle);1814 });1815 }1816 } else {1817 return this._computeLine(line);1818 }1819 },1820});1821return {1822 StatementModel: StatementModel,1823 ManualModel: ManualModel,1824};...
reconciliation_model.js
Source:reconciliation_model.js
1odoo.define('account.ReconciliationModel', function (require) {2"use strict";3var BasicModel = require('web.BasicModel');4var field_utils = require('web.field_utils');5var utils = require('web.utils');6var session = require('web.session');7var WarningDialog = require('web.CrashManager').WarningDialog;8var core = require('web.core');9var _t = core._t;10/**11 * Model use to fetch, format and update 'account.reconciliation.widget',12 * datas allowing reconciliation13 *14 * The statement internal structure::15 *16 * {17 * valuenow: integer18 * valuenow: valuemax19 * [bank_statement_line_id]: {20 * id: integer21 * display_name: string22 * }23 * reconcileModels: [object]24 * accounts: {id: code}25 * }26 *27 * The internal structure of each line is::28 *29 * {30 * balance: {31 * type: number - show/hide action button32 * amount: number - real amount33 * amount_str: string - formated amount34 * account_code: string35 * },36 * st_line: {37 * partner_id: integer38 * partner_name: string39 * }40 * mode: string ('inactive', 'match_rp', 'match_other', 'create')41 * reconciliation_proposition: {42 * id: number|string43 * partial_amount: number44 * invalid: boolean - through the invalid line (without account, label...)45 * account_code: string46 * date: string47 * date_maturity: string48 * label: string49 * amount: number - real amount50 * amount_str: string - formated amount51 * [already_paid]: boolean52 * [partner_id]: integer53 * [partner_name]: string54 * [account_code]: string55 * [journal_id]: {56 * id: integer57 * display_name: string58 * }59 * [ref]: string60 * [is_partially_reconciled]: boolean61 * [to_check]: boolean62 * [amount_currency_str]: string|false (amount in record currency)63 * }64 * mv_lines_match_rp: object - idem than reconciliation_proposition65 * mv_lines_match_other: object - idem than reconciliation_proposition66 * limitMoveLines: integer67 * filter: string68 * [createForm]: {69 * account_id: {70 * id: integer71 * display_name: string72 * }73 * tax_ids: {74 * id: integer75 * display_name: string76 * }77 * analytic_account_id: {78 * id: integer79 * display_name: string80 * }81 * analytic_tag_ids: {82 * }83 * label: string84 * amount: number,85 * [journal_id]: {86 * id: integer87 * display_name: string88 * }89 * }90 * }91 */92var StatementModel = BasicModel.extend({93 avoidCreate: false,94 quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_ids', 'force_tax_included', 'analytic_tag_ids', 'to_check'],95 // overridden in ManualModel96 modes: ['create', 'match_rp', 'match_other'],97 /**98 * @override99 *100 * @param {Widget} parent101 * @param {object} options102 */103 init: function (parent, options) {104 this._super.apply(this, arguments);105 this.reconcileModels = [];106 this.lines = {};107 this.valuenow = 0;108 this.valuemax = 0;109 this.alreadyDisplayed = [];110 this.domain = [];111 this.defaultDisplayQty = options && options.defaultDisplayQty || 10;112 this.limitMoveLines = options && options.limitMoveLines || 15;113 this.display_context = 'init';114 },115 //--------------------------------------------------------------------------116 // Public117 //--------------------------------------------------------------------------118 /**119 * add a reconciliation proposition from the matched lines120 * We also display a warning if the user tries to add 2 line with different121 * account type122 *123 * @param {string} handle124 * @param {number} mv_line_id125 * @returns {Promise}126 */127 addProposition: function (handle, mv_line_id) {128 var self = this;129 var line = this.getLine(handle);130 var prop = _.clone(_.find(line['mv_lines_'+line.mode], {'id': mv_line_id}));131 this._addProposition(line, prop);132 line['mv_lines_'+line.mode] = _.filter(line['mv_lines_'+line.mode], l => l['id'] != mv_line_id);133 // remove all non valid lines134 line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (prop) {return prop && !prop.invalid;});135 // Onchange the partner if not already set on the statement line.136 if(!line.st_line.partner_id && line.reconciliation_proposition137 && line.reconciliation_proposition.length == 1 && prop.partner_id && line.type === undefined){138 return this.changePartner(handle, {'id': prop.partner_id, 'display_name': prop.partner_name}, true);139 }140 return Promise.all([141 this._computeLine(line),142 this._performMoveLine(handle, 'match_rp', line.mode == 'match_rp'? 1 : 0),143 this._performMoveLine(handle, 'match_other', line.mode == 'match_other'? 1 : 0)144 ]);145 },146 /**147 * change the filter for the target line and fetch the new matched lines148 *149 * @param {string} handle150 * @param {string} filter151 * @returns {Promise}152 */153 changeFilter: function (handle, filter) {154 var line = this.getLine(handle);155 line['filter_'+line.mode] = filter;156 line['mv_lines_'+line.mode] = [];157 return this._performMoveLine(handle, line.mode);158 },159 /**160 * change the mode line ('inactive', 'match_rp', 'match_other', 'create'),161 * and fetch the new matched lines or prepare to create a new line162 *163 * ``match_rp``164 * display the matched lines from receivable/payable accounts, the user165 * can select the lines to apply there as proposition166 * ``match_other``167 * display the other matched lines, the user can select the lines to apply168 * there as proposition169 * ``create``170 * display fields and quick create button to create a new proposition171 * for the reconciliation172 *173 * @param {string} handle174 * @param {'inactive' | 'match_rp' | 'create'} mode175 * @returns {Promise}176 */177 changeMode: function (handle, mode) {178 var self = this;179 var line = this.getLine(handle);180 if (mode === 'default') {181 var match_requests = self.modes.filter(x => x.startsWith('match')).map(x => this._performMoveLine(handle, x))182 return Promise.all(match_requests).then(function() {183 return self.changeMode(handle, self._getDefaultMode(handle));184 });185 }186 if (mode === 'next') {187 var available_modes = self._getAvailableModes(handle)188 mode = available_modes[(available_modes.indexOf(line.mode) + 1) % available_modes.length];189 }190 line.mode = mode;191 if (['match_rp', 'match_other'].includes(line.mode)) {192 if (!(line['mv_lines_' + line.mode] && line['mv_lines_' + line.mode].length)) {193 return this._performMoveLine(handle, line.mode);194 } else {195 return this._formatMoveLine(handle, line.mode, []);196 }197 }198 if (line.mode === 'create') {199 return this.createProposition(handle);200 }201 return Promise.resolve();202 },203 /**204 * fetch the more matched lines205 *206 * @param {string} handle207 * @returns {Promise}208 */209 changeOffset: function (handle) {210 var line = this.getLine(handle);211 return this._performMoveLine(handle, line.mode);212 },213 /**214 * change the partner on the line and fetch the new matched lines215 *216 * @param {string} handle217 * @param {bool} preserveMode218 * @param {Object} partner219 * @param {string} partner.display_name220 * @param {number} partner.id221 * @returns {Promise}222 */223 changePartner: function (handle, partner, preserveMode) {224 var self = this;225 var line = this.getLine(handle);226 line.st_line.partner_id = partner && partner.id;227 line.st_line.partner_name = partner && partner.display_name || '';228 line.mv_lines_match_rp = [];229 line.mv_lines_match_other = [];230 return Promise.resolve(partner && this._changePartner(handle, partner.id))231 .then(function() {232 if(line.st_line.partner_id){233 _.each(line.reconciliation_proposition, function(prop){234 if(prop.partner_id != line.st_line.partner_id){235 line.reconciliation_proposition = [];236 return false;237 }238 });239 }240 return self._computeLine(line);241 })242 .then(function () {243 return self.changeMode(handle, preserveMode ? line.mode : 'default', true);244 })245 },246 /**247 * close the statement248 * @returns {Promise<number>} resolves to the res_id of the closed statements249 */250 closeStatement: function () {251 var self = this;252 return this._rpc({253 model: 'account.bank.statement.line',254 method: 'button_confirm_bank',255 args: [self.bank_statement_line_id.id],256 })257 .then(function () {258 return self.bank_statement_line_id.id;259 });260 },261 /**262 *263 * then open the first available line264 *265 * @param {string} handle266 * @returns {Promise}267 */268 createProposition: function (handle) {269 var line = this.getLine(handle);270 var prop = _.filter(line.reconciliation_proposition, '__focus');271 prop = this._formatQuickCreate(line);272 line.reconciliation_proposition.push(prop);273 line.createForm = _.pick(prop, this.quickCreateFields);274 return this._computeLine(line);275 },276 /**277 * Return context information and journal_id278 * @returns {Object} context279 */280 getContext: function () {281 return this.context;282 },283 /**284 * Return the lines that needs to be displayed by the widget285 *286 * @returns {Object} lines that are loaded and not yet displayed287 */288 getStatementLines: function () {289 var self = this;290 var linesToDisplay = _.pick(this.lines, function(value, key, object) {291 if (value.visible === true && self.alreadyDisplayed.indexOf(key) === -1) {292 self.alreadyDisplayed.push(key);293 return object;294 }295 });296 return linesToDisplay;297 },298 /**299 * Return a boolean telling if load button needs to be displayed or not300 * overridden in ManualModel301 *302 * @returns {boolean} true if load more button needs to be displayed303 */304 hasMoreLines: function () {305 var notDisplayed = _.filter(this.lines, function(line) { return !line.visible; });306 if (notDisplayed.length > 0) {307 return true;308 }309 return false;310 },311 /**312 * get the line data for this handle313 *314 * @param {Object} handle315 * @returns {Object}316 */317 getLine: function (handle) {318 return this.lines[handle];319 },320 /**321 * load data from322 *323 * - 'account.bank.statement' fetch the line id and bank_statement_id info324 * - 'account.reconcile.model' fetch all reconcile model (for quick add)325 * - 'account.account' fetch all account code326 * - 'account.reconciliation.widget' fetch each line data327 *328 * overridden in ManualModel329 * @param {Object} context330 * @param {number[]} context.statement_line_ids331 * @returns {Promise}332 */333 load: function (context) {334 var self = this;335 this.context = context;336 this.statement_line_ids = context.statement_line_ids;337 if (this.statement_line_ids === undefined) {338 // This could be undefined if the user pressed F5, take everything as fallback instead of rainbowman339 return self._rpc({340 model: 'account.bank.statement.line',341 method: 'search_read',342 fields: ['id'],343 domain: [['journal_id', '=?', context.active_id]],344 }).then(function (result) {345 self.statement_line_ids = result.map(r => r.id);346 return self.reload()347 })348 } else {349 return self.reload();350 }351 },352 /**353 * Load more bank statement line354 *355 * @param {integer} qty quantity to load356 * @returns {Promise}357 */358 loadMore: function(qty) {359 if (qty === undefined) {360 qty = this.defaultDisplayQty;361 }362 var ids = _.pluck(this.lines, 'id');363 ids = ids.splice(this.pagerIndex, qty);364 this.pagerIndex += qty;365 return this.loadData(ids, this._getExcludedIds());366 },367 /**368 * RPC method to load informations on lines369 * overridden in ManualModel370 *371 * @param {Array} ids ids of bank statement line passed to rpc call372 * @param {Array} excluded_ids list of move_line ids that needs to be excluded from search373 * @returns {Promise}374 */375 loadData: function(ids) {376 var self = this;377 var excluded_ids = this._getExcludedIds();378 return self._rpc({379 model: 'account.reconciliation.widget',380 method: 'get_bank_statement_line_data',381 args: [ids, excluded_ids],382 context: self.context,383 })384 .then(self._formatLine.bind(self));385 },386 /**387 * Reload all data388 */389 reload: function() {390 var self = this;391 self.alreadyDisplayed = [];392 self.lines = {};393 self.pagerIndex = 0;394 var def_statement = this._rpc({395 model: 'account.reconciliation.widget',396 method: 'get_bank_statement_data',397 kwargs: {"bank_statement_line_ids":self.statement_line_ids, "srch_domain":self.domain},398 context: self.context,399 })400 .then(function (statement) {401 self.statement = statement;402 self.bank_statement_line_id = self.statement_line_ids.length === 1 ? {id: self.statement_line_ids[0], display_name: statement.statement_name} : false;403 self.valuenow = self.valuenow || statement.value_min;404 self.valuemax = self.valuemax || statement.value_max;405 self.context.journal_id = statement.journal_id;406 _.each(statement.lines, function (res) {407 var handle = _.uniqueId('rline');408 self.lines[handle] = {409 id: res.st_line.id,410 partner_id: res.st_line.partner_id,411 handle: handle,412 reconciled: false,413 mode: 'inactive',414 mv_lines_match_rp: [],415 mv_lines_match_other: [],416 filter_match_rp: "",417 filter_match_other: "",418 reconciliation_proposition: [],419 reconcileModels: [],420 };421 });422 });423 var domainReconcile = [];424 if (self.context && self.context.company_ids) {425 domainReconcile.push(['company_id', 'in', self.context.company_ids]);426 }427 if (self.context && self.context.active_model === 'account.journal' && self.context.active_ids) {428 domainReconcile.push('|');429 domainReconcile.push(['match_journal_ids', '=', false]);430 domainReconcile.push(['match_journal_ids', 'in', self.context.active_ids]);431 }432 var def_reconcileModel = this._loadReconciliationModel({domainReconcile: domainReconcile});433 var def_account = this._rpc({434 model: 'account.account',435 method: 'search_read',436 fields: ['code'],437 })438 .then(function (accounts) {439 self.accounts = _.object(_.pluck(accounts, 'id'), _.pluck(accounts, 'code'));440 });441 var def_taxes = self._loadTaxes();442 return Promise.all([def_statement, def_reconcileModel, def_account, def_taxes]).then(function () {443 _.each(self.lines, function (line) {444 line.reconcileModels = self.reconcileModels;445 });446 var ids = _.pluck(self.lines, 'id');447 ids = ids.splice(0, self.defaultDisplayQty);448 self.pagerIndex = ids.length;449 return self._formatLine(self.statement.lines);450 });451 },452 _readAnalyticTags: function (params) {453 var self = this;454 this.analyticTags = {};455 if (!params || !params.res_ids || !params.res_ids.length) {456 return $.when();457 }458 var fields = (params && params.fields || []).concat(['id', 'display_name']);459 return this._rpc({460 model: 'account.analytic.tag',461 method: 'read',462 args: [463 params.res_ids,464 fields,465 ],466 }).then(function (tags) {467 for (var i=0; i<tags.length; i++) {468 var tag = tags[i];469 self.analyticTags[tag.id] = tag;470 }471 });472 },473 _loadReconciliationModel: function (params) {474 var self = this;475 return this._rpc({476 model: 'account.reconcile.model',477 method: 'search_read',478 domain: params.domainReconcile || [],479 })480 .then(function (reconcileModels) {481 var analyticTagIds = [];482 for (var i=0; i<reconcileModels.length; i++) {483 var modelTags = reconcileModels[i].analytic_tag_ids || [];484 for (var j=0; j<modelTags.length; j++) {485 if (analyticTagIds.indexOf(modelTags[j]) === -1) {486 analyticTagIds.push(modelTags[j]);487 }488 }489 }490 return self._readAnalyticTags({res_ids: analyticTagIds}).then(function () {491 for (var i=0; i<reconcileModels.length; i++) {492 var recModel = reconcileModels[i];493 var analyticTagData = [];494 var modelTags = reconcileModels[i].analytic_tag_ids || [];495 for (var j=0; j<modelTags.length; j++) {496 var tagId = modelTags[j];497 analyticTagData.push([tagId, self.analyticTags[tagId].display_name])498 }499 recModel.analytic_tag_ids = analyticTagData;500 }501 self.reconcileModels = reconcileModels;502 });503 });504 },505 _loadTaxes: function(){506 var self = this;507 self.taxes = {};508 return this._rpc({509 model: 'account.tax',510 method: 'search_read',511 fields: ['price_include', 'name'],512 }).then(function (taxes) {513 _.each(taxes, function(tax){514 self.taxes[tax.id] = {515 price_include: tax.price_include,516 display_name: tax.name,517 };518 });519 return taxes;520 });521 },522 /**523 * Add lines into the propositions from the reconcile model524 * Can add 2 lines, and each with its taxes. The second line become editable525 * in the create mode.526 *527 * @see 'updateProposition' method for more informations about the528 * 'amount_type'529 *530 * @param {string} handle531 * @param {integer} reconcileModelId532 * @returns {Promise}533 */534 quickCreateProposition: function (handle, reconcileModelId) {535 var self = this;536 var line = this.getLine(handle);537 var reconcileModel = _.find(this.reconcileModels, function (r) {return r.id === reconcileModelId;});538 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'];539 this._blurProposition(handle);540 var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields));541 focus.reconcileModelId = reconcileModelId;542 line.reconciliation_proposition.push(focus);543 var defs = [];544 if (reconcileModel.has_second_line) {545 defs.push(self._computeLine(line).then(function() {546 var second = {};547 _.each(fields, function (key) {548 second[key] = ("second_"+key) in reconcileModel ? reconcileModel["second_"+key] : reconcileModel[key];549 });550 var second_focus = self._formatQuickCreate(line, second);551 second_focus.reconcileModelId = reconcileModelId;552 line.reconciliation_proposition.push(second_focus);553 self._computeReconcileModels(handle, reconcileModelId);554 }))555 }556 return Promise.all(defs).then(function() {557 line.createForm = _.pick(focus, self.quickCreateFields);558 return self._computeLine(line);559 })560 },561 /**562 * Remove a proposition and switch to an active mode ('create' or 'match_rp' or 'match_other')563 * overridden in ManualModel564 *565 * @param {string} handle566 * @param {number} id (move line id)567 * @returns {Promise}568 */569 removeProposition: function (handle, id) {570 var self = this;571 var line = this.getLine(handle);572 var defs = [];573 var prop = _.find(line.reconciliation_proposition, {'id' : id});574 if (prop) {575 line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) {576 return p.id !== prop.id && p.id !== prop.link && p.link !== prop.id && (!p.link || p.link !== prop.link);577 });578 if (prop['reconcileModelId'] === undefined) {579 if (['receivable', 'payable', 'liquidity'].includes(prop.account_type)) {580 line.mv_lines_match_rp.unshift(prop);581 } else {582 line.mv_lines_match_other.unshift(prop);583 }584 }585 // No proposition left and then, reset the st_line partner.586 if(line.reconciliation_proposition.length == 0 && line.st_line.has_no_partner)587 defs.push(self.changePartner(line.handle));588 }589 line.mode = (id || line.mode !== "create") && isNaN(id) ? 'create' : 'match_rp';590 defs.push(this._computeLine(line));591 return Promise.all(defs).then(function() {592 return self.changeMode(handle, line.mode, true);593 })594 },595 getPartialReconcileAmount: function(handle, data) {596 var line = this.getLine(handle);597 var formatOptions = {598 currency_id: line.st_line.currency_id,599 noSymbol: true,600 };601 var prop = _.find(line.reconciliation_proposition, {'id': data.data});602 if (prop) {603 var amount = prop.partial_amount || prop.amount;604 // Check if we can get a partial amount that would directly set balance to zero605 var partial = Math.abs(line.balance.amount + amount);606 if (Math.abs(line.balance.amount) >= Math.abs(amount)) {607 amount = Math.abs(amount);608 } else if (partial <= Math.abs(prop.amount) && partial >= 0) {609 amount = partial;610 } else {611 amount = Math.abs(amount);612 }613 return field_utils.format.monetary(amount, {}, formatOptions);614 }615 },616 /**617 * Force the partial reconciliation to display the reconciliate button.618 *619 * @param {string} handle620 * @returns {Promise}621 */622 partialReconcile: function(handle, data) {623 var line = this.getLine(handle);624 var prop = _.find(line.reconciliation_proposition, {'id' : data.mvLineId});625 if (prop) {626 var amount = data.amount;627 try {628 amount = field_utils.parse.float(data.amount);629 }630 catch (err) {631 amount = NaN;632 }633 // Amount can't be greater than line.amount and can not be negative and must be a number634 // the amount we receive will be a string, so take sign of previous line amount in consideration in order to put635 // the amount in the correct left or right column636 if (amount >= Math.abs(prop.amount) || amount <= 0 || isNaN(amount)) {637 delete prop.partial_amount_str;638 delete prop.partial_amount;639 if (isNaN(amount) || amount < 0) {640 this.do_warn(_.str.sprintf(_t('The amount %s is not a valid partial amount'), data.amount));641 }642 return this._computeLine(line);643 }644 else {645 var format_options = { currency_id: line.st_line.currency_id };646 prop.partial_amount = (prop.amount > 0 ? 1 : -1)*amount;647 prop.partial_amount_str = field_utils.format.monetary(Math.abs(prop.partial_amount), {}, format_options);648 }649 }650 return this._computeLine(line);651 },652 /**653 * Change the value of the editable proposition line or create a new one.654 *655 * If the editable line comes from a reconcile model with 2 lines656 * and their 'amount_type' is "percent"657 * and their total equals 100% (this doesn't take into account the taxes658 * who can be included or not)659 * Then the total is recomputed to have 100%.660 *661 * @param {string} handle662 * @param {*} values663 * @returns {Promise}664 */665 updateProposition: function (handle, values) {666 var self = this;667 var line = this.getLine(handle);668 var prop = _.last(_.filter(line.reconciliation_proposition, '__focus'));669 if ('to_check' in values && values.to_check === false) {670 // check if we have another line with to_check and if yes don't change value of this proposition671 prop.to_check = line.reconciliation_proposition.some(function(rec_prop, index) {672 return rec_prop.id !== prop.id && rec_prop.to_check;673 });674 }675 if (!prop) {676 prop = this._formatQuickCreate(line);677 line.reconciliation_proposition.push(prop);678 }679 _.each(values, function (value, fieldName) {680 if (fieldName === 'analytic_tag_ids') {681 switch (value.operation) {682 case "ADD_M2M":683 // handle analytic_tag selection via drop down (single dict) and684 // full widget (array of dict)685 var vids = _.isArray(value.ids) ? value.ids : [value.ids];686 _.each(vids, function (val) {687 if (!_.findWhere(prop.analytic_tag_ids, {id: val.id})) {688 prop.analytic_tag_ids.push(val);689 }690 });691 break;692 case "FORGET":693 var id = self.localData[value.ids[0]].ref;694 prop.analytic_tag_ids = _.filter(prop.analytic_tag_ids, function (val) {695 return val.id !== id;696 });697 break;698 }699 }700 else if (fieldName === 'tax_ids') {701 switch(value.operation) {702 case "ADD_M2M":703 prop.__tax_to_recompute = true;704 if (!_.findWhere(prop.tax_ids, {id: value.ids.id})) {705 value.ids.price_include = self.taxes[value.ids.id] ? self.taxes[value.ids.id].price_include : false;706 prop.tax_ids.push(value.ids);707 }708 break;709 case "FORGET":710 prop.__tax_to_recompute = true;711 var id = self.localData[value.ids[0]].ref;712 prop.tax_ids = _.filter(prop.tax_ids, function (val) {713 return val.id !== id;714 });715 break;716 }717 }718 else {719 prop[fieldName] = values[fieldName];720 }721 });722 if ('account_id' in values) {723 prop.account_code = prop.account_id ? this.accounts[prop.account_id.id] : '';724 }725 if ('amount' in values) {726 prop.base_amount = values.amount;727 if (prop.reconcileModelId) {728 this._computeReconcileModels(handle, prop.reconcileModelId);729 }730 }731 if ('force_tax_included' in values || 'amount' in values || 'account_id' in values) {732 prop.__tax_to_recompute = true;733 }734 line.createForm = _.pick(prop, this.quickCreateFields);735 // If you check/uncheck the force_tax_included box, reset the createForm amount.736 if(prop.base_amount)737 line.createForm.amount = prop.base_amount;738 if (prop.tax_ids.length !== 1 ) {739 // When we have 0 or more than 1 taxes, reset the base_amount and force_tax_included, otherwise weird behavior can happen740 prop.amount = prop.base_amount;741 line.createForm.force_tax_included = false;742 }743 return this._computeLine(line);744 },745 /**746 * Format the value and send it to 'account.reconciliation.widget' model747 * Update the number of validated lines748 * overridden in ManualModel749 *750 * @param {(string|string[])} handle751 * @returns {Promise<Object>} resolved with an object who contains752 * 'handles' key753 */754 validate: function (handle) {755 var self = this;756 this.display_context = 'validate';757 var handles = [];758 if (handle) {759 handles = [handle];760 } else {761 _.each(this.lines, function (line, handle) {762 if (!line.reconciled && line.balance && !line.balance.amount && line.reconciliation_proposition.length) {763 handles.push(handle);764 }765 });766 }767 var ids = [];768 var values = [];769 var handlesPromises = [];770 _.each(handles, function (handle) {771 var line = self.getLine(handle);772 var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.invalid;});773 var computeLinePromise;774 if (props.length === 0) {775 // Usability: if user has not chosen any lines and click validate, it has the same behavior776 // as creating a write-off of the same amount.777 props.push(self._formatQuickCreate(line, {778 account_id: [line.st_line.open_balance_account_id, self.accounts[line.st_line.open_balance_account_id]],779 }));780 // update balance of line otherwise it won't be to zero and another line will be added781 line.reconciliation_proposition.push(props[0]);782 computeLinePromise = self._computeLine(line);783 }784 ids.push(line.id);785 handlesPromises.push(Promise.resolve(computeLinePromise).then(function() {786 var values_dict = {787 "partner_id": line.st_line.partner_id,788 "counterpart_aml_dicts": _.map(_.filter(props, function (prop) {789 return !isNaN(prop.id) && !prop.already_paid;790 }), self._formatToProcessReconciliation.bind(self, line)),791 "payment_aml_ids": _.pluck(_.filter(props, function (prop) {792 return !isNaN(prop.id) && prop.already_paid;793 }), 'id'),794 "new_aml_dicts": _.map(_.filter(props, function (prop) {795 return isNaN(prop.id) && prop.display;796 }), self._formatToProcessReconciliation.bind(self, line)),797 "to_check": line.to_check,798 };799 // If the lines are not fully balanced, create an unreconciled amount.800 // line.st_line.currency_id is never false here because its equivalent to801 // statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side).802 // see: get_statement_line_for_reconciliation_widget method in account/models/account_bank_statement.py for more details803 var currency = session.get_currency(line.st_line.currency_id);804 var balance = line.balance.amount;805 if (!utils.float_is_zero(balance, currency.digits[1])) {806 var unreconciled_amount_dict = {807 'account_id': line.st_line.open_balance_account_id,808 'credit': balance > 0 ? balance : 0,809 'debit': balance < 0 ? -balance : 0,810 'name': line.st_line.name + ' : ' + _t("Open balance"),811 };812 values_dict['new_aml_dicts'].push(unreconciled_amount_dict);813 }814 values.push(values_dict);815 line.reconciled = true;816 self.valuenow++;817 }));818 _.each(self.lines, function(other_line) {819 if (other_line != line) {820 var filtered_prop = other_line.reconciliation_proposition.filter(p => !line.reconciliation_proposition.map(l => l.id).includes(p.id));821 if (filtered_prop.length != other_line.reconciliation_proposition.length) {822 other_line.need_update = true;823 other_line.reconciliation_proposition = filtered_prop;824 }825 self._computeLine(line);826 }827 })828 });829 return Promise.all(handlesPromises).then(function() {830 return self._rpc({831 model: 'account.reconciliation.widget',832 method: 'process_bank_statement_line',833 args: [ids, values],834 context: self.context,835 })836 .then(self._validatePostProcess.bind(self))837 .then(function () {838 return {handles: handles};839 });840 });841 },842 //--------------------------------------------------------------------------843 // Private844 //--------------------------------------------------------------------------845 /**846 * add a line proposition after checking receivable and payable accounts constraint847 *848 * @private849 * @param {Object} line850 * @param {Object} prop851 */852 _addProposition: function (line, prop) {853 line.reconciliation_proposition.push(prop);854 },855 /**856 * stop the editable proposition line and remove it if it's invalid then857 * compute the line858 *859 * See :func:`_computeLine`860 *861 * @private862 * @param {string} handle863 * @returns {Promise}864 */865 _blurProposition: function (handle) {866 var line = this.getLine(handle);867 line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (l) {868 l.__focus = false;869 return !l.invalid;870 });871 },872 /**873 * When changing partner, read property_account_receivable and payable874 * of that partner because the counterpart account might cahnge depending875 * on the partner876 *877 * @private878 * @param {string} handle879 * @param {integer} partner_id880 * @returns {Promise}881 */882 _changePartner: function (handle, partner_id) {883 var self = this;884 return this._rpc({885 model: 'res.partner',886 method: 'read',887 args: [partner_id, ["property_account_receivable_id", "property_account_payable_id"]],888 }).then(function (result) {889 if (result.length > 0) {890 var line = self.getLine(handle);891 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];892 }893 });894 },895 /**896 * Calculates the balance; format each proposition amount_str and mark as897 * invalid the line with empty account_id, amount or label898 * Check the taxes server side for each updated propositions with tax_ids899 * extended by ManualModel900 *901 * @private902 * @param {Object} line903 * @returns {Promise}904 */905 _computeLine: function (line) {906 //balance_type907 var self = this;908 // compute taxes909 var tax_defs = [];910 var reconciliation_proposition = [];911 var formatOptions = {912 currency_id: line.st_line.currency_id,913 };914 line.to_check = false;915 _.each(line.reconciliation_proposition, function (prop) {916 if (prop.to_check) {917 // If one of the proposition is to_check, set the global to_check flag to true918 line.to_check = true;919 }920 if (prop.tax_repartition_line_id) {921 if (!_.find(line.reconciliation_proposition, {'id': prop.link}).__tax_to_recompute) {922 reconciliation_proposition.push(prop);923 }924 return;925 }926 if (!prop.already_paid && parseInt(prop.id)) {927 prop.is_move_line = true;928 }929 reconciliation_proposition.push(prop);930 if (prop.tax_ids && prop.tax_ids.length && prop.__tax_to_recompute && prop.base_amount) {931 reconciliation_proposition = _.filter(reconciliation_proposition, function (p) {932 return !p.tax_repartition_line_id || p.link !== prop.id;933 });934 var args = [prop.tax_ids.map(function(el){return el.id;}), prop.base_amount, formatOptions.currency_id];935 var add_context = {'round': true};936 if(prop.tax_ids.length === 1 && line.createForm.force_tax_included)937 add_context.force_price_include = true;938 tax_defs.push(self._rpc({939 model: 'account.tax',940 method: 'json_friendly_compute_all',941 args: args,942 context: $.extend({}, self.context || {}, add_context),943 })944 .then(function (result) {945 _.each(result.taxes, function(tax){946 var tax_prop = self._formatQuickCreate(line, {947 'link': prop.id,948 'tax_ids': tax.tax_ids,949 'tax_repartition_line_id': tax.tax_repartition_line_id,950 'tag_ids': tax.tag_ids,951 'amount': tax.amount,952 'label': prop.label ? prop.label + " " + tax.name : tax.name,953 'date': prop.date,954 'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id,955 'analytic': tax.analytic,956 '__focus': false957 });958 prop.tax_exigible = tax.tax_exigibility === 'on_payment' ? true : undefined;959 prop.amount = tax.base;960 prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);961 prop.invalid = !self._isValid(prop);962 tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, formatOptions);963 tax_prop.invalid = prop.invalid;964 reconciliation_proposition.push(tax_prop);965 });966 prop.tag_ids = result.base_tags;967 }));968 } else {969 prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);970 prop.display = self._isDisplayedProposition(prop);971 prop.invalid = !self._isValid(prop);972 }973 });974 return Promise.all(tax_defs).then(function () {975 _.each(reconciliation_proposition, function (prop) {976 prop.__tax_to_recompute = false;977 });978 line.reconciliation_proposition = reconciliation_proposition;979 var amount_currency = 0;980 var total = line.st_line.amount || 0;981 var isOtherCurrencyId = _.uniq(_.pluck(_.reject(reconciliation_proposition, 'invalid'), 'currency_id'));982 isOtherCurrencyId = isOtherCurrencyId.length === 1 && !total && isOtherCurrencyId[0] !== formatOptions.currency_id ? isOtherCurrencyId[0] : false;983 _.each(reconciliation_proposition, function (prop) {984 if (!prop.invalid) {985 total -= prop.partial_amount || prop.amount;986 if (isOtherCurrencyId) {987 amount_currency -= (prop.amount < 0 ? -1 : 1) * Math.abs(prop.amount_currency);988 }989 }990 });991 var company_currency = session.get_currency(line.st_line.currency_id);992 var company_precision = company_currency && company_currency.digits[1] || 2;993 total = utils.round_decimals(total, company_precision) || 0;994 if(isOtherCurrencyId){995 var other_currency = session.get_currency(isOtherCurrencyId);996 var other_precision = other_currency && other_currency.digits[1] || 2;997 amount_currency = utils.round_decimals(amount_currency, other_precision);998 }999 line.balance = {1000 amount: total,1001 amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions),1002 currency_id: isOtherCurrencyId,1003 amount_currency: isOtherCurrencyId ? amount_currency : total,1004 amount_currency_str: isOtherCurrencyId ? field_utils.format.monetary(Math.abs(amount_currency), {}, {1005 currency_id: isOtherCurrencyId1006 }) : false,1007 account_code: self.accounts[line.st_line.open_balance_account_id],1008 };1009 line.balance.show_balance = line.balance.amount_currency != 0;1010 line.balance.type = line.balance.amount_currency ? (line.st_line.partner_id ? 0 : -1) : 1;1011 });1012 },1013 /**1014 *1015 *1016 * @private1017 * @param {string} handle1018 * @param {integer} reconcileModelId1019 */1020 _computeReconcileModels: function (handle, reconcileModelId) {1021 var line = this.getLine(handle);1022 // if quick create with 2 lines who use 100%, change the both values in same time1023 var props = _.filter(line.reconciliation_proposition, {'reconcileModelId': reconcileModelId, '__focus': true});1024 if (props.length === 2 && props[0].percent && props[1].percent) {1025 if (props[0].percent + props[1].percent === 100) {1026 props[0].base_amount = props[0].amount = line.st_line.amount - props[1].base_amount;1027 props[0].__tax_to_recompute = true;1028 }1029 }1030 },1031 /**1032 * format a name_get into an object {id, display_name}, idempotent1033 *1034 * @private1035 * @param {Object|Array} [value] data or name_get1036 */1037 _formatNameGet: function (value) {1038 return value ? (value.id ? value : {'id': value[0], 'display_name': value[1]}) : false;1039 },1040 _formatMany2ManyTags: function (value) {1041 var res = [];1042 for (var i=0, len=value.length; i<len; i++) {1043 res[i] = {'id': value[i][0], 'display_name': value[i][1]};1044 }1045 return res;1046 },1047 _formatMany2ManyTagsTax: function(value) {1048 var res = [];1049 for (var i=0; i<value.length; i++) {1050 res.push({id: value[i], display_name: this.taxes[value[i]] ? this.taxes[value[i]].display_name : ''});1051 }1052 return res;1053 },1054 /**1055 * Format each propositions (amount, label, account_id)1056 * extended in ManualModel1057 *1058 * @private1059 * @param {Object} line1060 * @param {Object[]} props1061 */1062 _formatLineProposition: function (line, props) {1063 var self = this;1064 if (props.length) {1065 _.each(props, function (prop) {1066 prop.amount = prop.debit || -prop.credit;1067 prop.label = prop.name;1068 prop.account_id = self._formatNameGet(prop.account_id || line.account_id);1069 prop.is_partially_reconciled = prop.amount_str !== prop.total_amount_str;1070 prop.to_check = !!prop.to_check;1071 });1072 }1073 },1074 /**1075 * Format each server lines and propositions and compute all lines1076 * overridden in ManualModel1077 *1078 * @see '_computeLine'1079 *1080 * @private1081 * @param {Object[]} lines1082 * @returns {Promise}1083 */1084 _formatLine: function (lines) {1085 var self = this;1086 var defs = [];1087 _.each(lines, function (data) {1088 var line = _.find(self.lines, function (l) {1089 return l.id === data.st_line.id;1090 });1091 line.visible = true;1092 line.limitMoveLines = self.limitMoveLines;1093 _.extend(line, data);1094 self._formatLineProposition(line, line.reconciliation_proposition);1095 if (!line.reconciliation_proposition.length) {1096 delete line.reconciliation_proposition;1097 }1098 // No partner set on st_line and all matching amls have the same one: set it on the st_line.1099 defs.push(1100 self._computeLine(line)1101 .then(function(){1102 if(!line.st_line.partner_id && line.reconciliation_proposition.length > 0){1103 var hasDifferentPartners = function(prop){1104 return !prop.partner_id || prop.partner_id != line.reconciliation_proposition[0].partner_id;1105 };1106 if(!_.any(line.reconciliation_proposition, hasDifferentPartners)){1107 return self.changePartner(line.handle, {1108 'id': line.reconciliation_proposition[0].partner_id,1109 'display_name': line.reconciliation_proposition[0].partner_name,1110 }, true);1111 }1112 }else if(!line.st_line.partner_id && line.partner_id && line.partner_name){1113 return self.changePartner(line.handle, {1114 'id': line.partner_id,1115 'display_name': line.partner_name,1116 }, true);1117 }1118 return true;1119 })1120 .then(function(){1121 return data.write_off ? self.quickCreateProposition(line.handle, data.model_id) : true;1122 })1123 .then(function() {1124 // If still no partner set, take the one from context, if it exists1125 if (!line.st_line.partner_id && self.context.partner_id && self.context.partner_name) {1126 return self.changePartner(line.handle, {1127 'id': self.context.partner_id,1128 'display_name': self.context.partner_name,1129 }, true);1130 }1131 return true;1132 })1133 );1134 });1135 return Promise.all(defs);1136 },1137 /**1138 * Format the server value then compute the line1139 * overridden in ManualModel1140 *1141 * @see '_computeLine'1142 *1143 * @private1144 * @param {string} handle1145 * @param {Object[]} mv_lines1146 * @returns {Promise}1147 */1148 _formatMoveLine: function (handle, mode, mv_lines) {1149 var self = this;1150 var line = this.getLine(handle);1151 line['mv_lines_'+mode] = _.uniq(line['mv_lines_'+mode].concat(mv_lines), l => l.id);1152 if (mv_lines[0]){1153 line['remaining_'+mode] = mv_lines[0].recs_count - mv_lines.length;1154 } else if (line['mv_lines_'+mode].lenght == 0) {1155 line['remaining_'+mode] = 0;1156 }1157 this._formatLineProposition(line, mv_lines);1158 if ((line.mode == 'match_other' || line.mode == "match_rp") && !line['mv_lines_'+mode].length && !line['filter_'+mode].length) {1159 line.mode = self._getDefaultMode(handle);1160 if (line.mode !== 'match_rp' && line.mode !== 'match_other' && line.mode !== 'inactive') {1161 return this._computeLine(line).then(function () {1162 return self.createProposition(handle);1163 });1164 }1165 } else {1166 return this._computeLine(line);1167 }1168 },1169 /**1170 * overridden in ManualModel1171 */1172 _getDefaultMode: function(handle) {1173 var line = this.getLine(handle);1174 if (line.balance.amount === 01175 && (!line.st_line.mv_lines_match_rp || line.st_line.mv_lines_match_rp.length === 0)1176 && (!line.st_line.mv_lines_match_other || line.st_line.mv_lines_match_other.length === 0)) {1177 return 'inactive';1178 }1179 if (line.mv_lines_match_rp && line.mv_lines_match_rp.length) {1180 return 'match_rp';1181 }1182 if (line.mv_lines_match_other && line.mv_lines_match_other.length) {1183 return 'match_other';1184 }1185 return 'create';1186 },1187 _getAvailableModes: function(handle) {1188 var line = this.getLine(handle);1189 var modes = []1190 if (line.mv_lines_match_rp && line.mv_lines_match_rp.length) {1191 modes.push('match_rp')1192 }1193 if (line.mv_lines_match_other && line.mv_lines_match_other.length) {1194 modes.push('match_other')1195 }1196 modes.push('create')1197 return modes1198 },1199 /**1200 * Apply default values for the proposition, format datas and format the1201 * base_amount with the decimal number from the currency1202 * extended in ManualModel1203 *1204 * @private1205 * @param {Object} line1206 * @param {Object} values1207 * @returns {Object}1208 */1209 _formatQuickCreate: function (line, values) {1210 values = values || {};1211 var today = new moment().utc().format();1212 var account = this._formatNameGet(values.account_id);1213 var formatOptions = {1214 currency_id: line.st_line.currency_id,1215 };1216 var amount;1217 switch(values.amount_type) {1218 case 'percentage':1219 amount = line.balance.amount * values.amount / 100;1220 break;1221 case 'regex':1222 var matching = line.st_line.name.match(new RegExp(values.amount_from_label_regex))1223 amount = 0;1224 if (matching && matching.length == 2) {1225 matching = matching[1].replace(new RegExp('\\D' + values.decimal_separator, 'g'), '');1226 matching = matching.replace(values.decimal_separator, '.');1227 amount = parseFloat(matching) || 0;1228 amount = line.balance.amount > 0 ? amount : -amount;1229 }1230 break;1231 case 'fixed':1232 amount = values.amount;1233 break;1234 default:1235 amount = values.amount !== undefined ? values.amount : line.balance.amount;1236 }1237 var prop = {1238 'id': _.uniqueId('createLine'),1239 'label': values.label || line.st_line.name,1240 'account_id': account,1241 'account_code': account ? this.accounts[account.id] : '',1242 'analytic_account_id': this._formatNameGet(values.analytic_account_id),1243 'analytic_tag_ids': this._formatMany2ManyTags(values.analytic_tag_ids || []),1244 'journal_id': this._formatNameGet(values.journal_id),1245 'tax_ids': this._formatMany2ManyTagsTax(values.tax_ids || []),1246 'tag_ids': values.tag_ids,1247 'tax_repartition_line_id': values.tax_repartition_line_id,1248 'debit': 0,1249 'credit': 0,1250 'date': values.date ? values.date : field_utils.parse.date(today, {}, {isUTC: true}),1251 'force_tax_included': values.force_tax_included || false,1252 'base_amount': amount,1253 'percent': values.amount_type === "percentage" ? values.amount : null,1254 'link': values.link,1255 'display': true,1256 'invalid': true,1257 'to_check': !!values.to_check,1258 '__tax_to_recompute': true,1259 '__focus': '__focus' in values ? values.__focus : true,1260 };1261 if (prop.base_amount) {1262 // Call to format and parse needed to round the value to the currency precision1263 var sign = prop.base_amount < 0 ? -1 : 1;1264 var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, formatOptions);1265 prop.base_amount = sign * field_utils.parse.monetary(amount, {}, formatOptions);1266 }1267 prop.amount = prop.base_amount;1268 return prop;1269 },1270 /**1271 * Return list of account_move_line that has been selected and needs to be removed1272 * from other calls.1273 *1274 * @private1275 * @returns {Array} list of excluded ids1276 */1277 _getExcludedIds: function () {1278 var excludedIds = [];1279 _.each(this.lines, function(line) {1280 if (line.reconciliation_proposition) {1281 _.each(line.reconciliation_proposition, function(prop) {1282 if (parseInt(prop['id'])) {1283 excludedIds.push(prop['id']);1284 }1285 });1286 }1287 });1288 return excludedIds;1289 },1290 /**1291 * Defined whether the line is to be displayed or not. Here, we only display1292 * the line if it comes from the server or if an account is defined when it1293 * is created1294 * extended in ManualModel1295 *1296 * @private1297 * @param {object} prop1298 * @returns {Boolean}1299 */1300 _isDisplayedProposition: function (prop) {1301 return !isNaN(prop.id) || !!prop.account_id;1302 },1303 /**1304 * extended in ManualModel1305 * @private1306 * @param {object} prop1307 * @returns {Boolean}1308 */1309 _isValid: function (prop) {1310 return !isNaN(prop.id) || prop.account_id && prop.amount && prop.label && !!prop.label.length;1311 },1312 /**1313 * Fetch 'account.reconciliation.widget' propositions.1314 * overridden in ManualModel1315 *1316 * @see '_formatMoveLine'1317 *1318 * @private1319 * @param {string} handle1320 * @returns {Promise}1321 */1322 _performMoveLine: function (handle, mode, limit) {1323 limit = limit || this.limitMoveLines;1324 var line = this.getLine(handle);1325 var excluded_ids = _.map(_.union(line.reconciliation_proposition, line.mv_lines_match_rp, line.mv_lines_match_other), function (prop) {1326 return _.isNumber(prop.id) ? prop.id : null;1327 }).filter(id => id != null);1328 var filter = line['filter_'+mode] || "";1329 return this._rpc({1330 model: 'account.reconciliation.widget',1331 method: 'get_move_lines_for_bank_statement_line',1332 args: [line.id, line.st_line.partner_id, excluded_ids, filter, 0, limit, mode === 'match_rp' ? 'rp' : 'other'],1333 context: this.context,1334 })1335 .then(this._formatMoveLine.bind(this, handle, mode));1336 },1337 /**1338 * format the proposition to send information server side1339 * extended in ManualModel1340 *1341 * @private1342 * @param {object} line1343 * @param {object} prop1344 * @returns {object}1345 */1346 _formatToProcessReconciliation: function (line, prop) {1347 var amount = -prop.amount;1348 if (prop.partial_amount) {1349 amount = -prop.partial_amount;1350 }1351 var result = {1352 name : prop.label,1353 debit : amount > 0 ? amount : 0,1354 credit : amount < 0 ? -amount : 0,1355 tax_exigible: prop.tax_exigible,1356 analytic_tag_ids: [[6, null, _.pluck(prop.analytic_tag_ids, 'id')]]1357 };1358 if (!isNaN(prop.id)) {1359 result.counterpart_aml_id = prop.id;1360 } else {1361 result.account_id = prop.account_id.id;1362 if (prop.journal_id) {1363 result.journal_id = prop.journal_id.id;1364 }1365 }1366 if (!isNaN(prop.id)) result.counterpart_aml_id = prop.id;1367 if (prop.analytic_account_id) result.analytic_account_id = prop.analytic_account_id.id;1368 if (prop.tax_ids && prop.tax_ids.length) result.tax_ids = [[6, null, _.pluck(prop.tax_ids, 'id')]];1369 if (prop.tag_ids && prop.tag_ids.length) result.tag_ids = [[6, null, prop.tag_ids]];1370 if (prop.tax_repartition_line_id) result.tax_repartition_line_id = prop.tax_repartition_line_id;1371 if (prop.reconcileModelId) result.reconcile_model_id = prop.reconcileModelId1372 return result;1373 },1374 /**1375 * Hook to handle return values of the validate's line process.1376 *1377 * @private1378 * @param {Object} data1379 * @param {Object[]} data.moves list of processed account.move1380 * @returns {Deferred}1381 */1382 _validatePostProcess: function (data) {1383 var self = this;1384 return Promise.resolve();1385 },1386});1387/**1388 * Model use to fetch, format and update 'account.move.line' and 'res.partner'1389 * datas allowing manual reconciliation1390 */1391var ManualModel = StatementModel.extend({1392 quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_ids', 'force_tax_included', 'analytic_tag_ids', 'date', 'to_check'],1393 modes: ['create', 'match'],1394 //--------------------------------------------------------------------------1395 // Public1396 //--------------------------------------------------------------------------1397 /**1398 * Return a boolean telling if load button needs to be displayed or not1399 *1400 * @returns {boolean} true if load more button needs to be displayed1401 */1402 hasMoreLines: function () {1403 if (this.manualLines.length > this.pagerIndex) {1404 return true;1405 }1406 return false;1407 },1408 /**1409 * load data from1410 * - 'account.reconciliation.widget' fetch the lines to reconciliate1411 * - 'account.account' fetch all account code1412 *1413 * @param {Object} context1414 * @param {string} [context.mode] 'customers', 'suppliers' or 'accounts'1415 * @param {integer[]} [context.company_ids]1416 * @param {integer[]} [context.partner_ids] used for 'customers' and1417 * 'suppliers' mode1418 * @returns {Promise}1419 */1420 load: function (context) {1421 var self = this;1422 this.context = context;1423 var domain_account_id = [];1424 if (context && context.company_ids) {1425 domain_account_id.push(['company_id', 'in', context.company_ids]);1426 }1427 var def_account = this._rpc({1428 model: 'account.account',1429 method: 'search_read',1430 domain: domain_account_id,1431 fields: ['code'],1432 })1433 .then(function (accounts) {1434 self.account_ids = _.pluck(accounts, 'id');1435 self.accounts = _.object(self.account_ids, _.pluck(accounts, 'code'));1436 });1437 var domainReconcile = [];1438 var company_ids = context && context.company_ids || [session.company_id];1439 if (company_ids) {1440 domainReconcile.push(['company_id', 'in', company_ids]);1441 }1442 var def_reconcileModel = this._loadReconciliationModel({domainReconcile: domainReconcile});1443 var def_taxes = this._loadTaxes();1444 return Promise.all([def_reconcileModel, def_account, def_taxes]).then(function () {1445 switch(context.mode) {1446 case 'customers':1447 case 'suppliers':1448 var mode = context.mode === 'customers' ? 'receivable' : 'payable';1449 var args = ['partner', context.partner_ids || null, mode];1450 return self._rpc({1451 model: 'account.reconciliation.widget',1452 method: 'get_data_for_manual_reconciliation',1453 args: args,1454 context: context,1455 })1456 .then(function (result) {1457 self.manualLines = result;1458 self.valuenow = 0;1459 self.valuemax = Object.keys(self.manualLines).length;1460 var lines = self.manualLines.splice(0, self.defaultDisplayQty);1461 self.pagerIndex = lines.length;1462 return self.loadData(lines);1463 });1464 case 'accounts':1465 return self._rpc({1466 model: 'account.reconciliation.widget',1467 method: 'get_data_for_manual_reconciliation',1468 args: ['account', context.account_ids || self.account_ids],1469 context: context,1470 })1471 .then(function (result) {1472 self.manualLines = result;1473 self.valuenow = 0;1474 self.valuemax = Object.keys(self.manualLines).length;1475 var lines = self.manualLines.splice(0, self.defaultDisplayQty);1476 self.pagerIndex = lines.length;1477 return self.loadData(lines);1478 });1479 default:1480 var partner_ids = context.partner_ids || null;1481 var account_ids = context.account_ids || self.account_ids || null;1482 return self._rpc({1483 model: 'account.reconciliation.widget',1484 method: 'get_all_data_for_manual_reconciliation',1485 args: [partner_ids, account_ids],1486 context: context,1487 })1488 .then(function (result) {1489 // Flatten the result1490 self.manualLines = [].concat(result.accounts, result.customers, result.suppliers);1491 self.valuenow = 0;1492 self.valuemax = Object.keys(self.manualLines).length;1493 var lines = self.manualLines.splice(0, self.defaultDisplayQty);1494 self.pagerIndex = lines.length;1495 return self.loadData(lines);1496 });1497 }1498 });1499 },1500 /**1501 * Load more partners/accounts1502 * overridden in ManualModel1503 *1504 * @param {integer} qty quantity to load1505 * @returns {Promise}1506 */1507 loadMore: function(qty) {1508 if (qty === undefined) {1509 qty = this.defaultDisplayQty;1510 }1511 var lines = this.manualLines.splice(this.pagerIndex, qty);1512 this.pagerIndex += qty;1513 return this.loadData(lines);1514 },1515 /**1516 * Method to load informations on lines1517 *1518 * @param {Array} lines manualLines to load1519 * @returns {Promise}1520 */1521 loadData: function(lines) {1522 var self = this;1523 var defs = [];1524 _.each(lines, function (l) {1525 defs.push(self._formatLine(l.mode, l));1526 });1527 return Promise.all(defs);1528 },1529 /**1530 * Mark the account or the partner as reconciled1531 *1532 * @param {(string|string[])} handle1533 * @returns {Promise<Array>} resolved with the handle array1534 */1535 validate: function (handle) {1536 var self = this;1537 var handles = [];1538 if (handle) {1539 handles = [handle];1540 } else {1541 _.each(this.lines, function (line, handle) {1542 if (!line.reconciled && !line.balance.amount && line.reconciliation_proposition.length) {1543 handles.push(handle);1544 }1545 });1546 }1547 var def = Promise.resolve();1548 var process_reconciliations = [];1549 var reconciled = [];1550 _.each(handles, function (handle) {1551 var line = self.getLine(handle);1552 if(line.reconciled) {1553 return;1554 }1555 var props = line.reconciliation_proposition;1556 if (!props.length) {1557 self.valuenow++;1558 reconciled.push(handle);1559 line.reconciled = true;1560 process_reconciliations.push({1561 id: line.type === 'accounts' ? line.account_id : line.partner_id,1562 type: line.type,1563 mv_line_ids: [],1564 new_mv_line_dicts: [],1565 });1566 } else {1567 var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id');1568 var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id) && prop.display;}), self._formatToProcessReconciliation.bind(self, line));1569 process_reconciliations.push({1570 id: null,1571 type: null,1572 mv_line_ids: mv_line_ids,1573 new_mv_line_dicts: new_mv_line_dicts1574 });1575 }1576 line.reconciliation_proposition = [];1577 });1578 if (process_reconciliations.length) {1579 def = self._rpc({1580 model: 'account.reconciliation.widget',1581 method: 'process_move_lines',1582 args: [process_reconciliations],1583 });1584 }1585 return def.then(function() {1586 var defs = [];1587 var account_ids = [];1588 var partner_ids = [];1589 _.each(handles, function (handle) {1590 var line = self.getLine(handle);1591 if (line.reconciled) {1592 return;1593 }1594 line.filter = "";1595 defs.push(self._performMoveLine(handle, 'match').then(function () {1596 if(!line.mv_lines_match.length) {1597 self.valuenow++;1598 reconciled.push(handle);1599 line.reconciled = true;1600 if (line.type === 'accounts') {1601 account_ids.push(line.account_id.id);1602 } else {1603 partner_ids.push(line.partner_id.id);1604 }1605 }1606 }));1607 });1608 return Promise.all(defs).then(function () {1609 if (partner_ids.length) {1610 self._rpc({1611 model: 'res.partner',1612 method: 'mark_as_reconciled',1613 args: [partner_ids],1614 });1615 }1616 return {reconciled: reconciled, updated: _.difference(handles, reconciled)};1617 });1618 });1619 },1620 removeProposition: function (handle, id) {1621 var self = this;1622 var line = this.getLine(handle);1623 var defs = [];1624 var prop = _.find(line.reconciliation_proposition, {'id' : id});1625 if (prop) {1626 line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) {1627 return p.id !== prop.id && p.id !== prop.link && p.link !== prop.id && (!p.link || p.link !== prop.link);1628 });1629 line.mv_lines_match.unshift(prop);1630 // No proposition left and then, reset the st_line partner.1631 if(line.reconciliation_proposition.length == 0 && line.st_line.has_no_partner)1632 defs.push(self.changePartner(line.handle));1633 }1634 line.mode = (id || line.mode !== "create") && isNaN(id) ? 'create' : 'match';1635 defs.push(this._computeLine(line));1636 return Promise.all(defs).then(function() {1637 return self.changeMode(handle, line.mode, true);1638 })1639 },1640 //--------------------------------------------------------------------------1641 // Private1642 //--------------------------------------------------------------------------1643 /**1644 * override change the balance type to display or not the reconcile button1645 *1646 * @override1647 * @private1648 * @param {Object} line1649 * @returns {Promise}1650 */1651 _computeLine: function (line) {1652 return this._super(line).then(function () {1653 var props = _.reject(line.reconciliation_proposition, 'invalid');1654 _.each(line.reconciliation_proposition, function(p) {1655 delete p.is_move_line;1656 });1657 line.balance.type = -1;1658 if (!line.balance.amount_currency && props.length) {1659 line.balance.type = 1;1660 } else if(_.any(props, function (prop) {return prop.amount > 0;}) &&1661 _.any(props, function (prop) {return prop.amount < 0;})) {1662 line.balance.type = 0;1663 }1664 });1665 },1666 /**1667 * Format each server lines and propositions and compute all lines1668 *1669 * @see '_computeLine'1670 *1671 * @private1672 * @param {'customers' | 'suppliers' | 'accounts'} type1673 * @param {Object} data1674 * @returns {Promise}1675 */1676 _formatLine: function (type, data) {1677 var line = this.lines[_.uniqueId('rline')] = _.extend(data, {1678 type: type,1679 reconciled: false,1680 mode: 'inactive',1681 limitMoveLines: this.limitMoveLines,1682 filter: "",1683 reconcileModels: this.reconcileModels,1684 account_id: this._formatNameGet([data.account_id, data.account_name]),1685 st_line: data,1686 visible: true1687 });1688 this._formatLineProposition(line, line.reconciliation_proposition);1689 if (!line.reconciliation_proposition.length) {1690 delete line.reconciliation_proposition;1691 }1692 return this._computeLine(line);1693 },1694 /**1695 * override to add journal_id1696 *1697 * @override1698 * @private1699 * @param {Object} line1700 * @param {Object} props1701 */1702 _formatLineProposition: function (line, props) {1703 var self = this;1704 this._super(line, props);1705 if (props.length) {1706 _.each(props, function (prop) {1707 var tmp_value = prop.debit || prop.credit;1708 prop.credit = prop.credit !== 0 ? 0 : tmp_value;1709 prop.debit = prop.debit !== 0 ? 0 : tmp_value;1710 prop.amount = -prop.amount;1711 prop.journal_id = self._formatNameGet(prop.journal_id || line.journal_id);1712 prop.to_check = !!prop.to_check;1713 });1714 }1715 },1716 /**1717 * override to add journal_id on tax_created_line1718 *1719 * @private1720 * @param {Object} line1721 * @param {Object} values1722 * @returns {Object}1723 */1724 _formatQuickCreate: function (line, values) {1725 // Add journal to created line1726 if (values && values.journal_id === undefined && line && line.createForm && line.createForm.journal_id) {1727 values.journal_id = line.createForm.journal_id;1728 }1729 return this._super(line, values);1730 },1731 /**1732 * @override1733 * @param {object} prop1734 * @returns {Boolean}1735 */1736 _isDisplayedProposition: function (prop) {1737 return !!prop.journal_id && this._super(prop);1738 },1739 /**1740 * @override1741 * @param {object} prop1742 * @returns {Boolean}1743 */1744 _isValid: function (prop) {1745 return prop.journal_id && this._super(prop);1746 },1747 /**1748 * Fetch 'account.move.line' propositions.1749 *1750 * @see '_formatMoveLine'1751 *1752 * @override1753 * @private1754 * @param {string} handle1755 * @returns {Promise}1756 */1757 _performMoveLine: function (handle, mode, limit) {1758 limit = limit || this.limitMoveLines;1759 var line = this.getLine(handle);1760 var excluded_ids = _.map(_.union(line.reconciliation_proposition, line.mv_lines_match), function (prop) {1761 return _.isNumber(prop.id) ? prop.id : null;1762 }).filter(id => id != null);1763 var filter = line.filter || "";1764 var args = [line.account_id.id, line.partner_id, excluded_ids, filter, 0, limit];1765 return this._rpc({1766 model: 'account.reconciliation.widget',1767 method: 'get_move_lines_for_manual_reconciliation',1768 args: args,1769 context: this.context,1770 })1771 .then(this._formatMoveLine.bind(this, handle, ''));1772 },1773 _formatToProcessReconciliation: function (line, prop) {1774 var result = this._super(line, prop);1775 result['date'] = prop.date;1776 return result;1777 },1778 _getDefaultMode: function(handle) {1779 var line = this.getLine(handle);1780 if (line.balance.amount === 0 && (!line.st_line.mv_lines_match || line.st_line.mv_lines_match.length === 0)) {1781 return 'inactive';1782 }1783 return line.mv_lines_match.length > 0 ? 'match' : 'create';1784 },1785 _formatMoveLine: function (handle, mode, mv_lines) {1786 var self = this;1787 var line = this.getLine(handle);1788 line.mv_lines_match = _.uniq((line.mv_lines_match || []).concat(mv_lines), l => l.id);1789 this._formatLineProposition(line, mv_lines);1790 if (line.mode !== 'create' && !line.mv_lines_match.length && !line.filter.length) {1791 line.mode = this.avoidCreate || !line.balance.amount ? 'inactive' : 'create';1792 if (line.mode === 'create') {1793 return this._computeLine(line).then(function () {1794 return self.createProposition(handle);1795 });1796 }1797 } else {1798 return this._computeLine(line);1799 }1800 },1801});1802return {1803 StatementModel: StatementModel,1804 ManualModel: ManualModel,1805};...
estiModel.js
Source:estiModel.js
1/**2 * Created by kingw on 2015-10-31.3 */4var mysql = require('mysql');5var db_config = require('./db_config');6var pool = mysql.createPool(db_config);7var logger = require('../logger');8var async = require('async');9/*******************10 * Estimate Song List11 ********************/12exports.estiSongList = function(done){13 var sql =14 "SELECT DISTINCT(song_idx) song_idx, s.song_song, s.song_video, s.song_comment, u.user_freq, u.user_nickname " +15 "FROM atn_song s, atn_user u " +16 "WHERE s.song_user = u.user_idx " +17 "ORDER BY RAND() LIMIT 5"; // 5곡18 pool.query(sql, function(err, rows){19 if(err){20 logger.error("Estimate Song Send Error: ", err);21 done(false, "Estimate Song Send Error");22 }else{23 if(rows.length == 0){24 logger.error("Estimate Song Send no data");25 done(false, "Estimate Song Send Error"); // done callback26 }else{27 done(true, "success", rows);28 }29 }30 });31};32/*******************33 * Estimate Song Result34 ********************/35exports.estiResultSongFirst = function(data, done){36 pool.getConnection(function(err, conn){37 if(err){38 logger.error("Estimate Result First getConnection error:", err);39 done(false, "Estimate Result First getConnection error");40 }else{41 conn.beginTransaction(function(err){42 if(err){43 logger.error("Estimate Result First beginTransaction error:", err);44 done(false, "Estimate Result First beginTransaction error");45 conn.release();46 }else{47 async.waterfall([48 function(callback){49 var sql = "INSERT INTO atn_user() VALUE ()";50 conn.query(sql, function(err, rows){51 if(err){52 logger.error("Estimate Result First waterfall_1:", err);53 callback(err);54 }else{55 if(rows.length == 0){56 logger.error("Estimate Result First waterfall_2: no data");57 conn.rollback(function(){58 done(false, "Estimate Result First DB Error"); // error done callback59 conn.release();60 });61 }else{62 logger.info("rows.insertId:", rows.insertId);63 callback(null, rows.insertId);64 }65 }66 });67 },68 function(user_idx, callback){69 data.esti_user = user_idx; // ê°ì²´ì esti_user ì¶ê°70 var sql = "INSERT INTO atn_esti SET ?";71 conn.query(sql, data, function(err, rows){72 if(err){73 logger.error("Estimate Result First waterfall_3:", err);74 callback(err);75 }else{76 if(rows.affectedRows == 1){77 callback(null, user_idx);78 }else{79 conn.rollback(function(){80 logger.error("Estimate Result First waterfall_4");81 done(false, "Estimate Result First DB Error"); // error done callback82 conn.release();83 });84 }85 }86 });87 }88 ],89 function(err, user_idx){90 if(err){91 conn.rollback(function(){92 done(false, "Estimate Result First DB Error"); // error93 conn.release();94 });95 }else{96 conn.commit(function(err){97 if(err){98 logger.error("Estimate Result First Commit Error:", err);99 done(false, "Estimate Result First Commit Error"); // error100 conn.release();101 }else{102 done(true, "success", user_idx); // success103 conn.release();104 }105 });106 }107 }108 ); // waterfall109 }110 }); // beginTransaction111 }112 });113};114exports.estiResultSong = function(data, done){115 async.waterfall([116 function(callback){117 var sql = "SELECT * FROM atn_esti WHERE esti_user=? AND esti_song=?";118 pool.query(sql, [data.esti_user, data.esti_song], function(err, rows){119 if(err){120 logger.error("Estimate Result Waterfall Error_1:", err);121 callback(err);122 }else{123 callback(null, rows);124 }125 });126 },127 function(flag, callback){128 logger.info("flag:",flag);129 if(flag.length == 0){ // íê°íì§ ìì 곡130 var insert_sql = "INSERT INTO atn_esti SET ?";131 pool.query(insert_sql, data, function(err, rows){132 if(err){133 logger.error("Estimate Result Waterfall Error_2:", err);134 callback(err);135 }else{136 if(rows.affectedRows != 1){137 logger.error("Estimate Result Waterfall Error_3");138 done(false, "Estimate Result DB Error"); // callback ìì´ done139 }else{140 callback(null);141 }142 }143 });144 }else{ // ì´ë¯¸ íê°í곡145 var update_sql = "UPDATE atn_esti SET esti_esti=? WHERE esti_user=? AND esti_song=?";146 pool.query(update_sql, [data.esti_esti, data.esti_user, data.esti_song], function(err, rows){147 if(err){148 logger.error("Estimate Result Waterfall Error_4:", err);149 callback(err);150 }else{151 if(rows.affectedRows != 1){152 logger.error("Estimate Result Waterfall Error_5");153 done(false, "Estimate Result DB Error"); // callback ìì´ done154 }else{155 callback(null);156 }157 }158 });159 }160 }161 ],162 function(err){163 if(err){164 done(false, "Estimate Result Error");165 }else{166 done(true, "success");167 }168 }169 );170};171/*******************172 * Estimate Match173 ********************/174exports.estimate = function(uid, done){175 var sql = "SELECT esti_song, esti_esti FROM atn_esti WHERE esti_user = ?";176 pool.query(sql, uid, function(err, rows){177 if(err){178 logger.error("Estimate error:", err);179 done(err);180 }else{181 if(rows.length == 0){182 logger.error("No Estimate Data");183 done("No Estimate Data"); // my error code184 }else{185 done(null, rows);186 }187 }188 });189};190exports.otherList = function(uid, done){191 var sql =192 "SELECT user_idx, user_freq " +193 "FROM atn_user " +194 "WHERE user_freq IS NOT NULL AND user_idx != 1 AND user_idx != ? AND " +195 "user_idx NOT IN (SELECT bookmark_friend FROM atn_bookmark WHERE bookmark_my = ?)"; // 1ì ì´ìì196 pool.query(sql, [uid, uid], function(err, rows){197 if(err){198 logger.error("Other List error:", err);199 done(err);200 }else{201 if(rows.length == 0){202 logger.error("Other List No user");203 done("Other List No user"); // my error code204 }else{205 done(null, rows);206 }207 }208 });209};210exports.matchInsert = function(uid, other_idx, data, done){211 pool.getConnection(function(err, conn){212 if(err){213 logger.error("Match Insert getConnection error:", err);214 done(false, "Match Insert getConnection error");215 }else{216 conn.beginTransaction(function(err){217 if(err){218 logger.error("Match Insert beginTransaction error:", err);219 done(false, "Match Insert beginTransaction error");220 conn.release();221 }else{222 async.waterfall([223 function(callback){224 var sql = "Update atn_user SET user_partner=? WHERE user_idx=?";225 conn.query(sql, [other_idx, uid], function(err, rows){226 if(err){227 logger.error("Match Insert waterfall_1:", err);228 callback(err);229 }else{230 if(rows.affectedRows != 1){231 logger.error("Match Insert waterfall_2: no data");232 conn.rollback(function(){233 done(false, "Match Insert DB Error"); // error done callback234 conn.release();235 });236 }else{237 callback(null);238 }239 }240 });241 },242 function(callback){ // 기존 ë§¤ì¹ ê²°ê³¼ ìì 243 var sql = "SELECT COUNT(*) cnt FROM atn_match WHERE match_other=? AND match_my=?";244 conn.query(sql, [other_idx, uid], function(err, rows){245 if(err){246 logger.error("Match Insert waterfall_3:", err);247 callback(err);248 }else{249 if(rows[0].cnt){250 var delete_sql = "DELETE FROM atn_match WHERE match_other=? AND match_my=?";251 conn.query(delete_sql, [other_idx, uid], function(err, rows){252 if(err){253 logger.error("Match Insert waterfall_4:", err);254 callback(err);255 }else{256 callback(null);257 }258 });259 }else{260 callback(null);261 }262 }263 });264 },265 function(callback){266 if(data.length == 0){ // ë§¤ì¹ ê³¡ì´ ìì ììë267 callback(null);268 }else{269 var sql = "INSERT INTO atn_match(match_my, match_other, match_song) VALUES ?";270 logger.info("data:", [data]);271 conn.query(sql, [data], function(err, rows){272 if(err){273 logger.error("Match Insert waterfall_5:", err);274 callback(err);275 }else{276 if(rows.affectedRows == 0){277 conn.rollback(function(){278 logger.error("Match Insert waterfall_6");279 done(false, "Match Insert DB Error"); // error done callback280 conn.release();281 });282 }else{283 callback(null);284 }285 }286 });287 }288 }289 ],290 function(err){291 if(err){292 conn.rollback(function(){293 done(false, "Match Insert DB Error"); // error294 conn.release();295 });296 }else{297 conn.commit(function(err){298 if(err){299 logger.error("Match Insert Commit Error:", err);300 done(false, "Match Insert Commit Error"); // error301 conn.release();302 }else{303 done(true, "success"); // success304 conn.release();305 }306 });307 }308 }309 ); // waterfall310 }311 }); // beginTransaction312 }313 });314};315/*******************316 * Estimate Detail317 ********************/318exports.estiDetail = function(data, done){319 async.waterfall([320 function(callback){321 var sql = "SELECT user_partner FROM atn_user WHERE user_idx = ?";322 pool.query(sql, data, function(err, rows){323 if(err){324 logger.error("Estimate Detail waterfall_1: ", err);325 callback(err);326 }else{327 if(rows.length == 0){328 logger.error("No Estimate Detail Data_1");329 done(false, "No Estimate Detail Data_1"); // my error code330 }else{331 callback(null, rows[0].user_partner);332 }333 }334 });335 },336 function(partner_idx, callback){337 logger.info("partner_idx:", partner_idx);338 var sql =339 "SELECT user_freq, user_song, user_video, user_nickname, user_comment " +340 "FROM atn_user " +341 "WHERE user_idx = ?";342 pool.query(sql, partner_idx, function(err, rows){343 if(err){344 logger.error("Estimate Detail waterfall_2: ", err);345 callback(err);346 }else{347 if(rows.length == 0){348 logger.error("No Estimate Detail Data_2");349 done(false, "No Estimate Detail Data_2"); // my error code350 }else{351 callback(null, rows[0], partner_idx);352 }353 }354 });355 },356 function(partner, partner_idx, callback){357 logger.info("partner:", partner);358 var sql = "SELECT match_song FROM atn_match WHERE match_my = ? AND match_other = ?";359 pool.query(sql, [data, partner_idx], function(err, rows){360 if(err){361 logger.error("Estimate Detail waterfall_3: ", err);362 callback(err);363 }else{364 if(rows.length == 0){365 logger.error("No Estimate Detail Data_3");366 done(false, "No Estimate Detail Data_3"); // my error code367 }else{368 var song = [];369 var song_sql =370 "SELECT song_song, song_video, song_comment " +371 "FROM atn_song " +372 "WHERE song_idx = ?";373 async.each(rows, function(song_check, each_cb){374 logger.info("song_check:", song_check);375 var like_check = 1;376 var song_idx = song_check.match_song;377 if(song_check.match_song < 0){378 song_idx = song_idx * -1;379 like_check = -1;380 }381 pool.query(song_sql, song_idx, function(err, rows){382 if(err){383 logger.error("Estimate Detail Each Error:", err);384 each_cb(err);385 }else{386 logger.info("songs:", rows[0]);387 song.push({388 "song": rows[0].song_song,389 "video": rows[0].song_video,390 "comment": rows[0].song_comment,391 "like": like_check392 });393 each_cb();394 }395 });396 },397 function(err){398 if(err){399 callback(err);400 }else{401 callback(null, song, partner);402 }403 }404 ); // each405 }406 }407 });408 }409 ],410 function(err, song, partner){411 if(err){412 done(false, "Estimate Detail Error");413 }else{414 done(true, "success", song, partner);415 }416 }417 ); // waterfall418};419/*******************420 * Estimate Random421 ********************/422exports.estiRandom = function(uid, done){423 var sql =424 "SELECT user_idx, user_freq " +425 "FROM atn_user " +426 "WHERE user_freq IS NOT NULL AND user_idx != 1 AND user_idx != ? "+427 "ORDER BY RAND() LIMIT 1"; // 10곡// 21ì ì´ìì...
Snakefile
Source:Snakefile
1#BATCHES = [x for x in range(1,8)]2BATCHES = ["1"]3#WHITELIST_USE=TRUE4WHITELIST_EXPECTCELLS=965WHITELIST_CORRECT_THRESH=26GENOMEDIR="/storage/static_data/mm10/"7MAX_THREAD=48REF_GTF="/storage/static_data/mm10/mm10.gtf"9#BARCODE_FORMAT="CCCCCCNNNNNN"10BARCODE_FORMAT="NNNNNNCCCCCC"11BARCODE_EXTRACT="string"12BARCODES_EVEN="input_dir/barcodes001-096.txt"13BARCODES_ODD ="input_dir/barcodes097-192.txt"14results_dir="results_"+BARCODE_FORMAT15results_sample=results_dir+"/{sample}"16rule all:17 input:18 results_dir + "/complete_count_matrix.tsv"19rule mergeCounts:20 input:21 expand(results_sample + "/6_renamedmatrix/relabelled.matrix", sample = BATCHES)22 output:23 results_dir + "/complete_count_matrix.tsv"24 shell:25 "join -j 1 {input} > {output}"26rule renameCountHeaders:27 input:28 results_sample + "/5_countmatrix/counts.matrix"29 output:30 results_sample + "/6_renamedmatrix/relabelled.matrix"31 run:32 shell('''33 cat {input} | sed 1 s|\b[0-9]+_([ACTG]+)[_0-9.tx]+\b|F{sample}_\1|g > {output}34 ''')35rule countGenesPerCell:36 input:37 results_sample + "/4_sortedbam/sorted.bam"38 output:39 results_sample + "/5_countmatrix/counts.matrix"40 shell:41 "umi_tools counts --per-gene --gene-tag=XT --per-cell -I {input} -S {output}"42rule reindexBam:43 input:44 results_sample + "/3_featurecounts/Aligned.sortedByCoord.out.bam.featureCounts.bam"45 output:46 bam=results_sample + "/4_sortedbam/sorted.bam",47 bai=results_sample + "/4_sortedbam/sorted.bam.bai"48 run:49 shell("samtools sort {input} -o {output.bam}"),50 shell("samtools index {output.bam}")51rule reads2Genes:52 input:53 results_sample + "/2_starmap/Aligned.sortedByCoord.out.bam"54 output:55 bam=results_sample + "/3_featurecounts/Aligned.sortedByCoord.out.bam.featureCounts.bam",56 cm=results_sample + "/3_featurecounts/counts.tsv"57 params:58 threads=MAX_THREAD,59 gtf=REF_GTF,60 run:61 shell("featureCounts -a {params.gtf} -o {output.cm} -R BAM {input} -T {params.threads}"),62 shell("mv Aligned.sortedByCoord.out.bam.featureCounts.bam {output}")63rule mapReads:64 input:65 results_sample + "/1_umi_extract/R2_extracted.fastq"66 output:67 results_sample + "/2_starmap/Aligned.sortedByCoord.out.bam"68 params:69 gendir=GENOMEDIR,70 threads=MAX_THREAD,71 outfilterMMMax=1,72 readcommand="zcat",73 outSAMtype="BAM"74 run:75 shell('''76 STAR77 --readFilesIn {input}78 --runThreadN {params.threads}79 --genomeDir {params.gendir}80 --readFilesCommand {params.readcommand}81 --outFilterMultimapNmax {params.outfilterMMMax}82 --outSAMtype {params.outSAMtype} SortedByCoordinate83 '''),84 shell("mv Aligned.sortedByCoord.out.bam {output}")85rule whitelistBCsAndUmis:86 input:87 read1="input_dir/FACS{sample}_R1.fastq",88 read2="input_dir/FACS{sample}_R2.fastq",89 bar_even=results_sample + "/barcode_even.extracted",90 bar_odd =results_sample + "/barcode_odd.extracted"91 output:92 whitelist=results_sample + "/0_umi_whitelist/guessed_barcodes",93 log= results_sample + "/0_umi_whitelist/whitelist.log"94 params:95 plotprefix=results_sample + "/plots",96 expect_cells=WHITELIST_EXPECTCELLS,97 correct_thresh=WHITELIST_CORRECT_THRESH,98 bc_pattern=BARCODE_FORMAT,99 extract_method=BARCODE_EXTRACT100 shell:101 '''102 umi_tools whitelist\103 --extract-method='{params.extract_method}'\104 --bc-pattern='{params.bc_pattern}'\105 --set-cell-number={params.expect_cells}\106 --error-correct-threshold={params.correct_thresh}\107 --plot-prefix='{params.plotprefix}'\108 --method=umis\109 --stdin='{input.read1}'\110 --log='{output.log}'\111 --stdout {output.whitelist}112 '''113rule measureBarcodeOverlapWithWhitelist:114 input:115 guessed=results_sample + "/0_umi_whitelist/guessed_barcodes",116 bar_even=results_sample + "/__preproc/barcode_even.extracted",117 bar_odd =results_sample + "/__preproc/barcode_odd.extracted"118 output:119 results_sample + "/0_umi_whitelist/stats.txt"120 run:121 bar_used=input.bar_even if int(wildcards.sample)%2==0 else input.bar_odd122 designed_barcodes = {}123 MATCH_MAIN='matches_ḿain'124 MATCH_OTHER='matches_other'125 126 with open(bar_used, 'r') as f:127 for line in f:128 #num, barcode = line.split()129 barcode = line.strip()130 if barcode not in designed_barcodes:131 designed_barcodes[barcode] = {132 MATCH_MAIN : [],133 MATCH_OTHER: []134 }135 else:136 print("Duplicate:", barcode, file=sys.stderr)137 with open(input.guessed, 'r') as f:138 for line in f:139 tokens = line.split()140 barcode_main = tokens[0].strip()141 barcode_others = map(lambda x: x.strip(), tokens[1].split(','))142 # Direct match143 if barcode_main in designed_barcodes:144 designed_barcodes[barcode_main][MATCH_MAIN].append(barcode_main)145 # Matches with others146 for bar_other in barcode_others:147 if bar_other in designed_barcodes:148 # This other barcode exists in the designed barcodes, heres the149 # main barcode this other barcode it belongs to150 designed_barcodes[bar_other][MATCH_OTHER].append(barcode_main)151 with open(output[0], 'w') as fout:152 # Process map153 print("Wanted\tDirectMatch\tMergedWithOther", file=fout)154 155 for wanted_barcode in designed_barcodes:156 matches_main = designed_barcodes[wanted_barcode][MATCH_MAIN]157 match_w_other = designed_barcodes[wanted_barcode][MATCH_OTHER]158 print("%s\t%s\t%s" % (159 wanted_barcode, ','.join(matches_main), ','.join(match_w_other)160 ), file=fout)161 fout.close()162rule extractBCsAndUmis:163 input:164 stats=results_sample + "/0_umi_whitelist/stats.txt",165 # stats are not a required input, but good to be generated here166 read1="input_dir/FACS{sample}_R1.fastq",167 read2="input_dir/FACS{sample}_R2.fastq",168 whitelist=results_sample + "/0_umi_whitelist/guessed_barcodes"169 output:170 read1=results_sample + "/1_umi_extract/R1_extracted.fastq",171 read2=results_sample + "/1_umi_extract/R2_extracted.fastq",172 log=results_sample + "/1_umi_extract/log.txt"173 params:174 bc_pattern=BARCODE_FORMAT,175 extract_method=BARCODE_EXTRACT176 shell:177 '''178 umi_tools extract179 --error-correct-cell180 --filter-cell-barcode181 --whitelist='{input.whitelist}'182 --extract-method='{params.extract_method}'183 --bc-pattern='{params.bc_pattern}'184 --stdin='{input.read1}'185 --read2-in='{input.read2}'186 --stdout='{output.read1}'187 --read2-out='{output.read2}'188 --log='{output.log}'189 '''190rule extractBarcodes:191 input:192 odd=BARCODES_ODD,193 even=BARCODES_EVEN194 output:195 odd= results_sample + "/__preproc/barcode_odd.extracted",196 even=results_sample + "/__preproc/barcode_even.extracted"197 run:198 shell("cut -f 2 {input.odd} > {output.odd}" ),...
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!