diff --git a/addons/account/data/mail_template_data.xml b/addons/account/data/mail_template_data.xml
index 6507259b89c..d0365929333 100644
--- a/addons/account/data/mail_template_data.xml
+++ b/addons/account/data/mail_template_data.xml
@@ -72,12 +72,12 @@ from ${object.company_id.name}.
% set record = ctx.get('record')
% set company = record and record.company_id or user.company_id
-
+
-
+
@@ -94,7 +94,7 @@ from ${object.company_id.name}.
-
+
${object.body | safe}
@@ -106,7 +106,7 @@ from ${object.company_id.name}.
-
+
${company.name}
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index e1b20487b71..7d1344f7555 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -848,6 +848,7 @@ class AccountTax(models.Model):
price_unit * quantity eventually affected by previous taxes (if tax is include_base_amount XOR price_include)
"""
self.ensure_one()
+ price_include = self._context.get('force_price_include', self.price_include)
if self.amount_type == 'fixed':
# Use copysign to take into account the sign of the base amount which includes the sign
# of the quantity and the sign of the price_unit
@@ -861,11 +862,11 @@ class AccountTax(models.Model):
return math.copysign(quantity, base_amount) * self.amount
else:
return quantity * self.amount
- if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include):
+ if (self.amount_type == 'percent' and not price_include) or (self.amount_type == 'division' and self.price_include):
return base_amount * self.amount / 100
- if self.amount_type == 'percent' and self.price_include:
+ if self.amount_type == 'percent' and price_include:
return base_amount - (base_amount / (1 + self.amount / 100))
- if self.amount_type == 'division' and not self.price_include:
+ if self.amount_type == 'division' and not price_include:
return base_amount / (1 - self.amount / 100) - base_amount
@api.multi
@@ -899,13 +900,58 @@ class AccountTax(models.Model):
'analytic': boolean,
}]
} """
+
+ # 1) Flatten the taxes.
+
+ def collect_taxes(self, all_taxes=None):
+ # Collect all the taxes recursively ordered by the sequence.
+ # Example:
+ # group | seq | sub-group |
+ # ------------|-----------|
+ # | 1 | |
+ # ------------|-----------|
+ # t | 2 | | seq | |
+ # | | | 4 | |
+ # | | | 5 | |
+ # | | | 6 | |
+ # | | |
+ # ------------|-----------|
+ # | 3 | |
+ # ------------|-----------|
+ # Result: 1-4-5-6-3
+ if not all_taxes:
+ all_taxes = self.env['account.tax']
+ for tax in self.sorted(key=lambda r: r.sequence):
+ if tax.amount_type == 'group':
+ all_taxes = collect_taxes(tax.children_tax_ids, all_taxes)
+ else:
+ all_taxes += tax
+ return all_taxes
+
+ taxes = collect_taxes(self)
+
+ # 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True
+ # with price_include=True
+
+ base_excluded_flag = False # price_include=False && include_base_amount=True
+ included_flag = False # price_include=True
+ for tax in taxes:
+ if tax.price_include:
+ included_flag = True
+ elif tax.include_base_amount:
+ base_excluded_flag = True
+ if base_excluded_flag and included_flag:
+ raise UserError(_('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.'))
+
+ # 3) Deal with the rounding methods
+
if len(self) == 0:
company_id = self.env.user.company_id
else:
company_id = self[0].company_id
if not currency:
currency = company_id.currency_id
- taxes = []
+
# By default, for each tax, tax amount will first be computed
# and rounded at the 'Account' decimal precision for each
# PO/SO/invoice line and then these rounded amounts will be
@@ -930,50 +976,93 @@ class AccountTax(models.Model):
if not round_tax:
prec += 5
- base_values = self.env.context.get('base_values')
- if not base_values:
- total_excluded = total_included = base = round(price_unit * quantity, prec)
+ # 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
+ # tax | base | amount |
+ # /\ ----------------------------
+ # || tax_1 | XXXX | | <- we are looking for that, it's the total_excluded
+ # || tax_2 | | |
+ # || tax_3 | | |
+ # || ... | .. | .. |
+ # ----------------------------
+
+ def recompute_base(base_amount, fixed_amount, percent_amount):
+ # Recompute the new base amount based on included fixed/percent amount and the current base amount.
+ # Example:
+ # tax | amount |
+ # ------------------
+ # tax_1 | 10% |
+ # tax_2 | 15 |
+ # tax_3 | 20% |
+ # ------------------
+ # if base_amount = 145, the new base is computed as:
+ # (145 - 15) / (1.0 + ((10 + 20) / 100.0)) = 130 / 1.3 = 100
+ if fixed_amount == 0.0 and percent_amount == 0.0:
+ return base_amount
+ return (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0)
+
+ base = round(price_unit * quantity, prec)
+
+ # For the computation of move lines, we could have a negative base value.
+ # In this case, compute all with positive values and negative them at the end.
+ if base < 0:
+ base = -base
+ sign = -1
else:
- total_excluded, total_included, base = base_values
+ sign = 1
- # Sorting key is mandatory in this case. When no key is provided, sorted() will perform a
- # search. However, the search method is overridden in account.tax in order to add a domain
- # depending on the context. This domain might filter out some taxes from self, e.g. in the
- # case of group taxes.
- for tax in self.sorted(key=lambda r: r.sequence):
- if tax.amount_type == 'group':
- children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, base))
- ret = children.compute_all(price_unit, currency, quantity, product, partner)
- total_excluded = ret['total_excluded']
- base = ret['base'] if tax.include_base_amount else base
- total_included = ret['total_included']
- tax_amount = total_included - total_excluded
- taxes += ret['taxes']
- continue
+ # Keep track of the accumulated included fixed/percent amount.
+ incl_fixed_amount = incl_percent_amount = 0
+ for tax in reversed(taxes):
+ if tax.include_base_amount:
+ base = recompute_base(base, incl_fixed_amount, incl_percent_amount)
+ incl_fixed_amount = incl_percent_amount = 0
+ if tax.price_include:
+ if tax.amount_type == 'fixed':
+ incl_fixed_amount += tax.amount
+ elif tax.amount_type == 'percent':
+ incl_percent_amount += tax.amount
+ # Start the computation of accumulated amounts at the total_excluded value.
+ total_excluded = total_included = base = recompute_base(base, incl_fixed_amount, incl_percent_amount)
- tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
+ # 5) Iterate the taxes in the sequence order to fill missing base/amount values.
+ # tax | base | amount |
+ # || ----------------------------
+ # || tax_1 | OK | XXXX |
+ # || tax_2 | XXXX | XXXX |
+ # || tax_3 | XXXX | XXXX |
+ # \/ ... | .. | .. |
+ # ----------------------------
+ taxes_vals = []
+ for tax in taxes:
+ # Compute the amount of the tax but don't deal with the price_include because it's already
+ # took into account on the base amount except for 'division' tax:
+ # (tax.amount_type == 'percent' && not tax.price_include)
+ # == (tax.amount_type == 'division' && tax.price_include)
+ tax_amount = tax.with_context(force_price_include=False)._compute_amount(base, price_unit, quantity, product, partner)
if not round_tax:
tax_amount = round(tax_amount, prec)
else:
tax_amount = currency.round(tax_amount)
- if tax.price_include:
- total_excluded -= tax_amount
- base -= tax_amount
- else:
- total_included += tax_amount
-
- # Keep base amount used for the current tax
+ # Suppose:
+ # seq | amount | incl | incl_base | base | amount
+ # -----------------------------------------------
+ # 1 | 10 % | t | t | 100.0 | 10.0
+ # -----------------------------------------------
+ # ... the next computation must be done using 100.0 + 10.0 = 110.0 as base but
+ # the tax base of this tax will be 100.0.
tax_base = base
-
if tax.include_base_amount:
base += tax_amount
- taxes.append({
+ # The total_included amount is computed as the sum of total_excluded with all tax_amount
+ total_included += tax_amount
+
+ taxes_vals.append({
'id': tax.id,
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
- 'amount': tax_amount,
- 'base': tax_base,
+ 'amount': sign * tax_amount,
+ 'base': round(sign * tax_base, prec),
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
@@ -981,10 +1070,10 @@ class AccountTax(models.Model):
})
return {
- 'taxes': sorted(taxes, key=lambda k: k['sequence']),
- 'total_excluded': currency.round(total_excluded) if round_total else total_excluded,
- 'total_included': currency.round(total_included) if round_total else total_included,
- 'base': base,
+ 'taxes': taxes_vals,
+ 'total_excluded': sign * (currency.round(total_excluded) if round_total else total_excluded),
+ 'total_included': sign * (currency.round(total_included) if round_total else total_included),
+ 'base': round(sign * base, prec),
}
@api.model
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 715941ce02f..964b000cfc0 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -500,10 +500,10 @@ class AccountBankStatementLine(models.Model):
if self.amount_currency and self.currency_id:
amount = self.amount_currency
amount_currency = self.amount
- amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency
- amount_currency_str = formatLang(self.env, amount_currency_str, currency_obj=statement_currency)
+ amount_currency_str = formatLang(self.env, abs(amount_currency), currency_obj=statement_currency)
else:
amount = self.amount
+ amount_currency = amount
amount_currency_str = ""
amount_str = formatLang(self.env, abs(amount), currency_obj=self.currency_id or statement_currency)
@@ -525,6 +525,7 @@ class AccountBankStatementLine(models.Model):
'partner_name': self.partner_id.name,
'communication_partner_name': self.partner_name,
'amount_currency_str': amount_currency_str, # Amount in the statement currency
+ 'amount_currency': amount_currency, # Amount in the statement currency
'has_no_partner': not self.partner_id.id,
}
if self.partner_id:
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 1fae487036a..ffc572a2cab 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -1588,6 +1588,9 @@ class AccountInvoiceLine(models.Model):
self.price_unit = 0.0
domain['uom_id'] = []
else:
+ # Use the purchase uom by default
+ self.uom_id = self.product_id.uom_po_id
+
if part.lang:
product = self.product_id.with_context(lang=part.lang)
else:
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index 83b8ec1f824..66c8bb9f4a4 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -175,7 +175,7 @@ class account_journal(models.Model):
# optimization to read sum of balance from account_move_line
account_ids = tuple(ac for ac in [self.default_debit_account_id.id, self.default_credit_account_id.id] if ac)
if account_ids:
- amount_field = 'balance' if not self.currency_id else 'amount_currency'
+ amount_field = 'balance' if (not self.currency_id or self.currency_id == self.company_id.currency_id) else 'amount_currency'
query = """SELECT sum(%s) FROM account_move_line WHERE account_id in %%s AND date <= %%s;""" % (amount_field,)
self.env.cr.execute(query, (account_ids, fields.Date.today(),))
query_results = self.env.cr.dictfetchall()
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index ef60c1897a7..6a72ba478ea 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -698,19 +698,36 @@ class AccountMoveLine(models.Model):
'|', ('date_maturity', 'like', str),
'&', ('name', '!=', '/'), ('name', 'ilike', str)
]
- try:
- amount = float(str)
- amount_domain = [
- '|', ('amount_residual', '=', amount),
- '|', ('amount_residual_currency', '=', amount),
- '|', ('amount_residual', '=', -amount),
- '|', ('amount_residual_currency', '=', -amount),
- '&', ('account_id.internal_type', '=', 'liquidity'),
- '|', '|', ('debit', '=', amount), ('credit', '=', amount), ('amount_currency', '=', amount),
- ]
- str_domain = expression.OR([str_domain, amount_domain])
- except:
- pass
+ if str[0] in ['-', '+']:
+ try:
+ amounts_str = str.split('|')
+ for amount_str in amounts_str:
+ amount = amount_str[0] == '-' and float(amount_str) or float(amount_str[1:])
+ amount_domain = [
+ '|', ('amount_residual', '=', amount),
+ '|', ('amount_residual_currency', '=', amount),
+ '|', (amount_str[0] == '-' and 'credit' or 'debit', '=', float(amount_str[1:])),
+ ('amount_currency', '=', amount),
+ ]
+ str_domain = expression.OR([str_domain, amount_domain])
+ except:
+ pass
+ else:
+ try:
+ amount = float(str)
+ amount_domain = [
+ '|', ('amount_residual', '=', amount),
+ '|', ('amount_residual_currency', '=', amount),
+ '|', ('amount_residual', '=', -amount),
+ '|', ('amount_residual_currency', '=', -amount),
+ '&', ('account_id.internal_type', '=', 'liquidity'),
+ '|', '|', '|', ('debit', '=', amount), ('credit', '=', amount),
+ ('amount_currency', '=', amount),
+ ('amount_currency', '=', -amount),
+ ]
+ str_domain = expression.OR([str_domain, amount_domain])
+ except:
+ pass
return str_domain
def _domain_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False):
@@ -753,6 +770,7 @@ class AccountMoveLine(models.Model):
for line in self:
company_currency = line.account_id.company_id.currency_id
+ line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency
ret_line = {
'id': line.id,
'name': line.name and line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name,
@@ -769,7 +787,7 @@ class AccountMoveLine(models.Model):
'journal_id': [line.journal_id.id, line.journal_id.display_name],
'partner_id': line.partner_id.id,
'partner_name': line.partner_id.name,
- 'currency_id': (line.currency_id and line.amount_currency) and line.currency_id.id or False,
+ 'currency_id': line_currency.id,
}
debit = line.debit
@@ -784,7 +802,7 @@ class AccountMoveLine(models.Model):
# Get right debit / credit:
target_currency = target_currency or company_currency
- line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency
+ amount_currency = debit - credit
amount_currency_str = ""
total_amount_currency_str = ""
if line_currency != company_currency and target_currency == line_currency:
@@ -809,9 +827,10 @@ class AccountMoveLine(models.Model):
actual_debit = debit > 0 and amount or 0.0
actual_credit = credit > 0 and -amount or 0.0
currency = company_currency
+
if line_currency != target_currency:
- target_currency.compute(total_amount, line_currency)
- amount_currency_str = formatLang(self.env, target_currency.compute(abs(actual_debit or actual_credit), line_currency), currency_obj=line_currency)
+ amount_currency = target_currency.compute(total_amount, line_currency)
+ amount_currency_str = formatLang(self.env, abs(amount_currency), currency_obj=line_currency)
total_amount_currency_str = formatLang(self.env, target_currency.compute(total_amount, line_currency), currency_obj=line_currency)
if currency != target_currency:
ctx = context.copy()
@@ -827,6 +846,7 @@ class AccountMoveLine(models.Model):
ret_line['amount_str'] = amount_str
ret_line['total_amount_str'] = total_amount_str
ret_line['amount_currency_str'] = amount_currency_str
+ ret_line['amount_currency'] = amount_currency
ret_line['total_amount_currency_str'] = total_amount_currency_str
ret.append(ret_line)
return ret
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js
index fabfee1ad00..bfd2e48972e 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_action.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js
@@ -20,6 +20,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
change_offset: '_onAction',
change_partner: '_onAction',
add_proposition: '_onAction',
+ search_balance_amount: '_onAction',
remove_proposition: '_onAction',
update_proposition: '_onAction',
create_proposition: '_onAction',
@@ -38,7 +39,11 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
// used to instanciate the action interface
ActionRenderer: ReconciliationRenderer.StatementRenderer,
// used to instanciate each widget line
- LineRenderer: ReconciliationRenderer.LineRenderer
+ LineRenderer: ReconciliationRenderer.LineRenderer,
+ // used context params
+ params: ['statement_ids'],
+ // number of moves lines displayed in 'match' mode
+ limitMoveLines: 5,
},
/**
@@ -51,11 +56,25 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
this._super.apply(this, arguments);
this.action_manager = parent;
this.params = params;
- this.model = new this.config.Model(this, {modelName: "account.bank.statement.line"});
+ this.model = new this.config.Model(this, {
+ modelName: "account.bank.statement.line",
+ limitMoveLines: params.params && params.params.limitMoveLines || this.config.limitMoveLines,
+ });
this.widgets = [];
if (!this.action_manager) {
this.set_cp_bus(new Widget());
}
+ // Adding values from the context is necessary to put this information in the url via the action manager so that
+ // you can retrieve it if the person shares his url or presses f5
+ _.each(params.params, function (value, name) {
+ params.context[name] = name.indexOf('_ids') !== -1 ? _.map((value+'').split(), parseFloat) : value;
+ });
+ params.params = {};
+ _.each(this.config.params, function (name) {
+ if (params.context[name]) {
+ params.params[name] = name.indexOf('_ids') !== -1 && _.isArray(params.context[name]) ? params.context[name].join() : params.context[name];
+ }
+ });
},
/**
@@ -305,8 +324,15 @@ var ManualAction = StatementAction.extend({
config: {
Model: ReconciliationModel.ManualModel,
ActionRenderer: ReconciliationRenderer.ManualRenderer,
- LineRenderer: ReconciliationRenderer.ManualLineRenderer
+ LineRenderer: ReconciliationRenderer.ManualLineRenderer,
+ params: ['company_ids', 'mode', 'partner_ids', 'account_ids'],
+ limitMoveLines: 10,
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
/**
* call 'validate' or 'autoReconciliation' model method then destroy the
* reconcilied lines, update the not reconcilied and update the action
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index 880619411fe..41fb2d32003 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -66,6 +66,7 @@ var _t = core._t;
* }
* mv_lines: object - idem than reconciliation_proposition
* offset: integer
+ * limitMoveLines: integer
* filter: string
* [createForm]: {
* account_id: {
@@ -95,8 +96,11 @@ var StatementModel = BasicModel.extend({
/**
* @override
+ *
+ * @param {Widget} parent
+ * @param {object} options
*/
- init: function () {
+ init: function (parent, options) {
this._super.apply(this, arguments);
this.reconcileModels = [];
this.lines = {};
@@ -104,6 +108,7 @@ var StatementModel = BasicModel.extend({
this.valuemax = 0;
this.alreadyDisplayed = [];
this.defaultDisplayQty = 10;
+ this.limitMoveLines = options && options.limitMoveLines || 5;
},
//--------------------------------------------------------------------------
@@ -162,7 +167,9 @@ var StatementModel = BasicModel.extend({
* @returns {Deferred}
*/
changeFilter: function (handle, filter) {
- this.getLine(handle).filter = filter;
+ var line = this.getLine(handle);
+ line.filter = filter;
+ line.offset = 0;
return this._performMoveLine(handle);
},
/**
@@ -219,7 +226,7 @@ var StatementModel = BasicModel.extend({
* @returns {Deferred}
*/
changeOffset: function (handle, offset) {
- this.getLine(handle).offset += offset;
+ this.getLine(handle).offset += (offset > 0 ? 1 : -1) * this.limitMoveLines;
return this._performMoveLine(handle);
},
/**
@@ -360,14 +367,14 @@ var StatementModel = BasicModel.extend({
self.context.journal_id = statement.journal_id;
_.each(statement.st_lines_ids, function (id) {
self.lines[_.uniqueId('rline')] = {
- 'id': id,
- 'reconciled': false,
- 'mode': 'inactive',
- 'mv_lines': [],
- 'offset': 0,
- 'filter': "",
- 'reconciliation_proposition': [],
- 'reconcileModels': []
+ id: id,
+ reconciled: false,
+ mode: 'inactive',
+ mv_lines: [],
+ offset: 0,
+ filter: "",
+ reconciliation_proposition: [],
+ reconcileModels: [],
};
});
});
@@ -489,6 +496,25 @@ var StatementModel = BasicModel.extend({
}
return def;
},
+ searchBalanceAmount: function (handle) {
+ var line = this.getLine(handle);
+ var amount = line.balance.amount;
+ var amount_str = _.str.sprintf('%.2f', Math.abs(amount));
+ amount_str = (amount > '0' ? '-' : '+') + amount_str;
+ if (line.balance.amount_currency) {
+ var amount_currency = line.balance.amount_currency;
+ var amount_currency_str = _.str.sprintf('%.2f', Math.abs(amount_currency));
+ amount_str += '|' + (amount_currency > '0' ? '-' : '+') + amount_currency_str;
+ }
+ if (amount_str === line.filter) {
+ line.filter = '';
+ line.offset = 0;
+ return this.changeMode(handle, 'create');
+ }
+ line.filter = amount_str;
+ line.offset = 0;
+ return this.changeMode(handle, 'match');
+ },
/**
* Force the partial reconciliation to display the reconciliate button.
* This method should only be called when there is onely one proposition.
@@ -728,7 +754,7 @@ var StatementModel = BasicModel.extend({
return !p.is_tax || p.link !== prop.id;
});
- var args = [[prop.tax_id.id], prop.base_amount, line.st_line.currency_id];
+ var args = [[prop.tax_id.id], prop.base_amount, formatOptions.currency_id];
tax_defs.push(self._rpc({
model: 'account.tax',
method: 'json_friendly_compute_all',
@@ -758,11 +784,7 @@ var StatementModel = BasicModel.extend({
});
}));
} else {
- var currencyData = {
- currency_id: line.st_line.currency_id
- };
-
- prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, currencyData);
+ prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
prop.display = self._isDisplayedProposition(prop);
prop.invalid = !self._isValid(prop);
}
@@ -774,19 +796,32 @@ var StatementModel = BasicModel.extend({
});
line.reconciliation_proposition = reconciliation_proposition;
+ var amount_currency = 0;
var total = line.st_line.amount || 0;
- _.each(line.reconciliation_proposition, function (prop) {
+ var isOtherCurrencyId = _.uniq(_.pluck(_.reject(reconciliation_proposition, 'invalid'), 'currency_id'));
+ isOtherCurrencyId = isOtherCurrencyId.length === 1 && !total && isOtherCurrencyId[0] !== formatOptions.currency_id ? isOtherCurrencyId[0] : false;
+
+ _.each(reconciliation_proposition, function (prop) {
if (!prop.invalid) {
total -= prop.amount;
+ if (isOtherCurrencyId) {
+ amount_currency -= prop.amount_currency;
+ }
}
});
total = Math.round(total*1000)/1000 || 0;
line.balance = {
- 'amount': total,
- 'amount_str': field_utils.format.monetary(Math.abs(total), {}, formatOptions),
- 'account_code': self.accounts[line.st_line.open_balance_account_id],
+ amount: total,
+ amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions),
+ currency_id: isOtherCurrencyId,
+ amount_currency: isOtherCurrencyId ? amount_currency : false,
+ amount_currency_str: isOtherCurrencyId ? field_utils.format.monetary(Math.abs(amount_currency), {}, {
+ currency_id: isOtherCurrencyId
+ }) : false,
+ account_code: self.accounts[line.st_line.open_balance_account_id],
};
- line.balance.type = line.balance.amount ? (line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1;
+ line.balance.type = (isOtherCurrencyId && amount_currency || line.balance.amount) ?
+ (line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1;
});
},
/**
@@ -851,6 +886,7 @@ var StatementModel = BasicModel.extend({
return l.id === data.st_line.id;
});
line.visible = true;
+ line.limitMoveLines = self.limitMoveLines;
_.extend(line, data);
self._formatLineProposition(line, line.reconciliation_proposition);
if (!line.reconciliation_proposition.length) {
@@ -989,7 +1025,7 @@ var StatementModel = BasicModel.extend({
})));
var filter = line.filter || "";
var offset = line.offset;
- var limit = 6;
+ var limit = this.limitMoveLines+1;
return this._rpc({
model: 'account.bank.statement.line',
method: 'get_move_lines_for_reconciliation_widget',
@@ -1196,6 +1232,8 @@ var ManualModel = StatementModel.extend({
if (line.reconciled) {
return;
}
+ line.filter = "";
+ line.offset = 0;
defs.push(self._performMoveLine(handle).then(function () {
if(!line.mv_lines.length) {
self.valuenow++;
@@ -1243,7 +1281,7 @@ var ManualModel = StatementModel.extend({
*/
_computeLine: function (line) {
return this._super(line).then(function () {
- var props = line.reconciliation_proposition;
+ var props = _.reject(line.reconciliation_proposition, 'invalid');
line.balance.type = -1;
if (!line.balance.amount && props.length) {
line.balance.type = 1;
@@ -1265,15 +1303,16 @@ var ManualModel = StatementModel.extend({
*/
_formatLine: function (type, data) {
var line = this.lines[_.uniqueId('rline')] = _.extend(data, {
- 'type': type,
- 'reconciled': false,
- 'mode': 'inactive',
- 'offset': 0,
- 'filter': "",
- 'reconcileModels': [],
- 'account_id': this._formatNameGet([data.account_id, data.account_name]),
- 'st_line': data,
- 'visible': true
+ type: type,
+ reconciled: false,
+ mode: 'inactive',
+ offset: 0,
+ limitMoveLines: this.limitMoveLines,
+ filter: "",
+ reconcileModels: [],
+ account_id: this._formatNameGet([data.account_id, data.account_name]),
+ st_line: data,
+ visible: true
});
this._formatLineProposition(line, line.reconciliation_proposition);
if (!line.reconciliation_proposition.length) {
@@ -1337,7 +1376,7 @@ var ManualModel = StatementModel.extend({
})));
var filter = line.filter || "";
var offset = line.offset;
- var limit = 6;
+ var limit = this.limitMoveLines+1;
var args = [line.account_id.id, line.partner_id, excluded_ids, filter, offset, limit];
return this._rpc({
model: 'account.move.line',
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index f71f3d09011..9e72fdeb44f 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -250,7 +250,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
events: {
'click .accounting_view caption .o_buttons button': '_onValidate',
'click .accounting_view thead td': '_onTogglePanel',
- 'click .accounting_view tfoot td': '_onShowPanel',
+ 'click .accounting_view tfoot td:not(.cell_left,.cell_right)': '_onShowPanel',
+ 'click tfoot .cell_left, tfoot .cell_right': '_onSearchBalanceAmount',
'input input.filter': '_onFilterChange',
'click .match_controls .fa-chevron-left:not(.disabled)': '_onPrevious',
'click .match_controls .fa-chevron-right:not(.disabled)': '_onNext',
@@ -386,7 +387,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
// mv_lines
var $mv_lines = this.$('.match table tbody').empty();
- _.each(state.mv_lines.slice(0,5), function (line) {
+ _.each(state.mv_lines.slice(0, state.limitMoveLines), function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
$('')
@@ -395,13 +396,19 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
$mv_lines.append($line);
});
- this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= 5);
+ this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= state.limitMoveLines);
this.$('.match .fa-chevron-left').toggleClass('disabled', !state.offset);
this.$('.match').css('max-height', !state.mv_lines.length && !state.filter.length ? '0px' : '');
// balance
+ this.$('.popover').remove();
this.$('table tfoot').html(qweb.render("reconciliation.line.balance", {'state': state}));
+ // filter
+ if (_.str.strip(this.$('input.filter').val()) !== state.filter) {
+ this.$('input.filter').val(state.filter);
+ }
+
// create form
if (state.createForm) {
if (!this.fields.account_id) {
@@ -590,6 +597,12 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
var mode = this.$el.data('mode') === 'inactive' ? 'match' : 'inactive';
this.trigger_up('change_mode', {'data': mode});
},
+ /**
+ * @private
+ */
+ _onSearchBalanceAmount: function () {
+ this.trigger_up('search_balance_amount');
+ },
/**
* @private
*/
@@ -637,13 +650,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
* @private
*/
_onPrevious: function () {
- this.trigger_up('change_offset', {'data': -5});
+ this.trigger_up('change_offset', {'data': -1});
},
/**
* @private
*/
_onNext: function () {
- this.trigger_up('change_offset', {'data': 5});
+ this.trigger_up('change_offset', {'data': 1});
},
/**
* @private
diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml
index 7b50cff9ebb..20ef80c45c8 100644
--- a/addons/account/static/src/xml/account_reconciliation.xml
+++ b/addons/account/static/src/xml/account_reconciliation.xml
@@ -25,7 +25,7 @@
-
+
Tip: Hit CTRL-Enter to reconcile all the balanced items in the sheet.
@@ -129,8 +129,8 @@
Create Write-offOpen balanceChoose counterpart
-
-
+
+
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index a8fa2c6f233..dda6ffa0664 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -475,7 +475,10 @@ var session = {
var options = {
context: {
statement_ids: [4]
- }
+ },
+ params: {
+ limitMoveLines: 5,
+ },
};
Datas.params = {
@@ -1051,7 +1054,7 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
- 'data': this.params.data,
+ data: this.params.data,
session: this.params.session,
});
clientAction.appendTo($('#qunit-fixture'));
diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py
index 84aa784fad3..b39d36f7bf3 100644
--- a/addons/account/tests/test_tax.py
+++ b/addons/account/tests/test_tax.py
@@ -1,6 +1,7 @@
+# -*- coding: utf-8 -*-
+
from odoo.addons.account.tests.account_test_users import AccountTestUsers
-from odoo.tests.common import TransactionCase
-from odoo.tools import float_compare
+
import time
@@ -27,6 +28,12 @@ class TestTax(AccountTestUsers):
'amount': 10,
'sequence': 3,
})
+ self.percent_tax_bis = self.tax_model.create({
+ 'name': "Percent tax bis",
+ 'amount_type': 'percent',
+ 'amount': 21,
+ 'sequence': 3,
+ })
self.division_tax = self.tax_model.create({
'name': "Division tax",
'amount_type': 'division',
@@ -67,47 +74,125 @@ class TestTax(AccountTestUsers):
self.bank_account = self.bank_journal.default_debit_account_id
self.expense_account = self.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later
+ def _check_compute_all_results(self, base, total_included, total_excluded, taxes, res):
+ self.assertAlmostEqual(res['base'], base)
+ self.assertAlmostEqual(res['total_included'], total_included)
+ self.assertAlmostEqual(res['total_excluded'], total_excluded)
+ for i in range(0, len(taxes)):
+ self.assertAlmostEqual(res['taxes'][i]['base'], taxes[i][0])
+ self.assertAlmostEqual(res['taxes'][i]['amount'], taxes[i][1])
+
def test_tax_group_of_group_tax(self):
self.fixed_tax.include_base_amount = True
- self.group_tax.include_base_amount = True
- self.group_of_group_tax.include_base_amount = True
res = self.group_of_group_tax.compute_all(200.0)
- self.assertEquals(res['total_excluded'], 200.0)
- # After calculation of first group
- # base = 210
- # total_included = 231
- # Base of the first grouped is passed
- # Base after the second group (220) is dropped.
- # Base of the group of groups is passed out,
- # so we obtain base as after first group
- self.assertEquals(res['base'], 210.0)
- self.assertEquals(res['total_included'], 263.0)
+ self._check_compute_all_results(
+ 220, # 'base'
+ 263, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | | t
+ (210.0, 21.0), # | 3 | 10% | |
+ (210.0, 10.0), # | 1 | 10 | | t
+ (220.0, 22.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_group(self):
res = self.group_tax.compute_all(200.0)
- self.assertEquals(res['total_excluded'], 200.0)
- self.assertEquals(res['total_included'], 230.0)
- self.assertEquals(len(res['taxes']), 2)
- self.assertEquals(res['taxes'][0]['amount'], 10.0)
- self.assertEquals(res['taxes'][1]['amount'], 20.0)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 230, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | |
+ (200.0, 20.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_percent_division(self):
self.division_tax.price_include = True
self.division_tax.include_base_amount = True
+ res_division = self.division_tax.compute_all(200.0)
+ self._check_compute_all_results(
+ 220, # 'base'
+ 220, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 20.0), # | 4 | 10/ | t | t
+ # ---------------------------------------------------
+ ],
+ res_division
+ )
self.percent_tax.price_include = False
self.percent_tax.include_base_amount = False
- res_division = self.division_tax.compute_all(200.0)
res_percent = self.percent_tax.compute_all(200.0)
- self.assertEquals(res_division['taxes'][0]['amount'], 20.0)
- self.assertEquals(res_percent['taxes'][0]['amount'], 20.0)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 220, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 20.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
self.division_tax.price_include = False
self.division_tax.include_base_amount = False
+ res_division = self.division_tax.compute_all(200.0)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 222.22, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 22.22), # | 4 | 10/ | |
+ # ---------------------------------------------------
+ ],
+ res_division
+ )
self.percent_tax.price_include = True
self.percent_tax.include_base_amount = True
- res_division = self.division_tax.compute_all(200.0)
res_percent = self.percent_tax.compute_all(200.0)
- self.assertEquals(res_division['taxes'][0]['amount'], 22.22)
- self.assertEquals(res_percent['taxes'][0]['amount'], 18.18)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 200, # 'total_included'
+ 181.82, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (181.82, 18.18), # | 3 | 10% | t | t
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
+ self.percent_tax_bis.price_include = True
+ self.percent_tax_bis.include_base_amount = True
+ res_percent = self.percent_tax_bis.compute_all(7.0)
+ self._check_compute_all_results(
+ 7.0, # 'base'
+ 7.0, # 'total_included'
+ 5.79, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (5.79, 1.21), # | 3 | 21% | t | t
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
def test_tax_sequence_normalized_set(self):
self.division_tax.sequence = 1
@@ -115,19 +200,53 @@ class TestTax(AccountTestUsers):
self.percent_tax.sequence = 3
taxes_set = (self.group_tax | self.division_tax)
res = taxes_set.compute_all(200.0)
- self.assertEquals(res['taxes'][0]['amount'], 22.22)
- self.assertEquals(res['taxes'][1]['amount'], 10.0)
- self.assertEquals(res['taxes'][2]['amount'], 20.0)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 252.22, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 22.22), # | 1 | 10/ | |
+ (200.0, 10.0), # | 2 | 10 | |
+ (200.0, 20.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_include_base_amount(self):
self.fixed_tax.include_base_amount = True
res = self.group_tax.compute_all(200.0)
- self.assertEquals(res['total_included'], 231.0)
+ self._check_compute_all_results(
+ 210, # 'base'
+ 231, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | | t
+ (210.0, 21.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_currency(self):
self.division_tax.amount = 15.0
res = self.division_tax.compute_all(200.0, currency=self.env.ref('base.VEF'))
- self.assertAlmostEqual(res['total_included'], 235.2941)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 235.2941, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 35.2941), # | 4 | 15/ | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_move_lines_creation(self):
""" Test that creating a move.line with tax_ids generates the tax move lines and adjust line amount when a tax is price_include """
@@ -172,3 +291,58 @@ class TestTax(AccountTestUsers):
aml_with_taxes = move.line_ids.filtered(lambda l: set(l.tax_ids.ids) == set([self.group_tax.id, self.fixed_tax_bis.id]))
self.assertEquals(len(aml_with_taxes), 1)
self.assertEquals(aml_with_taxes.credit, 190)
+
+ def test_advanced_taxes_computation_0(self):
+ '''Test more advanced taxes computation (see issue 34471).'''
+ tax_1 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_1',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'include_base_amount': True,
+ 'sequence': 1,
+ })
+ tax_2 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_2',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'sequence': 2,
+ })
+ tax_3 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_3',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'sequence': 3,
+ })
+ tax_4 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_4',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'sequence': 4,
+ })
+ tax_5 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_5',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'sequence': 5,
+ })
+ taxes = tax_1 + tax_2 + tax_3 + tax_4 + tax_5
+ res = taxes.compute_all(132.0)
+ self._check_compute_all_results(
+ 110, # 'base'
+ 154, # 'total_included'
+ 100, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (100.0, 10.0), # | 1 | 10% | t | t
+ (110.0, 11.0), # | 3 | 10% | |
+ (110.0, 11.0), # | 3 | 10% | t |
+ (110.0, 11.0), # | 3 | 10% | |
+ (110.0, 11.0), # | 3 | 10% | t |
+ # ---------------------------------------------------
+ ],
+ res
+ )
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 20c63983ec4..db8accc8808 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -322,10 +322,12 @@
-
+
+
+
diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py
index fc4789d0706..dd22ea851f3 100644
--- a/addons/account_asset/models/account_invoice.py
+++ b/addons/account_asset/models/account_invoice.py
@@ -12,6 +12,16 @@ from odoo.addons import decimal_precision as dp
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
+ @api.model
+ def _refund_cleanup_lines(self, lines):
+ result = super(AccountInvoice, self)._refund_cleanup_lines(lines)
+ for i, line in enumerate(lines):
+ for name, field in line._fields.items():
+ if name == 'asset_category_id':
+ result[i][2][name] = False
+ break
+ return result
+
@api.multi
def action_cancel(self):
res = super(AccountInvoice, self).action_cancel()
diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py
index 0d09f1e68aa..e231cd0f6d0 100644
--- a/addons/auth_oauth/controllers/main.py
+++ b/addons/auth_oauth/controllers/main.py
@@ -6,6 +6,7 @@ import logging
import json
+import werkzeug.urls
import werkzeug.utils
from werkzeug.exceptions import BadRequest
@@ -148,7 +149,11 @@ class OAuthController(http.Controller):
url = '/web#action=%s' % action
elif menu:
url = '/web#menu_id=%s' % menu
- return login_and_redirect(*credentials, redirect_url=url)
+ resp = login_and_redirect(*credentials, redirect_url=url)
+ # Since /web is hardcoded, verify user has right to land on it
+ if werkzeug.urls.url_parse(resp.location).path == '/web' and not request.env.user.has_group('base.group_user'):
+ resp.location = '/'
+ return resp
except AttributeError:
# auth_signup is not installed
_logger.error("auth_signup not installed on database %s: oauth sign up cancelled." % (dbname,))
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index 720cdc1e71a..542283903f1 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -597,9 +597,18 @@ class Import(models.TransientModel):
@api.multi
def _parse_import_data(self, data, import_fields, options):
+ """ Lauch first call to _parse_import_data_recursive with an
+ empty prefix. _parse_import_data_recursive will be run
+ recursively for each relational field.
+ """
+ return self._parse_import_data_recursive(self.res_model, '', data, import_fields, options)
+
+ @api.multi
+ def _parse_import_data_recursive(self, model, prefix, data, import_fields, options):
# Get fields of type date/datetime
- all_fields = self.env[self.res_model].fields_get()
+ all_fields = self.env[model].fields_get()
for name, field in all_fields.items():
+ name = prefix + name
if field['type'] in ('date', 'datetime') and name in import_fields:
# Parse date
index = import_fields.index(name)
@@ -618,7 +627,11 @@ class Import(models.TransientModel):
raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e))
except Exception as e:
raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, e))
-
+ # Check if the field is in import_field and is a relational (followed by /)
+ # Also verify that the field name exactly match the import_field at the correct level.
+ elif any(name + '/' in import_field and name == import_field.split('/')[prefix.count('/')] for import_field in import_fields):
+ # Recursive call with the relational as new model and add the field name to the prefix
+ self._parse_import_data_recursive(field['relation'], name + '/', data, import_fields, options)
elif field['type'] in ('float', 'monetary') and name in import_fields:
# Parse float, sometimes float values from file have currency symbol or () to denote a negative value
# We should be able to manage both case
diff --git a/addons/base_import/tests/test_base_import.py b/addons/base_import/tests/test_base_import.py
index a59cfa9205c..55f3300da91 100644
--- a/addons/base_import/tests/test_base_import.py
+++ b/addons/base_import/tests/test_base_import.py
@@ -400,7 +400,33 @@ class test_convert_import_data(TransactionCase):
# if results empty, no errors
self.assertItemsEqual(results, [])
+ def test_parse_relational_fields(self):
+ """ Ensure that relational fields float and date are correctly
+ parsed during the import call.
+ """
+ import_wizard = self.env['base_import.import'].create({
+ 'res_model': 'res.partner',
+ 'file': u'name,parent_id/id,parent_id/date,parent_id/credit_limit\n'
+ u'"foo","__export__.res_partner_1","2017年10月12日","5,69"\n'.encode('utf-8'),
+ 'file_type': 'text/csv'
+ })
+ options = {
+ 'date_format': '%Y年%m月%d日',
+ 'quoting': '"',
+ 'separator': ',',
+ 'float_decimal_separator': ',',
+ 'float_thousand_separator': '.',
+ 'headers': True
+ }
+ data, import_fields = import_wizard._convert_import_data(
+ ['name', 'parent_id/.id', 'parent_id/date', 'parent_id/credit_limit'],
+ options
+ )
+ result = import_wizard._parse_import_data(data, import_fields, options)
+ # Check if the data 5,69 as been correctly parsed.
+ self.assertEqual(float(result[0][-1]), 5.69)
+ self.assertEqual(str(result[0][-2]), '2017-10-12')
def test_filtered(self):
""" If ``False`` is provided as field mapping for a column,
diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py
index dc547b2c422..2d2b37c939b 100644
--- a/addons/base_vat/models/res_partner.py
+++ b/addons/base_vat/models/res_partner.py
@@ -18,6 +18,12 @@ from odoo import api, models, _
from odoo.tools.misc import ustr
from odoo.exceptions import ValidationError
+_eu_country_vat = {
+ 'GR': 'EL'
+}
+
+_eu_country_vat_inverse = {v: k for k, v in _eu_country_vat.items()}
+
_ref_vat = {
'at': 'ATU12345675',
'be': 'BE0477472701',
@@ -79,6 +85,7 @@ class ResPartner(models.Model):
# Foreign companies that trade with non-enterprises in the EU
# may have a VATIN starting with "EU" instead of a country code.
return True
+ country_code = _eu_country_vat_inverse.get(country_code, country_code)
return bool(self.env['res.country'].search([('code', '=ilike', country_code)]))
return check_func(vat_number)
@@ -96,6 +103,19 @@ class ResPartner(models.Model):
# country code or empty VAT number), so we fall back to the simple check.
return self.simple_vat_check(country_code, vat_number)
+ @api.model
+ def fix_eu_vat_number(self, country_id, vat):
+ europe = self.env.ref('base.europe')
+ country = self.env["res.country"].browse(country_id)
+ if not europe:
+ europe = self.env["res.country.group"].search([('name', '=', 'Europe')], limit=1)
+ if europe and country and country.id in europe.country_ids.ids:
+ vat = re.sub('[^A-Za-z0-9]', '', vat).upper()
+ country_code = _eu_country_vat.get(country.code, country.code).upper()
+ if vat[:2] != country_code:
+ vat = country_code + vat
+ return vat
+
@api.constrains('vat', 'commercial_partner_country_id')
def check_vat(self):
if self.env.context.get('company_id'):
diff --git a/addons/base_vat_autocomplete/models/res_partner.py b/addons/base_vat_autocomplete/models/res_partner.py
index 3b25f99a8f1..41b40ec9c1d 100644
--- a/addons/base_vat_autocomplete/models/res_partner.py
+++ b/addons/base_vat_autocomplete/models/res_partner.py
@@ -6,6 +6,8 @@ import re
from odoo import api, models
+from suds.client import Client
+
_logger = logging.getLogger(__name__)
try:
@@ -14,6 +16,7 @@ except ImportError:
_logger.warning('Python `stdnum` library not found, unable to call VIES service to detect address based on VAT number.')
stdnum_vat = None
+
class ResPartner(models.Model):
_inherit = 'res.partner'
@@ -40,7 +43,17 @@ class ResPartner(models.Model):
if not partner.vat:
return {}
if len(partner.vat) > 5 and partner.vat[:2].lower() in stdnum_vat.country_codes:
- result = stdnum_vat.check_vies(partner.vat)
+ # Equivalent to stdnum_vat.check_vies(partner.vat).
+ # However, we want to add a custom timeout to the suds.client
+ # because by default, it's 120 seconds and this is to long.
+ try:
+ client = Client(stdnum_vat.vies_wsdl, timeout=5)
+ partner_vat = stdnum_vat.compact(partner.vat)
+ result = client.service.checkVat(partner_vat[:2], partner_vat[2:])
+ except:
+ # Avoid blocking the client when the service is unreachable/unavailable
+ return {}
+
if not result['valid']:
return {}
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 9c0ddc5c13f..37bd8e3e3ff 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -670,27 +670,27 @@ class Meeting(models.Model):
1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
2) if event all day ,return : AllDay, July-31-2013
"""
- timezone = self._context.get('tz')
- if not timezone:
- timezone = self.env.user.partner_id.tz or 'UTC'
- timezone = tools.ustr(timezone).encode('utf-8') # make safe for str{p,f}time()
+ timezone = self._context.get('tz') or self.env.user.partner_id.tz or 'UTC'
+ timezone = pycompat.to_native(timezone) # make safe for str{p,f}time()
# get date/time format according to context
- format_date, format_time = self.with_context(tz=timezone)._get_date_formats()
+ format_date, format_time = self._get_date_formats()
# convert date and time into user timezone
- date = fields.Datetime.context_timestamp(self.with_context(tz=timezone), fields.Datetime.from_string(start))
- date_deadline = fields.Datetime.context_timestamp(self.with_context(tz=timezone), fields.Datetime.from_string(stop))
+ self_tz = self.with_context(tz=timezone)
+ date = fields.Datetime.context_timestamp(self_tz, fields.Datetime.from_string(start))
+ date_deadline = fields.Datetime.context_timestamp(self_tz, fields.Datetime.from_string(stop))
# convert into string the date and time, using user formats
- date_str = date.strftime(format_date).decode('utf-8')
- time_str = date.strftime(format_time).decode('utf-8')
+ to_text = pycompat.to_text
+ date_str = to_text(date.strftime(format_date))
+ time_str = to_text(date.strftime(format_time))
if zallday:
display_time = _("AllDay , %s") % (date_str)
elif zduration < 24:
duration = date + timedelta(hours=zduration)
- duration_time = duration.strftime(format_time).decode('utf-8')
+ duration_time = to_text(duration.strftime(format_time))
display_time = _(u"%s at (%s To %s) (%s)") % (
date_str,
time_str,
@@ -698,8 +698,8 @@ class Meeting(models.Model):
timezone,
)
else:
- dd_date = date_deadline.strftime(format_date).decode('utf-8')
- dd_time = date_deadline.strftime(format_time).decode('utf-8')
+ dd_date = to_text(date_deadline.strftime(format_date))
+ dd_time = to_text(date_deadline.strftime(format_time))
display_time = _(u"%s at %s To\n %s at %s (%s)") % (
date_str,
time_str,
diff --git a/addons/crm/controllers/main.py b/addons/crm/controllers/main.py
index d92758e961b..4f1a6960645 100644
--- a/addons/crm/controllers/main.py
+++ b/addons/crm/controllers/main.py
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
from odoo.addons.mail.controllers.main import MailController
from odoo import http
+_logger = logging.getLogger(__name__)
+
class CrmController(http.Controller):
@@ -12,8 +15,9 @@ class CrmController(http.Controller):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
if comparison and record:
try:
- record.case_mark_won()
+ record.action_set_won()
except Exception:
+ _logger.exception("Could not mark crm.lead as won")
return MailController._redirect_to_messaging()
return redirect
@@ -22,8 +26,9 @@ class CrmController(http.Controller):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
if comparison and record:
try:
- record.case_mark_lost()
+ record.action_set_lost()
except Exception:
+ _logger.exception("Could not mark crm.lead as lost")
return MailController._redirect_to_messaging()
return redirect
@@ -34,5 +39,6 @@ class CrmController(http.Controller):
try:
record.convert_opportunity(record.partner_id.id)
except Exception:
+ _logger.exception("Could not convert crm.lead to opportunity")
return MailController._redirect_to_messaging()
return redirect
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 334b52be890..31a91c08885 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -267,6 +267,14 @@ class Lead(models.Model):
# ORM override (CRUD, fields_view_get, ...)
# ----------------------------------------
+ @api.model
+ def name_create(self, name):
+ res = super(Lead, self).name_create(name)
+
+ # update the probability of the lead if the stage is set to update it automatically
+ self.browse(res[0])._onchange_stage_id()
+ return res
+
@api.model
def create(self, vals):
# set up context used to find the lead's sales channel which is needed
diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml
index 6728604b8d8..24625320191 100644
--- a/addons/crm/report/crm_opportunity_report_views.xml
+++ b/addons/crm/report/crm_opportunity_report_views.xml
@@ -104,17 +104,10 @@
-
-
-
-
-
-
-
diff --git a/addons/crm/static/description/index.html b/addons/crm/static/description/index.html
index df8b9a447f8..6f4ab397d69 100644
--- a/addons/crm/static/description/index.html
+++ b/addons/crm/static/description/index.html
@@ -43,7 +43,7 @@
Automate activities based on your sales scripts: calls, meetings, mails and quotations. Get all information you need, right on the opportunity: page viewed, mails received, etc.
-
+
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index 60e7c350c70..a48a6dda3b0 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -385,7 +385,7 @@
domain="[('activity_ids.user_id', '=', uid)]"/>
@@ -630,7 +630,7 @@
domain="[('activity_ids.user_id', '=', uid)]"/>
@@ -640,7 +640,7 @@
+ [('project_id','!=',False)]
diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py
index 02f083110e7..3d42918c2ad 100644
--- a/addons/hw_proxy/controllers/main.py
+++ b/addons/hw_proxy/controllers/main.py
@@ -18,9 +18,9 @@ _logger = logging.getLogger(__name__)
# Those are the builtin raspberry pi USB modules, they should
# not appear in the list of connected devices.
BANNED_DEVICES = {
- "0424:9514", # Standard Microsystem Corp. Builtin Ethernet module
- "1d6b:0002", # Linux Foundation 2.0 root hub
- "0424:ec00", # Standard Microsystem Corp. Other Builtin Ethernet module
+ "0424:9514", # Standard Microsystem Corp. Builtin Ethernet module
+ "1d6b:0002", # Linux Foundation 2.0 root hub
+ "0424:ec00", # Standard Microsystem Corp. Other Builtin Ethernet module
}
diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py
index 659ea1974c5..5e264dc6715 100644
--- a/addons/iap/models/iap.py
+++ b/addons/iap/models/iap.py
@@ -8,6 +8,7 @@ import werkzeug.urls
import requests
from odoo import api, fields, models, exceptions
+from odoo.tools import pycompat
_logger = logging.getLogger(__name__)
@@ -57,8 +58,10 @@ def jsonrpc(url, method='call', params=None):
e_class = InsufficientCreditError
elif name == 'AccessError':
e_class = exceptions.AccessError
- else:
+ elif name == 'UserError':
e_class = exceptions.UserError
+ else:
+ raise requests.exceptions.ConnectionError()
e = e_class(message)
e.data = response['error']['data']
raise e
@@ -93,7 +96,7 @@ def charge(env, key, account_token, credit, description=None, credit_template=No
except InsufficientCreditError as e:
if credit_template:
arguments = json.loads(e.args[0])
- arguments['body'] = env['ir.qweb'].render(credit_template)
+ arguments['body'] = pycompat.to_text(env['ir.qweb'].render(credit_template))
e.args = (json.dumps(arguments),)
raise e
try:
diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml
index 36c26f56eb4..34789916a48 100644
--- a/addons/mail/data/mail_data.xml
+++ b/addons/mail/data/mail_data.xml
@@ -71,6 +71,7 @@
% if ctx.get('has_button_access'):
@@ -110,7 +111,7 @@
% endif
-
+
diff --git a/addons/mail/data/mail_shortcode_data.xml b/addons/mail/data/mail_shortcode_data.xml
index 64f1aa7129f..afd0cf91146 100644
--- a/addons/mail/data/mail_shortcode_data.xml
+++ b/addons/mail/data/mail_shortcode_data.xml
@@ -103,8 +103,8 @@
3:(
- 😈
- <img src='/mail/static/src/img/smiley/angry.png' alt='😈'/>
+ 😠
+ <img src='/mail/static/src/img/smiley/angry.png' alt='😠'/>Angryimage
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 25b01ce80b5..a47a6ffac6b 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -251,7 +251,7 @@ class Channel(models.Model):
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if not author or author not in self.channel_partner_ids:
return {
- 'error_mesage': _('restricted to channel members'),
+ 'error_message': _('restricted to channel members'),
}
return True
return super(Channel, self)._alias_check_contact(message, message_dict, alias)
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index fd736269968..53ed1da740c 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -829,11 +829,13 @@ class MailThread(models.AbstractModel):
def _routing_warn(self, error_message, warn_suffix, message_id, route, raise_exception):
""" Tools method used in message_route_verify: whether to log a warning or raise an error """
- full_message = _('Routing mail with Message-Id %s: route %s: %s') % (message_id, route, error_message)
+ short_message = _("Mailbox unavailable - %s") % error_message
+ full_message = ('Routing mail with Message-Id %s: route %s: %s' %
+ (message_id, route, error_message))
+ _logger.info(full_message + (warn_suffix and '; %s' % warn_suffix or ''))
if raise_exception:
- raise ValueError(full_message)
- else:
- _logger.info(full_message + warn_suffix and '; %s' % warn_suffix or '')
+ # sender should not see private diagnostics info, just the error
+ raise ValueError(short_message)
def _routing_create_bounce_email(self, email_from, body_html, message):
bounce_to = tools.decode_message_header(message, 'Return-Path') or email_from
@@ -1387,6 +1389,7 @@ class MailThread(models.AbstractModel):
node.getparent().remove(node)
if postprocessed:
body = etree.tostring(root, pretty_print=False, encoding='UTF-8')
+ body = pycompat.to_native(body)
return body, attachments
def _message_extract_payload(self, message, save_original=False):
@@ -1495,10 +1498,8 @@ class MailThread(models.AbstractModel):
'message_type': 'email',
}
if not isinstance(message, Message):
- # message_from_string works on a native str, so on py2 we need to
- # encode incoming unicode strings
- if pycompat.PY2 and not isinstance(message, str):
- message = message.encode('utf-8')
+ # message_from_string works on a native str
+ message = pycompat.to_native(message)
message = email.message_from_string(message)
message_id = message['message-id']
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index 0b7533c853d..3a81423a9b2 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -54,7 +54,6 @@ class Partner(models.Model):
website_url = 'http://%s' % user.company_id.website if not user.company_id.website.lower().startswith(('http:', 'https:')) else user.company_id.website
else:
website_url = False
- company_name = user.company_id.name
model_name = False
if message.model:
@@ -74,9 +73,15 @@ class Partner(models.Model):
if message.res_id and message.model in self.env:
record = self.env[message.model].browse(message.res_id)
+ company = user.company_id;
+ if record and hasattr(record, 'company_id'):
+ company = record.company_id;
+ company_name = company.name;
+
return {
'signature': signature,
'website_url': website_url,
+ 'company': company,
'company_name': company_name,
'model_name': model_name,
'record': record,
diff --git a/addons/mail/wizard/email_template_preview_view.xml b/addons/mail/wizard/email_template_preview_view.xml
index 7d5cd1cb6d6..67068f5dc84 100644
--- a/addons/mail/wizard/email_template_preview_view.xml
+++ b/addons/mail/wizard/email_template_preview_view.xml
@@ -28,6 +28,9 @@
+
diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py
index bcee631aad6..ab902ee7d75 100644
--- a/addons/mass_mailing/controllers/main.py
+++ b/addons/mass_mailing/controllers/main.py
@@ -17,10 +17,10 @@ class MassMailController(http.Controller):
if mailing.exists():
res_id = res_id and int(res_id)
res_ids = []
- if mailing.mailing_model_name == 'mail.mass_mailing.contact':
+ if mailing.mailing_model_name == 'mail.mass_mailing.list':
contacts = request.env['mail.mass_mailing.contact'].sudo().search([
('email', '=', email),
- ('list_id', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids])
+ ('list_ids', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids])
])
res_ids = contacts.ids
else:
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index ca2e739c35c..2f3666efc77 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -15,6 +15,16 @@ from odoo.tools.translate import html_translate
_logger = logging.getLogger(__name__)
+MASS_MAILING_BUSINESS_MODELS = [
+ 'crm.lead',
+ 'event.registration',
+ 'hr.applicant',
+ 'res.partner',
+ 'event.track',
+ 'sale.order',
+ 'mail.mass_mailing.list',
+]
+
class MassMailingTag(models.Model):
"""Model of categories of mass mailing, i.e. marketing, newsletter, ... """
_name = 'mail.mass_mailing.tag'
@@ -320,7 +330,7 @@ class MassMailing(models.Model):
default=lambda self: self.env['mail.message']._get_default_from())
# recipients
mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True)
- mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain="[('is_mail_thread', '=', True)]",
+ mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id)
mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name')
mailing_domain = fields.Char(string='Domain', oldname='domain', default=[])
diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
index a97f055f6c9..5190cd00838 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
@@ -404,4 +404,15 @@ odoo_top[callback+"_updown"] = function (value, fields_values, field_name) {
if ($editable_area.html().indexOf('on_change_model_and_list') !== -1) {
$editable_area.empty();
}
+// Adding compatibility for the outlook compliance of mailings.
+// Commit of such compatibility : a14f89c8663c9cafecb1cc26918055e023ecbe42
+options.registry.background.include({
+ start: function() {
+ this._super();
+ var $table_target = this.$target.find('table:first');
+ if ($table_target) {
+ this.$target = $table_target;
+ }
+ }
+});
});
diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml
index 31f4664f552..4875421e812 100644
--- a/addons/mass_mailing/views/mass_mailing_views.xml
+++ b/addons/mass_mailing/views/mass_mailing_views.xml
@@ -415,7 +415,7 @@
-
+ date_from and line.invoice_id.date_invoice < date_to:
+ if line.invoice_id.date_invoice > (date_from or '0000-00-00') and line.invoice_id.date_invoice < (date_to or '0000-00-00'):
date_from = line.invoice_id.date_invoice
MemberLine.create({
'partner': line.invoice_id.partner_id.id,
@@ -60,9 +60,9 @@ class AccountInvoiceLine(models.Model):
invoice_line.product_id.membership and \
not MemberLine.search([('account_invoice_line', '=', invoice_line.id)]):
# Product line is a membership product
- date_from = invoice_line.product_id.membership_date_from or '0000-00-00'
- date_to = invoice_line.product_id.membership_date_to or '0000-00-00'
- if date_from < (invoice_line.invoice_id.date_invoice or '0000-00-00') < date_to:
+ date_from = invoice_line.product_id.membership_date_from
+ date_to = invoice_line.product_id.membership_date_to
+ if date_from and date_from < (invoice_line.invoice_id.date_invoice or '0000-00-00') < (date_to or '0000-00-00'):
date_from = invoice_line.invoice_id.date_invoice
MemberLine.create({
'partner': invoice_line.invoice_id.partner_id.id,
diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py
index 89da36bc273..348d4d04dfa 100644
--- a/addons/membership/models/partner.py
+++ b/addons/membership/models/partner.py
@@ -108,7 +108,7 @@ class Partner(models.Model):
s = 4
if partner.member_lines:
for mline in partner.member_lines:
- if mline.date_to >= today and mline.date_from <= today:
+ if (mline.date_to or '0000-00-00') >= today and (mline.date_from or '0000-00-00') <= today:
if mline.account_invoice_line.invoice_id:
mstate = mline.account_invoice_line.invoice_id.state
if mstate == 'paid':
@@ -126,7 +126,7 @@ class Partner(models.Model):
s = 3
if s == 4:
for mline in partner.member_lines:
- if mline.date_from < today and mline.date_to < today and mline.date_from <= mline.date_to and mline.account_invoice_line and mline.account_invoice_line.invoice_id.state == 'paid':
+ if (mline.date_from or '0000-00-00') < today and (mline.date_to or '0000-00-00') < today and (mline.date_from or '0000-00-00') <= (mline.date_to or '0000-00-00') and mline.account_invoice_line and mline.account_invoice_line.invoice_id.state == 'paid':
s = 5
else:
s = 6
diff --git a/addons/mrp/i18n/mrp.pot b/addons/mrp/i18n/mrp.pot
index 617117deea1..0b2fac329be 100644
--- a/addons/mrp/i18n/mrp.pot
+++ b/addons/mrp/i18n/mrp.pot
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-10 11:34+0000\n"
-"PO-Revision-Date: 2017-10-10 11:34+0000\n"
+"POT-Creation-Date: 2017-10-16 11:13+0000\n"
+"PO-Revision-Date: 2017-10-16 11:13+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -423,18 +423,12 @@ msgstr ""
#: model:ir.actions.act_window,name:mrp.template_open_bom
#: model:ir.model.fields,field_description:mrp.field_product_product_bom_ids
#: model:ir.model.fields,field_description:mrp.field_product_template_bom_ids
-#: model:ir.ui.view,arch_db:mrp.mrp_bom_line_tree_view
#: model:ir.ui.view,arch_db:mrp.mrp_bom_tree_view
#: model:ir.ui.view,arch_db:mrp.product_product_form_view_bom_button
#: model:ir.ui.view,arch_db:mrp.product_template_form_view_bom_button
msgid "Bill of Materials"
msgstr ""
-#. module: mrp
-#: model:ir.actions.act_window,name:mrp.mrp_bom_line_action_from_bom
-msgid "Bill of Materials Structure"
-msgstr ""
-
#. module: mrp
#: model:ir.model.fields,help:mrp.field_mrp_production_bom_id
msgid "Bill of Materials allow you to define the list of required raw materials to make a finished product."
@@ -1202,6 +1196,21 @@ msgstr ""
msgid "If the manufacturing order is unlocked you can add to the initial demand."
msgstr ""
+#. module: mrp
+#: model:ir.ui.view,arch_db:mrp.mrp_bom_form_view
+msgid "If the product is a finished product: When processing a sales\n"
+" order for this product, the delivery order will contain the raw\n"
+" materials, instead of the finished product."
+msgstr ""
+
+#. module: mrp
+#: model:ir.ui.view,arch_db:mrp.mrp_bom_form_view
+msgid "If the product is a semi-finished product: When processing a\n"
+" manufacturing order that contains that product as component,\n"
+" the raw materials of that product will be added to the\n"
+" manufacturing order of the final product."
+msgstr ""
+
#. module: mrp
#: model:ir.ui.view,arch_db:mrp.mrp_workcenter_kanban
#: model:ir.ui.view,arch_db:mrp.stock_production_type_kanban
@@ -1301,14 +1310,10 @@ msgstr ""
#. module: mrp
#: model:ir.ui.view,arch_db:mrp.view_mrp_bom_filter
+#: selection:mrp.bom,type:0
msgid "Kit"
msgstr ""
-#. module: mrp
-#: model:ir.model.fields,help:mrp.field_mrp_bom_type
-msgid "Kit (Phantom): When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product."
-msgstr ""
-
#. module: mrp
#: model:product.product,name:mrp.product_product_laptop_charger
#: model:product.template,name:mrp.product_product_laptop_charger_product_template
@@ -2601,11 +2606,6 @@ msgstr ""
msgid "Settings"
msgstr ""
-#. module: mrp
-#: selection:mrp.bom,type:0
-msgid "Ship this product as a set of components (kit)"
-msgstr ""
-
#. module: mrp
#: code:addons/mrp/models/mrp_unbuild.py:89
#, python-format
@@ -3206,12 +3206,6 @@ msgstr ""
msgid "When a procurement has a ‘produce’ route with a operation type set, it will try to create a Manufacturing Order for that product using a BoM of the same operation type. That allows to define procurement rules which trigger different manufacturing orders with different BoMs."
msgstr ""
-#. module: mrp
-#: model:ir.ui.view,arch_db:mrp.mrp_bom_form_view
-msgid "When processing a sales order for this product, the delivery order\n"
-" will contain the raw materials, instead of the finished product."
-msgstr ""
-
#. module: mrp
#: model:ir.model.fields,help:mrp.field_stock_warehouse_manufacture_to_resupply
msgid "When products are manufactured, they can be manufactured in this warehouse."
diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py
index f41e4a5a111..4d9a0e5bcbe 100644
--- a/addons/mrp/models/mrp_bom.py
+++ b/addons/mrp/models/mrp_bom.py
@@ -24,9 +24,8 @@ class MrpBom(models.Model):
help="If the active field is set to False, it will allow you to hide the bills of material without removing it.")
type = fields.Selection([
('normal', 'Manufacture this product'),
- ('phantom', 'Ship this product as a set of components (kit)')], 'BoM Type',
- default='normal', required=True,
- help="Kit (Phantom): When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product.")
+ ('phantom', 'Kit')], 'BoM Type',
+ default='normal', required=True)
product_tmpl_id = fields.Many2one(
'product.template', 'Product',
domain="[('type', 'in', ['product', 'consu'])]", required=True)
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 59bafc27487..4a56ef217bc 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -107,7 +107,7 @@ class MrpProduction(models.Model):
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain=[('scrapped', '=', False)])
finished_move_line_ids = fields.One2many(
- 'stock.move.line', compute='_compute_lines', inverse='_inverse_lines',
+ 'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product"
)
workorder_ids = fields.One2many(
'mrp.workorder', 'production_id', 'Work Orders',
diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml
index 19b56e17c2e..299f96c1b61 100644
--- a/addons/mrp/views/mrp_bom_views.xml
+++ b/addons/mrp/views/mrp_bom_views.xml
@@ -29,9 +29,20 @@
-
- When processing a sales order for this product, the delivery order
- will contain the raw materials, instead of the finished product.
+
+
+
+ If the product is a finished product: When processing a sales
+ order for this product, the delivery order will contain the raw
+ materials, instead of the finished product.
+
+
+ If the product is a semi-finished product: When processing a
+ manufacturing order that contains that product as component,
+ the raw materials of that product will be added to the
+ manufacturing order of the final product.
+