[MERGE] forward port branch 11.0 up to 2156af480f
This commit is contained in:
@@ -1424,7 +1424,7 @@ class AccountInvoice(models.Model):
|
|||||||
res = {}
|
res = {}
|
||||||
for line in self.tax_line_ids:
|
for line in self.tax_line_ids:
|
||||||
res.setdefault(line.tax_id.tax_group_id, {'base': 0.0, 'amount': 0.0})
|
res.setdefault(line.tax_id.tax_group_id, {'base': 0.0, 'amount': 0.0})
|
||||||
res[line.tax_id.tax_group_id]['amount'] += line.amount
|
res[line.tax_id.tax_group_id]['amount'] += line.amount_total
|
||||||
res[line.tax_id.tax_group_id]['base'] += line.base
|
res[line.tax_id.tax_group_id]['base'] += line.base
|
||||||
res = sorted(res.items(), key=lambda l: l[0].sequence)
|
res = sorted(res.items(), key=lambda l: l[0].sequence)
|
||||||
res = [(
|
res = [(
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ class AccountMoveLine(models.Model):
|
|||||||
# TODO: put the invoice link and partner_id on the account_move
|
# TODO: put the invoice link and partner_id on the account_move
|
||||||
invoice_id = fields.Many2one('account.invoice', oldname="invoice")
|
invoice_id = fields.Many2one('account.invoice', oldname="invoice")
|
||||||
partner_id = fields.Many2one('res.partner', string='Partner', ondelete='restrict')
|
partner_id = fields.Many2one('res.partner', string='Partner', ondelete='restrict')
|
||||||
user_type_id = fields.Many2one('account.account.type', related='account_id.user_type_id', index=True, store=True, oldname="user_type")
|
user_type_id = fields.Many2one('account.account.type', related='account_id.user_type_id', index=True, store=True, oldname="user_type", readonly=True)
|
||||||
tax_exigible = fields.Boolean(string='Appears in VAT report', default=True,
|
tax_exigible = fields.Boolean(string='Appears in VAT report', default=True,
|
||||||
help="Technical field used to mark a tax line as exigible in the vat report or not (only exigible journal items are displayed). By default all new journal items are directly exigible, but with the feature cash_basis on taxes, some will become exigible only when the payment is recorded.")
|
help="Technical field used to mark a tax line as exigible in the vat report or not (only exigible journal items are displayed). By default all new journal items are directly exigible, but with the feature cash_basis on taxes, some will become exigible only when the payment is recorded.")
|
||||||
parent_state = fields.Char(compute="_compute_parent_state", help="State of the parent account.move")
|
parent_state = fields.Char(compute="_compute_parent_state", help="State of the parent account.move")
|
||||||
|
|||||||
@@ -330,6 +330,12 @@ class AccountReconciliation(models.AbstractModel):
|
|||||||
allowed_ids = set(Partner.browse(ids).ids)
|
allowed_ids = set(Partner.browse(ids).ids)
|
||||||
rows = [row for row in rows if row['partner_id'] in allowed_ids]
|
rows = [row for row in rows if row['partner_id'] in allowed_ids]
|
||||||
|
|
||||||
|
# Keep mode for future use in JS
|
||||||
|
if res_type == 'account':
|
||||||
|
mode = 'accounts'
|
||||||
|
else:
|
||||||
|
mode = 'customers' if account_type == 'receivable' else 'suppliers'
|
||||||
|
|
||||||
# Fetch other data
|
# Fetch other data
|
||||||
for row in rows:
|
for row in rows:
|
||||||
account = Account.browse(row['account_id'])
|
account = Account.browse(row['account_id'])
|
||||||
@@ -338,6 +344,7 @@ class AccountReconciliation(models.AbstractModel):
|
|||||||
partner_id = is_partner and row['partner_id'] or None
|
partner_id = is_partner and row['partner_id'] or None
|
||||||
rec_prop = self._get_move_line_reconciliation_proposition(account.id, partner_id)
|
rec_prop = self._get_move_line_reconciliation_proposition(account.id, partner_id)
|
||||||
row['reconciliation_proposition'] = self._prepare_move_lines(rec_prop, target_currency=currency)
|
row['reconciliation_proposition'] = self._prepare_move_lines(rec_prop, target_currency=currency)
|
||||||
|
row['mode'] = mode
|
||||||
row['company_id'] = account.company_id.id
|
row['company_id'] = account.company_id.id
|
||||||
|
|
||||||
# Return the partners with a reconciliation proposition first, since they are most likely to
|
# Return the partners with a reconciliation proposition first, since they are most likely to
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
|||||||
partners_amount = {}
|
partners_amount = {}
|
||||||
aml_ids = cr.fetchall()
|
aml_ids = cr.fetchall()
|
||||||
aml_ids = aml_ids and [x[0] for x in aml_ids] or []
|
aml_ids = aml_ids and [x[0] for x in aml_ids] or []
|
||||||
for line in self.env['account.move.line'].browse(aml_ids):
|
for line in self.env['account.move.line'].browse(aml_ids).with_context(prefetch_fields=False):
|
||||||
partner_id = line.partner_id.id or False
|
partner_id = line.partner_id.id or False
|
||||||
if partner_id not in partners_amount:
|
if partner_id not in partners_amount:
|
||||||
partners_amount[partner_id] = 0.0
|
partners_amount[partner_id] = 0.0
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ var StatementAction = AbstractAction.extend(ControlPanelMixin, {
|
|||||||
LineRenderer: ReconciliationRenderer.LineRenderer,
|
LineRenderer: ReconciliationRenderer.LineRenderer,
|
||||||
// used context params
|
// used context params
|
||||||
params: ['statement_ids'],
|
params: ['statement_ids'],
|
||||||
|
// number of statements/partners/accounts to display
|
||||||
|
defaultDisplayQty: 10,
|
||||||
// number of moves lines displayed in 'match' mode
|
// number of moves lines displayed in 'match' mode
|
||||||
limitMoveLines: 5,
|
limitMoveLines: 5,
|
||||||
},
|
},
|
||||||
@@ -59,6 +61,7 @@ var StatementAction = AbstractAction.extend(ControlPanelMixin, {
|
|||||||
this.params = params;
|
this.params = params;
|
||||||
this.model = new this.config.Model(this, {
|
this.model = new this.config.Model(this, {
|
||||||
modelName: "account.reconciliation.widget",
|
modelName: "account.reconciliation.widget",
|
||||||
|
defaultDisplayQty: params.params && params.params.defaultDisplayQty || this.config.defaultDisplayQty,
|
||||||
limitMoveLines: params.params && params.params.limitMoveLines || this.config.limitMoveLines,
|
limitMoveLines: params.params && params.params.limitMoveLines || this.config.limitMoveLines,
|
||||||
});
|
});
|
||||||
this.widgets = [];
|
this.widgets = [];
|
||||||
@@ -319,6 +322,7 @@ var ManualAction = StatementAction.extend({
|
|||||||
ActionRenderer: ReconciliationRenderer.ManualRenderer,
|
ActionRenderer: ReconciliationRenderer.ManualRenderer,
|
||||||
LineRenderer: ReconciliationRenderer.ManualLineRenderer,
|
LineRenderer: ReconciliationRenderer.ManualLineRenderer,
|
||||||
params: ['company_ids', 'mode', 'partner_ids', 'account_ids'],
|
params: ['company_ids', 'mode', 'partner_ids', 'account_ids'],
|
||||||
|
defaultDisplayQty: 30,
|
||||||
limitMoveLines: 10,
|
limitMoveLines: 10,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ var StatementModel = BasicModel.extend({
|
|||||||
this.valuenow = 0;
|
this.valuenow = 0;
|
||||||
this.valuemax = 0;
|
this.valuemax = 0;
|
||||||
this.alreadyDisplayed = [];
|
this.alreadyDisplayed = [];
|
||||||
this.defaultDisplayQty = 10;
|
this.defaultDisplayQty = options && options.defaultDisplayQty || 10;
|
||||||
this.limitMoveLines = options && options.limitMoveLines || 5;
|
this.limitMoveLines = options && options.limitMoveLines || 5;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1175,6 +1175,17 @@ var ManualModel = StatementModel.extend({
|
|||||||
// Public
|
// Public
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a boolean telling if load button needs to be displayed or not
|
||||||
|
*
|
||||||
|
* @returns {boolean} true if load more button needs to be displayed
|
||||||
|
*/
|
||||||
|
hasMoreLines: function () {
|
||||||
|
if (this.manualLines.length > this.pagerIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* load data from
|
* load data from
|
||||||
* - 'account.reconciliation.widget' fetch the lines to reconciliate
|
* - 'account.reconciliation.widget' fetch the lines to reconciliate
|
||||||
@@ -1234,10 +1245,12 @@ var ManualModel = StatementModel.extend({
|
|||||||
context: context,
|
context: context,
|
||||||
})
|
})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
var defs = _.map(result, self._formatLine.bind(self, context.mode));
|
self.manualLines = result;
|
||||||
self.valuenow = 0;
|
self.valuenow = 0;
|
||||||
self.valuemax = Object.keys(self.lines).length;
|
self.valuemax = Object.keys(self.manualLines).length;
|
||||||
return $.when.apply($, defs);
|
var lines = self.manualLines.splice(0, self.defaultDisplayQty);
|
||||||
|
self.pagerIndex = lines.length;
|
||||||
|
return self.loadData(lines);
|
||||||
});
|
});
|
||||||
case 'accounts':
|
case 'accounts':
|
||||||
return self._rpc({
|
return self._rpc({
|
||||||
@@ -1247,10 +1260,12 @@ var ManualModel = StatementModel.extend({
|
|||||||
context: context,
|
context: context,
|
||||||
})
|
})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
var defs = _.map(result, self._formatLine.bind(self, 'accounts'));
|
self.manualLines = result;
|
||||||
self.valuenow = 0;
|
self.valuenow = 0;
|
||||||
self.valuemax = Object.keys(self.lines).length;
|
self.valuemax = Object.keys(self.manualLines).length;
|
||||||
return $.when.apply($, defs);
|
var lines = self.manualLines.splice(0, self.defaultDisplayQty);
|
||||||
|
self.pagerIndex = lines.length;
|
||||||
|
return self.loadData(lines);
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
var partner_ids = context.partner_ids;
|
var partner_ids = context.partner_ids;
|
||||||
@@ -1266,16 +1281,46 @@ var ManualModel = StatementModel.extend({
|
|||||||
context: context,
|
context: context,
|
||||||
})
|
})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
var defs = _.map(result.accounts, self._formatLine.bind(self, 'accounts'));
|
// Flatten the result
|
||||||
defs = defs.concat(_.map(result.customers, self._formatLine.bind(self, 'customers')));
|
self.manualLines = [].concat(result.accounts, result.customers, result.suppliers)
|
||||||
defs = defs.concat(_.map(result.suppliers, self._formatLine.bind(self, 'suppliers')));
|
|
||||||
self.valuenow = 0;
|
self.valuenow = 0;
|
||||||
self.valuemax = Object.keys(self.lines).length;
|
self.valuemax = Object.keys(self.manualLines).length;
|
||||||
return $.when.apply($, defs);
|
var lines = self.manualLines.splice(0, self.defaultDisplayQty);
|
||||||
|
self.pagerIndex = lines.length;
|
||||||
|
return self.loadData(lines);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Load more partners/accounts
|
||||||
|
*
|
||||||
|
* @param {integer} qty quantity to load
|
||||||
|
* @returns {Deferred}
|
||||||
|
*/
|
||||||
|
loadMore: function(qty) {
|
||||||
|
if (qty === undefined) {
|
||||||
|
qty = this.defaultDisplayQty;
|
||||||
|
}
|
||||||
|
var lines = this.manualLines.splice(this.pagerIndex, qty);
|
||||||
|
this.pagerIndex += qty;
|
||||||
|
return this.loadData(lines);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Method to load informations on lines
|
||||||
|
*
|
||||||
|
* @param {Array} lines manualLines to load
|
||||||
|
* @returns {Deferred}
|
||||||
|
*/
|
||||||
|
loadData: function(lines) {
|
||||||
|
var self = this;
|
||||||
|
var defs = [];
|
||||||
|
_.each(lines, function (l) {
|
||||||
|
defs.push(self._formatLine(l.mode, l))
|
||||||
|
});
|
||||||
|
return $.when.apply($, defs);
|
||||||
|
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Mark the account or the partner as reconciled
|
* Mark the account or the partner as reconciled
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -436,12 +436,12 @@ var auto_reconciliation = {
|
|||||||
var data_for_manual_reconciliation_widget = {
|
var data_for_manual_reconciliation_widget = {
|
||||||
'[null,null]': {
|
'[null,null]': {
|
||||||
'customers': [
|
'customers': [
|
||||||
{'account_id': 287, 'partner_name': "Agrolait", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-14 12:30:31", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 8, 'account_name': "101200 Account Receivable"},
|
{'account_id': 287, 'partner_name': "Agrolait", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-14 12:30:31", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'mode': "customers"},
|
||||||
{'account_id': 7, 'partner_name': "Camptocamp", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-13 14:24:55", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 12, 'account_name': "101200 Account Receivable"}
|
{'account_id': 7, 'partner_name': "Camptocamp", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-13 14:24:55", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'mode': "customers"}
|
||||||
],
|
],
|
||||||
'accounts': [
|
'accounts': [
|
||||||
{
|
{
|
||||||
'account_id': 283, 'account_name': "101000 Current Assets", 'currency_id': 3, 'max_date': "2017-02-16 14:32:04", 'last_time_entries_checked': "2017-02-16", 'account_code': "101000",
|
'account_id': 283, 'account_name': "101000 Current Assets", 'currency_id': 3, 'max_date': "2017-02-16 14:32:04", 'last_time_entries_checked': "2017-02-16", 'account_code': "101000", 'mode': "accounts",
|
||||||
'reconciliation_proposition': [
|
'reconciliation_proposition': [
|
||||||
{'account_id': 283, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-16", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 8, 'account_name': "101000 Current Assets", 'name': "BNK1/2017/0006: Customer Payment", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 399, 'credit': 1000.0, 'journal_id': [3, "Bank"], 'amount_str': "$ 1,000.00", 'debit': 0.0, 'account_code': "101000", 'ref': "", 'already_paid': false},
|
{'account_id': 283, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-16", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 8, 'account_name': "101000 Current Assets", 'name': "BNK1/2017/0006: Customer Payment", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 399, 'credit': 1000.0, 'journal_id': [3, "Bank"], 'amount_str': "$ 1,000.00", 'debit': 0.0, 'account_code': "101000", 'ref': "", 'already_paid': false},
|
||||||
{'account_id': 283, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-03-18", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 8, 'account_name': "101000 Current Assets", 'name': "INV/2017/0006", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 402, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 1,000.00", 'debit': 1000.0, 'account_code': "101000", 'ref': "", 'already_paid': false}
|
{'account_id': 283, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-03-18", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 8, 'account_name': "101000 Current Assets", 'name': "INV/2017/0006", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 402, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 1,000.00", 'debit': 1000.0, 'account_code': "101000", 'ref': "", 'already_paid': false}
|
||||||
@@ -455,20 +455,20 @@ var data_for_manual_reconciliation_widget = {
|
|||||||
{'account_id': 284, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-16", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 8, 'account_name': "101000 Current Assets", 'name': "BNK1/999: Customer Payment", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 999, 'credit': 1000.0, 'journal_id': [3, "Bank"], 'amount_str': "$ 1,000.00", 'debit': 0.0, 'account_code': "111100", 'ref': "", 'already_paid': false},
|
{'account_id': 284, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-16", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 8, 'account_name': "101000 Current Assets", 'name': "BNK1/999: Customer Payment", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 999, 'credit': 1000.0, 'journal_id': [3, "Bank"], 'amount_str': "$ 1,000.00", 'debit': 0.0, 'account_code': "111100", 'ref': "", 'already_paid': false},
|
||||||
{'account_id': 284, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-03-18", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 8, 'account_name': "101000 Current Assets", 'name': "INV/998", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 998, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 1,000.00", 'debit': 1000.0, 'account_code': "111100", 'ref': "", 'already_paid': false}
|
{'account_id': 284, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-03-18", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 8, 'account_name': "101000 Current Assets", 'name': "INV/998", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 998, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 1,000.00", 'debit': 1000.0, 'account_code': "111100", 'ref': "", 'already_paid': false}
|
||||||
],
|
],
|
||||||
'currency_id': 3, 'max_date': "2017-02-14 12:36:05", 'last_time_entries_checked': null, 'account_code': "111100", 'partner_id': 8, 'account_name': "Account Payable"
|
'currency_id': 3, 'max_date': "2017-02-14 12:36:05", 'last_time_entries_checked': null, 'account_code': "111100", 'partner_id': 8, 'account_name': "Account Payable", 'mode': "suppliers"
|
||||||
}, {
|
}, {
|
||||||
'account_id': 284, 'partner_name': "Camptocamp",
|
'account_id': 284, 'partner_name': "Camptocamp",
|
||||||
'reconciliation_proposition': [
|
'reconciliation_proposition': [
|
||||||
{'account_id': 284, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-16", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 12, 'account_name': "101000 Current Assets", 'name': "BNK1/1999: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 1999, 'credit': 1000.0, 'journal_id': [3, "Bank"], 'amount_str': "$ 1,000.00", 'debit': 0.0, 'account_code': "111100", 'ref': "", 'already_paid': false},
|
{'account_id': 284, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-16", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 12, 'account_name': "101000 Current Assets", 'name': "BNK1/1999: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 1999, 'credit': 1000.0, 'journal_id': [3, "Bank"], 'amount_str': "$ 1,000.00", 'debit': 0.0, 'account_code': "111100", 'ref': "", 'already_paid': false},
|
||||||
{'account_id': 284, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-03-18", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 12, 'account_name': "101000 Current Assets", 'name': "INV/1998", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 1998, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 1,000.00", 'debit': 1000.0, 'account_code': "111100", 'ref': "", 'already_paid': false}
|
{'account_id': 284, 'account_type': "other", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-03-18", 'date': "2017-02-16", 'total_amount_str': "$ 1,000.00", 'partner_id': 12, 'account_name': "101000 Current Assets", 'name': "INV/1998", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 1998, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 1,000.00", 'debit': 1000.0, 'account_code': "111100", 'ref': "", 'already_paid': false}
|
||||||
],
|
],
|
||||||
'currency_id': 3, 'max_date': "2017-02-14 12:36:05", 'last_time_entries_checked': null, 'account_code': "111100", 'partner_id': 12, 'account_name': "Account Payable"
|
'currency_id': 3, 'max_date': "2017-02-14 12:36:05", 'last_time_entries_checked': null, 'account_code': "111100", 'partner_id': 12, 'account_name': "Account Payable", 'mode': "suppliers"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
'["partner",null,"receivable"]': [
|
'["partner",null,"receivable"]': [
|
||||||
{'account_id': 287, 'partner_name': "Agrolait", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-14 12:30:31", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 8, 'account_name': "101200 Account Receivable"},
|
{'account_id': 287, 'partner_name': "Agrolait", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-14 12:30:31", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'mode': "customers"},
|
||||||
{'account_id': 287, 'partner_name': "Camptocamp", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-13 14:24:55", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 12, 'account_name': "101200 Account Receivable"}
|
{'account_id': 287, 'partner_name': "Camptocamp", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-13 14:24:55", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'mode': "customers"}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class AccountMoveLineReconcile(models.TransientModel):
|
|||||||
credit += line.credit
|
credit += line.credit
|
||||||
debit += line.debit
|
debit += line.debit
|
||||||
precision = self.env.user.company_id.currency_id.decimal_places
|
precision = self.env.user.company_id.currency_id.decimal_places
|
||||||
writeoff = float_round(debit - credit, precision_digits=precision)
|
writeoff = float_round(debit - credit, precision_digits=precision) + 0.0 #remove -0.0 result
|
||||||
credit = float_round(credit, precision_digits=precision)
|
credit = float_round(credit, precision_digits=precision)
|
||||||
debit = float_round(debit, precision_digits=precision)
|
debit = float_round(debit, precision_digits=precision)
|
||||||
return {'trans_nbr': len(lines), 'credit': credit, 'debit': debit, 'writeoff': writeoff}
|
return {'trans_nbr': len(lines), 'credit': credit, 'debit': debit, 'writeoff': writeoff}
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ class MrpProduction(models.Model):
|
|||||||
if move:
|
if move:
|
||||||
if quantity > 0:
|
if quantity > 0:
|
||||||
move[0].write({'product_uom_qty': quantity})
|
move[0].write({'product_uom_qty': quantity})
|
||||||
|
move.unit_factor = quantity / move.raw_material_production_id.product_qty
|
||||||
elif quantity < 0: # Do not remove 0 lines
|
elif quantity < 0: # Do not remove 0 lines
|
||||||
if move[0].quantity_done > 0:
|
if move[0].quantity_done > 0:
|
||||||
raise UserError(_('Lines need to be deleted, but can not as you still have some quantities to consume in them. '))
|
raise UserError(_('Lines need to be deleted, but can not as you still have some quantities to consume in them. '))
|
||||||
|
|||||||
@@ -1558,11 +1558,21 @@ var ReceiptScreenWidget = ScreenWidget.extend({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
print_web: function() {
|
print_web: function() {
|
||||||
if($.browser.safari){
|
if ($.browser.safari) {
|
||||||
document.execCommand('print', false, null);
|
document.execCommand('print', false, null);
|
||||||
}
|
} else {
|
||||||
else{
|
try {
|
||||||
window.print();
|
window.print();
|
||||||
|
} catch(err) {
|
||||||
|
if (navigator.userAgent.toLowerCase().indexOf("android") > -1) {
|
||||||
|
this.gui.show_popup('error',{
|
||||||
|
'title':_t('Printing is not supported on some android browsers'),
|
||||||
|
'body': _t('Printing is not supported on some android browsers due to no default printing protocol is available. It is possible to print your tickets by making use of an IoT Box.'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.pos.get_order()._printed = true;
|
this.pos.get_order()._printed = true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ class ReportIntrastat(models.Model):
|
|||||||
intrastat.id as intrastat_id,
|
intrastat.id as intrastat_id,
|
||||||
upper(inv_country.code) as code,
|
upper(inv_country.code) as code,
|
||||||
sum(case when inv_line.price_unit is not null
|
sum(case when inv_line.price_unit is not null
|
||||||
then inv_line.price_unit * inv_line.quantity
|
then inv_line.price_unit * (1.0 - coalesce(inv_line.discount, 0.0) / 100.0) * inv_line.quantity
|
||||||
else 0
|
else 0
|
||||||
end) as value,
|
end) as value,
|
||||||
sum(
|
sum(
|
||||||
case when uom.category_id != puom.category_id then (pt.weight * inv_line.quantity)
|
case when uom.category_id != puom.category_id then (coalesce(nullif(pp.weight, 0), pt.weight) * inv_line.quantity)
|
||||||
else (pt.weight * inv_line.quantity * uom.factor) end
|
else (coalesce(nullif(pp.weight, 0), pt.weight) * inv_line.quantity * uom.factor) end
|
||||||
) as weight,
|
) as weight,
|
||||||
sum(
|
sum(
|
||||||
case when uom.category_id != puom.category_id then inv_line.quantity
|
case when uom.category_id != puom.category_id then inv_line.quantity
|
||||||
|
|||||||
@@ -3241,6 +3241,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
_.each(element._changes, function (command) {
|
_.each(element._changes, function (command) {
|
||||||
if (command.operation === 'DELETE' ||
|
if (command.operation === 'DELETE' ||
|
||||||
command.operation === 'FORGET' ||
|
command.operation === 'FORGET' ||
|
||||||
|
(command.operation === 'ADD' && !command.isNew)||
|
||||||
command.operation === 'REMOVE_ALL') {
|
command.operation === 'REMOVE_ALL') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6246,6 +6246,51 @@ QUnit.module('relational_fields', {
|
|||||||
form.destroy();
|
form.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('one2many, onchange, edition and multipage...', function (assert) {
|
||||||
|
assert.expect(7);
|
||||||
|
|
||||||
|
this.data.partner.onchanges = {
|
||||||
|
turtles: function (obj) {
|
||||||
|
obj.turtles = [[5]].concat(obj.turtles);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.data.partner.records[0].turtles = [1,2,3];
|
||||||
|
|
||||||
|
var form = createView({
|
||||||
|
View: FormView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch:'<form string="Partners">' +
|
||||||
|
'<field name="turtles">' +
|
||||||
|
'<tree editable="bottom" limit="2">' +
|
||||||
|
'<field name="turtle_foo"/>' +
|
||||||
|
'</tree>' +
|
||||||
|
'</field>' +
|
||||||
|
'</form>',
|
||||||
|
res_id: 1,
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
assert.step(args.method + ' ' + args.model)
|
||||||
|
return this._super(route, args);
|
||||||
|
},
|
||||||
|
viewOptions: {
|
||||||
|
mode: 'edit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
form.$('.o_field_x2many_list_row_add a').click();
|
||||||
|
form.$('.o_field_x2many_list_row_add a').click();
|
||||||
|
|
||||||
|
assert.verifySteps([
|
||||||
|
'read partner',
|
||||||
|
'read turtle',
|
||||||
|
'default_get turtle',
|
||||||
|
'onchange partner',
|
||||||
|
'default_get turtle',
|
||||||
|
'onchange partner',
|
||||||
|
]);
|
||||||
|
form.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('one2many with CREATE onchanges correctly refreshed', function (assert) {
|
QUnit.test('one2many with CREATE onchanges correctly refreshed', function (assert) {
|
||||||
assert.expect(5);
|
assert.expect(5);
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ var History = function History($editable) {
|
|||||||
$editable.removeAttr('contentEditable').removeProp('contentEditable');
|
$editable.removeAttr('contentEditable').removeProp('contentEditable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$editable.trigger('content_will_be_destroyed');
|
||||||
$editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
|
$editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
|
||||||
|
$editable.trigger('content_was_recreated');
|
||||||
|
|
||||||
$('.oe_overlay').remove();
|
$('.oe_overlay').remove();
|
||||||
$('.note-control-selection').hide();
|
$('.note-control-selection').hide();
|
||||||
|
|
||||||
@@ -290,6 +293,20 @@ var RTEWidget = Widget.extend({
|
|||||||
|
|
||||||
var $editable = this.editable();
|
var $editable = this.editable();
|
||||||
|
|
||||||
|
// When a undo/redo is performed, the whole DOM is changed so we have
|
||||||
|
// to prepare for it (website will restart animations for example)
|
||||||
|
// TODO should be better handled
|
||||||
|
$editable.on('content_will_be_destroyed', function (ev) {
|
||||||
|
self.trigger_up('content_will_be_destroyed', {
|
||||||
|
$target: $(ev.currentTarget),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$editable.on('content_was_recreated', function (ev) {
|
||||||
|
self.trigger_up('content_was_recreated', {
|
||||||
|
$target: $(ev.currentTarget),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$editable.addClass('o_editable')
|
$editable.addClass('o_editable')
|
||||||
.data('rte', this)
|
.data('rte', this)
|
||||||
.each(function () {
|
.each(function () {
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ var SnippetEditor = Widget.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.trigger_up('snippet_cloned', {$target: $clone});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Called when the overlay dimensions/positions should be recomputed.
|
* Called when the overlay dimensions/positions should be recomputed.
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
|
|||||||
edit: '_startEditMode',
|
edit: '_startEditMode',
|
||||||
}),
|
}),
|
||||||
custom_events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.custom_events || {}, {
|
custom_events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.custom_events || {}, {
|
||||||
|
content_will_be_destroyed: '_onContentWillBeDestroyed',
|
||||||
|
content_was_recreated: '_onContentWasRecreated',
|
||||||
|
snippet_cloned: '_onSnippetCloned',
|
||||||
snippet_dropped: '_onSnippetDropped',
|
snippet_dropped: '_onSnippetDropped',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -87,6 +90,44 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
|
|||||||
// Handlers
|
// Handlers
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when content will be destroyed in the page. Notifies the
|
||||||
|
* WebsiteRoot that is should stop the animations.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {OdooEvent} ev
|
||||||
|
*/
|
||||||
|
_onContentWillBeDestroyed: function (ev) {
|
||||||
|
this.trigger_up('animation_stop_demand', {
|
||||||
|
$target: ev.data.$target,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Called when content will be recreated in the page. Notifies the
|
||||||
|
* WebsiteRoot that is should start the animations.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {OdooEvent} ev
|
||||||
|
*/
|
||||||
|
_onContentWasRecreated: function (ev) {
|
||||||
|
this.trigger_up('animation_start_demand', {
|
||||||
|
editableMode: true,
|
||||||
|
$target: ev.data.$target,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Called when a snippet is cloned in the page. Notifies the WebsiteRoot
|
||||||
|
* that is should start the animations for this snippet.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {OdooEvent} ev
|
||||||
|
*/
|
||||||
|
_onSnippetCloned: function (ev) {
|
||||||
|
this.trigger_up('animation_start_demand', {
|
||||||
|
editableMode: true,
|
||||||
|
$target: ev.data.$target,
|
||||||
|
});
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Called when a snippet is dropped in the page. Notifies the WebsiteRoot
|
* Called when a snippet is dropped in the page. Notifies the WebsiteRoot
|
||||||
* that is should start the animations for this snippet.
|
* that is should start the animations for this snippet.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from odoo.addons import decimal_precision as dp
|
|||||||
|
|
||||||
from odoo.tools import pycompat
|
from odoo.tools import pycompat
|
||||||
from odoo.tools.translate import html_translate
|
from odoo.tools.translate import html_translate
|
||||||
from odoo.tools import float_is_zero
|
from odoo.tools import float_compare
|
||||||
|
|
||||||
|
|
||||||
class ProductStyle(models.Model):
|
class ProductStyle(models.Model):
|
||||||
@@ -199,8 +199,14 @@ class Product(models.Model):
|
|||||||
for p, p2 in pycompat.izip(self, self2):
|
for p, p2 in pycompat.izip(self, self2):
|
||||||
taxes = partner.property_account_position_id.map_tax(p.sudo().taxes_id.filtered(lambda x: x.company_id == company_id), p, partner)
|
taxes = partner.property_account_position_id.map_tax(p.sudo().taxes_id.filtered(lambda x: x.company_id == company_id), p, partner)
|
||||||
p.website_price = taxes.compute_all(p2.price, pricelist.currency_id, quantity=qty, product=p2, partner=partner)[ret]
|
p.website_price = taxes.compute_all(p2.price, pricelist.currency_id, quantity=qty, product=p2, partner=partner)[ret]
|
||||||
price_without_pricelist = taxes.compute_all(p.list_price, pricelist.currency_id)[ret]
|
# We must convert the price_without_pricelist in the same currency than the
|
||||||
p.website_price_difference = False if float_is_zero(price_without_pricelist - p.website_price, precision_rounding=pricelist.currency_id.rounding) else True
|
# website_price, otherwise the comparison doesn't make sense. Moreover, we show a price
|
||||||
|
# difference only if the website price is lower
|
||||||
|
price_without_pricelist = p.list_price
|
||||||
|
if company_id.currency_id != pricelist.currency_id:
|
||||||
|
price_without_pricelist = company_id.currency_id.compute(price_without_pricelist, pricelist.currency_id)
|
||||||
|
price_without_pricelist = taxes.compute_all(price_without_pricelist, pricelist.currency_id)[ret]
|
||||||
|
p.website_price_difference = True if float_compare(price_without_pricelist, p.website_price, precision_rounding=pricelist.currency_id.rounding) > 0 else False
|
||||||
p.website_public_price = taxes.compute_all(p2.lst_price, quantity=qty, product=p2, partner=partner)[ret]
|
p.website_public_price = taxes.compute_all(p2.lst_price, quantity=qty, product=p2, partner=partner)[ret]
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ odoo.define('website_sale.website_sale', function (require) {
|
|||||||
var product_ids = [];
|
var product_ids = [];
|
||||||
var product_dom = $(event.target).closest(".js_product").find(".js_add_cart_variants");
|
var product_dom = $(event.target).closest(".js_product").find(".js_add_cart_variants");
|
||||||
var qty = $(event.target).closest('form').find('input[name="add_qty"]').val();
|
var qty = $(event.target).closest('form').find('input[name="add_qty"]').val();
|
||||||
|
if ($('body').hasClass('editor_enable')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var attribute_value_ids = product_dom.data("attribute_value_ids");
|
var attribute_value_ids = product_dom.data("attribute_value_ids");
|
||||||
_.each(attribute_value_ids, function(entry) {
|
_.each(attribute_value_ids, function(entry) {
|
||||||
product_ids.push(entry[0]);
|
product_ids.push(entry[0]);
|
||||||
@@ -123,7 +126,7 @@ odoo.define('website_sale.website_sale', function (require) {
|
|||||||
|
|
||||||
$(oe_website_sale).on("change", ".oe_cart input.js_quantity[data-product-id]", function () {
|
$(oe_website_sale).on("change", ".oe_cart input.js_quantity[data-product-id]", function () {
|
||||||
var $input = $(this);
|
var $input = $(this);
|
||||||
if ($input.data('update_change')) {
|
if ($input.data('update_change') || $('body').hasClass('editor_enable')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var value = parseInt($input.val() || 0, 10);
|
var value = parseInt($input.val() || 0, 10);
|
||||||
@@ -192,6 +195,9 @@ odoo.define('website_sale.website_sale', function (require) {
|
|||||||
|
|
||||||
// hack to add and remove from cart with json
|
// hack to add and remove from cart with json
|
||||||
$(oe_website_sale).on('click', 'a.js_add_cart_json', function (ev) {
|
$(oe_website_sale).on('click', 'a.js_add_cart_json', function (ev) {
|
||||||
|
if ($('body').hasClass('editor_enable')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
var $link = $(ev.currentTarget);
|
var $link = $(ev.currentTarget);
|
||||||
var $input = $link.parent().find("input");
|
var $input = $link.parent().find("input");
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ class Property(models.Model):
|
|||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _update_values(self, values):
|
def _update_values(self, values):
|
||||||
value = values.pop('value', None)
|
if 'value' not in values:
|
||||||
if not value:
|
|
||||||
return values
|
return values
|
||||||
|
value = values.pop('value')
|
||||||
|
|
||||||
prop = None
|
prop = None
|
||||||
type_ = values.get('type')
|
type_ = values.get('type')
|
||||||
@@ -81,7 +81,9 @@ class Property(models.Model):
|
|||||||
raise UserError(_('Invalid type'))
|
raise UserError(_('Invalid type'))
|
||||||
|
|
||||||
if field == 'value_reference':
|
if field == 'value_reference':
|
||||||
if isinstance(value, models.BaseModel):
|
if not value:
|
||||||
|
value = False
|
||||||
|
elif isinstance(value, models.BaseModel):
|
||||||
value = '%s,%d' % (value._name, value.id)
|
value = '%s,%d' % (value._name, value.id)
|
||||||
elif isinstance(value, pycompat.integer_types):
|
elif isinstance(value, pycompat.integer_types):
|
||||||
field_id = values.get('fields_id')
|
field_id = values.get('fields_id')
|
||||||
|
|||||||
@@ -716,6 +716,12 @@ class TestFields(common.TransactionCase):
|
|||||||
self.assertEqual(record.sudo(user1).tag_id, tag0.browse())
|
self.assertEqual(record.sudo(user1).tag_id, tag0.browse())
|
||||||
self.assertEqual(record.sudo(user2).tag_id, tag0)
|
self.assertEqual(record.sudo(user2).tag_id, tag0)
|
||||||
|
|
||||||
|
record.sudo(user1).foo = False
|
||||||
|
record.invalidate_cache()
|
||||||
|
self.assertEqual(record.sudo(user0).foo, 'main')
|
||||||
|
self.assertEqual(record.sudo(user1).foo, False)
|
||||||
|
self.assertEqual(record.sudo(user2).foo, 'default')
|
||||||
|
|
||||||
# create company record and attribute
|
# create company record and attribute
|
||||||
company_record = self.env['test_new_api.company'].create({'foo': 'ABC'})
|
company_record = self.env['test_new_api.company'].create({'foo': 'ABC'})
|
||||||
attribute_record = self.env['test_new_api.company.attr'].create({
|
attribute_record = self.env['test_new_api.company.attr'].create({
|
||||||
|
|||||||
@@ -108,12 +108,12 @@ class TestOnChange(common.TransactionCase):
|
|||||||
|
|
||||||
def test_onchange_one2many(self):
|
def test_onchange_one2many(self):
|
||||||
""" test the effect of onchange() on one2many fields """
|
""" test the effect of onchange() on one2many fields """
|
||||||
BODY = "What a beautiful day!"
|
|
||||||
USER = self.env.user
|
USER = self.env.user
|
||||||
|
|
||||||
# create an independent message
|
# create an independent message
|
||||||
message = self.Message.create({'body': BODY})
|
message1 = self.Message.create({'body': "ABC"})
|
||||||
self.assertEqual(message.name, "[%s] %s" % ('', USER.name))
|
message2 = self.Message.create({'body': "ABC"})
|
||||||
|
self.assertEqual(message1.name, "[%s] %s" % ('', USER.name))
|
||||||
|
|
||||||
field_onchange = self.Discussion._onchange_spec()
|
field_onchange = self.Discussion._onchange_spec()
|
||||||
self.assertEqual(field_onchange.get('name'), '1')
|
self.assertEqual(field_onchange.get('name'), '1')
|
||||||
@@ -130,12 +130,14 @@ class TestOnChange(common.TransactionCase):
|
|||||||
'moderator': False,
|
'moderator': False,
|
||||||
'participants': [],
|
'participants': [],
|
||||||
'messages': [
|
'messages': [
|
||||||
(4, message.id),
|
(4, message1.id),
|
||||||
|
(4, message2.id),
|
||||||
|
(1, message2.id, {'body': "XYZ"}),
|
||||||
(0, 0, {
|
(0, 0, {
|
||||||
'name': "[%s] %s" % ('', USER.name),
|
'name': "[%s] %s" % ('', USER.name),
|
||||||
'body': BODY,
|
'body': "ABC",
|
||||||
'author': USER.id,
|
'author': USER.id,
|
||||||
'size': len(BODY),
|
'size': 3,
|
||||||
'important': False,
|
'important': False,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -143,20 +145,27 @@ class TestOnChange(common.TransactionCase):
|
|||||||
self.env.cache.invalidate()
|
self.env.cache.invalidate()
|
||||||
result = self.Discussion.onchange(values, 'name', field_onchange)
|
result = self.Discussion.onchange(values, 'name', field_onchange)
|
||||||
self.assertIn('messages', result['value'])
|
self.assertIn('messages', result['value'])
|
||||||
self.assertItemsEqual(result['value']['messages'], [
|
self.assertEqual(result['value']['messages'], [
|
||||||
(5,),
|
(5,),
|
||||||
(1, message.id, {
|
(1, message1.id, {
|
||||||
'name': "[%s] %s" % ("Foo", USER.name),
|
'name': "[%s] %s" % ("Foo", USER.name),
|
||||||
'body': message.body,
|
'body': "ABC",
|
||||||
'author': message.author.name_get()[0],
|
'author': USER.name_get()[0],
|
||||||
'size': message.size,
|
'size': 3,
|
||||||
'important': message.important,
|
'important': False,
|
||||||
|
}),
|
||||||
|
(1, message2.id, {
|
||||||
|
'name': "[%s] %s" % ("Foo", USER.name),
|
||||||
|
'body': "XYZ", # this must be sent back
|
||||||
|
'author': USER.name_get()[0],
|
||||||
|
'size': 3,
|
||||||
|
'important': False,
|
||||||
}),
|
}),
|
||||||
(0, 0, {
|
(0, 0, {
|
||||||
'name': "[%s] %s" % ("Foo", USER.name),
|
'name': "[%s] %s" % ("Foo", USER.name),
|
||||||
'body': BODY,
|
'body': "ABC",
|
||||||
'author': USER.name_get()[0],
|
'author': USER.name_get()[0],
|
||||||
'size': len(BODY),
|
'size': 3,
|
||||||
'important': False,
|
'important': False,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -167,7 +176,8 @@ class TestOnChange(common.TransactionCase):
|
|||||||
result = self.Discussion.with_context(generate_dummy_message=True).onchange(values, 'name', one_level_fields)
|
result = self.Discussion.with_context(generate_dummy_message=True).onchange(values, 'name', one_level_fields)
|
||||||
self.assertEqual(result['value']['messages'], [
|
self.assertEqual(result['value']['messages'], [
|
||||||
(5,),
|
(5,),
|
||||||
(4, message.id),
|
(4, message1.id),
|
||||||
|
(4, message2.id),
|
||||||
(0, 0, {}),
|
(0, 0, {}),
|
||||||
(0, 0, {}),
|
(0, 0, {}),
|
||||||
])
|
])
|
||||||
@@ -259,12 +269,16 @@ class TestOnChange(common.TransactionCase):
|
|||||||
'name': partner2.name,
|
'name': partner2.name,
|
||||||
'lines': [
|
'lines': [
|
||||||
(5,),
|
(5,),
|
||||||
(1, line1.id, {'name': partner2.name,
|
(1, line1.id, {
|
||||||
'partner': (partner2.id, partner2.name),
|
'name': partner2.name,
|
||||||
'tags': [(5,)]}),
|
'partner': (partner2.id, partner2.name),
|
||||||
(0, 0, {'name': partner2.name,
|
'tags': [(5,)],
|
||||||
'partner': (partner2.id, partner2.name),
|
}),
|
||||||
'tags': [(5,)]}),
|
(0, 0, {
|
||||||
|
'name': partner2.name,
|
||||||
|
'partner': (partner2.id, partner2.name),
|
||||||
|
'tags': [(5,)],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -280,18 +294,35 @@ class TestOnChange(common.TransactionCase):
|
|||||||
self.env.cache.invalidate()
|
self.env.cache.invalidate()
|
||||||
|
|
||||||
result = multi.onchange(values, 'partner', field_onchange)
|
result = multi.onchange(values, 'partner', field_onchange)
|
||||||
self.assertEqual(result['value'], {
|
expected_value = {
|
||||||
'name': partner2.name,
|
'name': partner2.name,
|
||||||
'lines': [
|
'lines': [
|
||||||
(5,),
|
(5,),
|
||||||
(1, line1.id, {'name': partner2.name,
|
(1, line1.id, {
|
||||||
'partner': (partner2.id, partner2.name),
|
'name': partner2.name,
|
||||||
'tags': [(5,)]}),
|
'partner': (partner2.id, partner2.name),
|
||||||
(0, 0, {'name': partner2.name,
|
'tags': [(5,)],
|
||||||
'partner': (partner2.id, partner2.name),
|
}),
|
||||||
'tags': [(5,), (0, 0, {'name': 'Tag'})]}),
|
(0, 0, {
|
||||||
|
'name': partner2.name,
|
||||||
|
'partner': (partner2.id, partner2.name),
|
||||||
|
'tags': [(5,), (0, 0, {'name': 'Tag'})],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
}
|
||||||
|
self.assertEqual(result['value'], expected_value)
|
||||||
|
|
||||||
|
# ensure ID is not returned when asked and a many2many record is set to be created
|
||||||
|
self.env.cache.invalidate()
|
||||||
|
|
||||||
|
result = multi.onchange(values, 'partner', dict(field_onchange, **{'lines.tags.id': None}))
|
||||||
|
self.assertEqual(result['value'], expected_value)
|
||||||
|
|
||||||
|
# ensure inverse of one2many field is not returned
|
||||||
|
self.env.cache.invalidate()
|
||||||
|
|
||||||
|
result = multi.onchange(values, 'partner', dict(field_onchange, **{'lines.multi': None}))
|
||||||
|
self.assertEqual(result['value'], expected_value)
|
||||||
|
|
||||||
def test_onchange_specific(self):
|
def test_onchange_specific(self):
|
||||||
""" test the effect of field-specific onchange method """
|
""" test the effect of field-specific onchange method """
|
||||||
@@ -323,8 +354,7 @@ class TestOnChange(common.TransactionCase):
|
|||||||
self.assertIn('participants', result['value'])
|
self.assertIn('participants', result['value'])
|
||||||
self.assertItemsEqual(
|
self.assertItemsEqual(
|
||||||
result['value']['participants'],
|
result['value']['participants'],
|
||||||
[(5,)] + [(1, user.id, {'display_name': user.display_name})
|
[(5,)] + [(4, user.id) for user in discussion.participants + demo],
|
||||||
for user in discussion.participants + demo],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_onchange_default(self):
|
def test_onchange_default(self):
|
||||||
@@ -360,6 +390,8 @@ class TestOnChange(common.TransactionCase):
|
|||||||
self.assertEqual(len(discussion.messages), 3)
|
self.assertEqual(len(discussion.messages), 3)
|
||||||
messages = [(4, msg.id) for msg in discussion.messages]
|
messages = [(4, msg.id) for msg in discussion.messages]
|
||||||
messages[0] = (1, messages[0][1], {'body': 'test onchange'})
|
messages[0] = (1, messages[0][1], {'body': 'test onchange'})
|
||||||
|
lines = ["%s:%s" % (m.name, m.body) for m in discussion.messages]
|
||||||
|
lines[0] = "%s:%s" % (discussion.messages[0].name, 'test onchange')
|
||||||
values = {
|
values = {
|
||||||
'name': discussion.name,
|
'name': discussion.name,
|
||||||
'moderator': demo.id,
|
'moderator': demo.id,
|
||||||
@@ -370,8 +402,7 @@ class TestOnChange(common.TransactionCase):
|
|||||||
}
|
}
|
||||||
result = discussion.onchange(values, 'messages', field_onchange)
|
result = discussion.onchange(values, 'messages', field_onchange)
|
||||||
self.assertIn('message_concat', result['value'])
|
self.assertIn('message_concat', result['value'])
|
||||||
self.assertEqual(result['value']['message_concat'],
|
self.assertEqual(result['value']['message_concat'], "\n".join(lines))
|
||||||
"\n".join(["%s:%s" % (m.name, m.body) for m in discussion.messages]))
|
|
||||||
|
|
||||||
def test_onchange_one2many_with_domain_on_related_field(self):
|
def test_onchange_one2many_with_domain_on_related_field(self):
|
||||||
""" test the value of the one2many field when defined with a domain on a related field"""
|
""" test the value of the one2many field when defined with a domain on a related field"""
|
||||||
@@ -417,28 +448,22 @@ class TestOnChange(common.TransactionCase):
|
|||||||
'categories': [(4, cat.id) for cat in discussion.categories],
|
'categories': [(4, cat.id) for cat in discussion.categories],
|
||||||
'messages': [(4, msg.id) for msg in discussion.messages],
|
'messages': [(4, msg.id) for msg in discussion.messages],
|
||||||
'participants': [(4, usr.id) for usr in discussion.participants],
|
'participants': [(4, usr.id) for usr in discussion.participants],
|
||||||
'message_changes': 0,
|
|
||||||
'important_messages': [(4, msg.id) for msg in discussion.important_messages],
|
'important_messages': [(4, msg.id) for msg in discussion.important_messages],
|
||||||
'important_emails': [(4, eml.id) for eml in discussion.important_emails],
|
'important_emails': [(4, eml.id) for eml in discussion.important_emails],
|
||||||
}
|
}
|
||||||
|
self.env.cache.invalidate()
|
||||||
result = discussion.onchange(values, 'name', field_onchange)
|
result = discussion.onchange(values, 'name', field_onchange)
|
||||||
|
|
||||||
# When one2many domain contains non-computed field, things are ok
|
|
||||||
self.assertEqual(result['value']['important_messages'],
|
|
||||||
[(5,)] + [(4, msg.id) for msg in discussion.important_messages])
|
|
||||||
|
|
||||||
# But here with commit 5676d81, we get value of: [(2, email.id)]
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result['value']['important_emails'],
|
result['value']['important_emails'],
|
||||||
[(5,),
|
[(5,), (1, email.id, {
|
||||||
(1, email.id, {
|
'name': u'[Foo Bar] %s' % USER.name,
|
||||||
'name': u'[Foo Bar] %s' % USER.name,
|
'body': BODY,
|
||||||
'body': email.body,
|
'author': USER.name_get()[0],
|
||||||
'author': USER.name_get()[0],
|
'size': len(BODY),
|
||||||
'important': True,
|
'important': True,
|
||||||
'email_to': demo.email,
|
'email_to': demo.email,
|
||||||
'size': email.size,
|
})],
|
||||||
})]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_onchange_related(self):
|
def test_onchange_related(self):
|
||||||
|
|||||||
+93
-40
@@ -852,6 +852,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
# avoid broken transaction) and keep going
|
# avoid broken transaction) and keep going
|
||||||
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
|
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
_logger.exception("Error while loading record")
|
||||||
message = (_(u'Unknown error during import:') + u' %s: %s' % (type(e), e))
|
message = (_(u'Unknown error during import:') + u' %s: %s' % (type(e), e))
|
||||||
moreinfo = _('Resolve other errors first')
|
moreinfo = _('Resolve other errors first')
|
||||||
messages.append(dict(info, type='error', message=message, moreinfo=moreinfo))
|
messages.append(dict(info, type='error', message=message, moreinfo=moreinfo))
|
||||||
@@ -4981,31 +4982,101 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
if not all(name in self._fields for name in names):
|
if not all(name in self._fields for name in names):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# filter out keys in field_onchange that do not refer to actual fields
|
def PrefixTree(model, dotnames):
|
||||||
dotnames = []
|
""" Return a prefix tree for sequences of field names. """
|
||||||
for dotname in field_onchange:
|
if not dotnames:
|
||||||
try:
|
return {}
|
||||||
model = self.browse()
|
# group dotnames by prefix
|
||||||
for name in dotname.split('.'):
|
suffixes = defaultdict(list)
|
||||||
model = model[name]
|
for dotname in dotnames:
|
||||||
dotnames.append(dotname)
|
# name, *names = dotname.split('.', 1)
|
||||||
except Exception:
|
names = dotname.split('.', 1)
|
||||||
pass
|
name = names.pop(0)
|
||||||
|
suffixes[name].extend(names)
|
||||||
|
# fill in prefix tree in fields order
|
||||||
|
tree = OrderedDict()
|
||||||
|
for name, field in model._fields.items():
|
||||||
|
if name in suffixes:
|
||||||
|
tree[name] = subtree = PrefixTree(model[name], suffixes[name])
|
||||||
|
if subtree and field.type == 'one2many':
|
||||||
|
subtree.pop(field.inverse_name, None)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
class Snapshot(dict):
|
||||||
|
""" A dict with the values of a record, following a prefix tree. """
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __init__(self, record, tree):
|
||||||
|
# put record in dict to include it when comparing snapshots
|
||||||
|
super(Snapshot, self).__init__({'<record>': record, '<tree>': tree})
|
||||||
|
for name, subnames in tree.items():
|
||||||
|
# x2many fields are serialized as a list of line snapshots
|
||||||
|
self[name] = (
|
||||||
|
[Snapshot(line, subnames) for line in record[name]]
|
||||||
|
if subnames else record[name]
|
||||||
|
)
|
||||||
|
|
||||||
|
def diff(self, other):
|
||||||
|
""" Return the values in ``self`` that differ from ``other``.
|
||||||
|
Requires record cache invalidation for correct output!
|
||||||
|
"""
|
||||||
|
record = self['<record>']
|
||||||
|
result = {}
|
||||||
|
for name, subnames in self['<tree>'].items():
|
||||||
|
if (name == 'id') or (other.get(name) == self[name]):
|
||||||
|
continue
|
||||||
|
if not subnames:
|
||||||
|
field = record._fields[name]
|
||||||
|
result[name] = field.convert_to_onchange(self[name], record, {})
|
||||||
|
else:
|
||||||
|
# x2many fields: serialize value as commands
|
||||||
|
result[name] = commands = [(5,)]
|
||||||
|
for line_snapshot in self[name]:
|
||||||
|
line = line_snapshot['<record>']
|
||||||
|
if not line.id:
|
||||||
|
# new line: send diff from scratch
|
||||||
|
line_diff = line_snapshot.diff({})
|
||||||
|
commands.append((0, line.id.ref or 0, line_diff))
|
||||||
|
else:
|
||||||
|
# existing line: check diff from database
|
||||||
|
# (requires a clean record cache!)
|
||||||
|
line_diff = line_snapshot.diff(Snapshot(line, subnames))
|
||||||
|
if line_diff:
|
||||||
|
# send all fields because the web client
|
||||||
|
# might need them to evaluate modifiers
|
||||||
|
line_diff = line_snapshot.diff({})
|
||||||
|
commands.append((1, line.id, line_diff))
|
||||||
|
else:
|
||||||
|
commands.append((4, line.id))
|
||||||
|
return result
|
||||||
|
|
||||||
|
nametree = PrefixTree(self.browse(), field_onchange)
|
||||||
|
|
||||||
|
# prefetch x2many lines without data (for the initial snapshot)
|
||||||
|
for name, subnames in nametree.items():
|
||||||
|
if subnames and values.get(name):
|
||||||
|
# retrieve all ids in commands, and read the expected fields
|
||||||
|
line_ids = []
|
||||||
|
for cmd in values[name]:
|
||||||
|
if cmd[0] in (1, 4):
|
||||||
|
line_ids.append(cmd[1])
|
||||||
|
elif cmd[0] == 6:
|
||||||
|
line_ids.extend(cmd[2])
|
||||||
|
lines = self.browse()[name].browse(line_ids)
|
||||||
|
lines.read(list(subnames), load='_classic_write')
|
||||||
|
|
||||||
# create a new record with values, and attach ``self`` to it
|
# create a new record with values, and attach ``self`` to it
|
||||||
with env.do_in_onchange():
|
with env.do_in_onchange():
|
||||||
record = self.new(values)
|
record = self.new(values)
|
||||||
values = {name: record[name] for name in record._cache}
|
|
||||||
# attach ``self`` with a different context (for cache consistency)
|
# attach ``self`` with a different context (for cache consistency)
|
||||||
record._origin = self.with_context(__onchange=True)
|
record._origin = self.with_context(__onchange=True)
|
||||||
|
|
||||||
# load fields on secondary records, to avoid false changes
|
# make a snapshot based on the initial values of record
|
||||||
with env.do_in_onchange():
|
with env.do_in_onchange():
|
||||||
for dotname in dotnames:
|
snapshot0 = snapshot1 = Snapshot(record, nametree)
|
||||||
record.mapped(dotname)
|
|
||||||
|
|
||||||
# determine which field(s) should be triggered an onchange
|
# determine which field(s) should be triggered an onchange
|
||||||
todo = list(names) or list(values)
|
todo = list(names or nametree)
|
||||||
done = set()
|
done = set()
|
||||||
|
|
||||||
# dummy assignment: trigger invalidations on the record
|
# dummy assignment: trigger invalidations on the record
|
||||||
@@ -5021,7 +5092,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
record[name] = value
|
record[name] = value
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
dirty = set()
|
|
||||||
|
|
||||||
# process names in order (or the keys of values if no name given)
|
# process names in order (or the keys of values if no name given)
|
||||||
while todo:
|
while todo:
|
||||||
@@ -5035,34 +5105,17 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
if field_onchange.get(name):
|
if field_onchange.get(name):
|
||||||
record._onchange_eval(name, field_onchange[name], result)
|
record._onchange_eval(name, field_onchange[name], result)
|
||||||
|
|
||||||
# force re-evaluation of function fields on secondary records
|
# make a snapshot (this forces evaluation of computed fields)
|
||||||
for dotname in dotnames:
|
snapshot1 = Snapshot(record, nametree)
|
||||||
record.mapped(dotname)
|
|
||||||
|
|
||||||
# determine which fields have been modified
|
# determine which fields have been modified
|
||||||
for name, oldval in values.items():
|
for name in nametree:
|
||||||
field = self._fields[name]
|
if snapshot1[name] != snapshot0[name]:
|
||||||
newval = record[name]
|
|
||||||
if newval != oldval or (
|
|
||||||
field.type in ('one2many', 'many2many') and newval._is_dirty()
|
|
||||||
):
|
|
||||||
todo.append(name)
|
todo.append(name)
|
||||||
dirty.add(name)
|
|
||||||
|
|
||||||
# determine subfields for field.convert_to_onchange() below
|
# determine values that have changed by comparing snapshots
|
||||||
Tree = lambda: defaultdict(Tree)
|
self.invalidate_cache()
|
||||||
subnames = Tree()
|
result['value'] = snapshot1.diff(snapshot0)
|
||||||
for dotname in dotnames:
|
|
||||||
subtree = subnames
|
|
||||||
for name in dotname.split('.'):
|
|
||||||
subtree = subtree[name]
|
|
||||||
|
|
||||||
# collect values from dirty fields
|
|
||||||
with env.do_in_onchange():
|
|
||||||
result['value'] = {
|
|
||||||
name: self._fields[name].convert_to_onchange(record[name], record, subnames[name])
|
|
||||||
for name in dirty
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user