[MERGE] forward port branch 11.0 up to c2e878e34c

This commit is contained in:
Christophe Simonis
2018-03-01 19:43:25 +01:00
131 changed files with 3253 additions and 530 deletions
+5
View File
@@ -4,6 +4,7 @@ include README.md
recursive-include odoo *.css recursive-include odoo *.css
recursive-include odoo *.csv recursive-include odoo *.csv
recursive-include odoo *.doc recursive-include odoo *.doc
recursive-include odoo *.docx
recursive-include odoo *.eml recursive-include odoo *.eml
recursive-include odoo *.eot recursive-include odoo *.eot
recursive-include odoo *.gif recursive-include odoo *.gif
@@ -16,6 +17,8 @@ recursive-include odoo *.less
recursive-include odoo *.md recursive-include odoo *.md
recursive-include odoo *.mp3 recursive-include odoo *.mp3
recursive-include odoo *.ogg recursive-include odoo *.ogg
recursive-include odoo *.ods
recursive-include odoo *.odt
recursive-include odoo *.otf recursive-include odoo *.otf
recursive-include odoo *.pdf recursive-include odoo *.pdf
recursive-include odoo *.png recursive-include odoo *.png
@@ -35,8 +38,10 @@ recursive-include odoo *.wsdl
recursive-include odoo *.xls recursive-include odoo *.xls
recursive-include odoo *.xsd recursive-include odoo *.xsd
recursive-include odoo *.xsl recursive-include odoo *.xsl
recursive-include odoo *.xlsx
recursive-include odoo *.xml recursive-include odoo *.xml
recursive-include odoo *.yml recursive-include odoo *.yml
recursive-include odoo *.zip
recursive-include odoo/addons/l10n_mx_edi *.xslt *.key *.cer *.txt recursive-include odoo/addons/l10n_mx_edi *.xslt *.key *.cer *.txt
recursive-include odoo/addons/l10n_mx_reports *.xslt recursive-include odoo/addons/l10n_mx_reports *.xslt
recursive-exclude * *.py[co] recursive-exclude * *.py[co]
+1 -1
View File
@@ -14,7 +14,7 @@
<field name="lang">${object.partner_id.lang}</field> <field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="xml"> <field name="body_html" type="xml">
<p>Dear ${object.partner_id.name},</p> <p>Dear ${object.partner_id.name},</p>
<p>Thank you for your payment.<br />Here is your payment receipt <strong>${(object.name or '').replace('/','-')}</strong> amounting to <strong>${object.amount} ${object.currency_id.name}</strong> from ${object.company_id.name}.</p> <p>Thank you for your payment.<br />Here is your payment receipt <strong>${(object.name or '').replace('/','-')}</strong> amounting to <strong>${format_amount(object.amount, object.currency_id)}</strong> from ${object.company_id.name}.</p>
<p>If you have any questions, please do not hesitate to contact us.</p> <p>If you have any questions, please do not hesitate to contact us.</p>
<p>Best regards, <p>Best regards,
% if user and user.signature: % if user and user.signature:
+5 -1
View File
@@ -443,7 +443,7 @@ class AccountJournal(models.Model):
for journal in self: for journal in self:
if journal.sequence_id and journal.sequence_number_next: if journal.sequence_id and journal.sequence_number_next:
sequence = journal.sequence_id._get_current_sequence() sequence = journal.sequence_id._get_current_sequence()
sequence.number_next = journal.sequence_number_next sequence.sudo().number_next = journal.sequence_number_next
@api.multi @api.multi
# do not depend on 'refund_sequence_id.date_range_ids', because # do not depend on 'refund_sequence_id.date_range_ids', because
@@ -525,6 +525,8 @@ class AccountJournal(models.Model):
if ('company_id' in vals and journal.company_id.id != vals['company_id']): if ('company_id' in vals and journal.company_id.id != vals['company_id']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its company.')) raise UserError(_('This journal already contains items, therefore you cannot modify its company.'))
if self.bank_account_id:
self.bank_account_id.company_id = vals['company_id']
if ('code' in vals and journal.code != vals['code']): if ('code' in vals and journal.code != vals['code']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its short name.')) raise UserError(_('This journal already contains items, therefore you cannot modify its short name.'))
@@ -538,6 +540,8 @@ class AccountJournal(models.Model):
self.default_debit_account_id.currency_id = vals['currency_id'] self.default_debit_account_id.currency_id = vals['currency_id']
if not 'default_credit_account_id' in vals and self.default_credit_account_id: if not 'default_credit_account_id' in vals and self.default_credit_account_id:
self.default_credit_account_id.currency_id = vals['currency_id'] self.default_credit_account_id.currency_id = vals['currency_id']
if self.bank_account_id:
self.bank_account_id.currency_id = vals['currency_id']
if 'bank_account_id' in vals and not vals.get('bank_account_id'): if 'bank_account_id' in vals and not vals.get('bank_account_id'):
raise UserError(_('You cannot remove the bank account from the journal once set.')) raise UserError(_('You cannot remove the bank account from the journal once set.'))
result = super(AccountJournal, self).write(vals) result = super(AccountJournal, self).write(vals)
@@ -340,7 +340,7 @@ class AccountBankStatement(models.Model):
def link_bank_to_partner(self): def link_bank_to_partner(self):
for statement in self: for statement in self:
for st_line in statement.line_ids: for st_line in statement.line_ids:
if st_line.bank_account_id and st_line.partner_id and st_line.bank_account_id.partner_id != st_line.partner_id: if st_line.bank_account_id and st_line.partner_id and not st_line.bank_account_id.partner_id:
st_line.bank_account_id.partner_id = st_line.partner_id st_line.bank_account_id.partner_id = st_line.partner_id
@@ -341,12 +341,13 @@ class account_journal(models.Model):
ctx.update({ ctx.update({
'journal_type': self.type, 'journal_type': self.type,
'default_journal_id': self.id, 'default_journal_id': self.id,
'search_default_journal_id': self.id,
'default_type': invoice_type, 'default_type': invoice_type,
'type': invoice_type 'type': invoice_type
}) })
[action] = self.env.ref('account.%s' % action_name).read() [action] = self.env.ref('account.%s' % action_name).read()
if not self.env.context.get('use_domain'):
ctx['search_default_journal_id'] = self.id
action['context'] = ctx action['context'] = ctx
action['domain'] = self._context.get('use_domain', []) action['domain'] = self._context.get('use_domain', [])
account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False) account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False)
+2 -2
View File
@@ -106,7 +106,7 @@ class AccountMove(models.Model):
default=lambda self: self.env.user.company_id) default=lambda self: self.env.user.company_id)
matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method")
# Dummy Account field to search on account.move by account_id # Dummy Account field to search on account.move by account_id
dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False) dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True)
tax_cash_basis_rec_id = fields.Many2one( tax_cash_basis_rec_id = fields.Many2one(
'account.partial.reconcile', 'account.partial.reconcile',
string='Tax Cash Basis Entry of', string='Tax Cash Basis Entry of',
@@ -1361,7 +1361,7 @@ class AccountMoveLine(models.Model):
record.payment_id.state = 'reconciled' record.payment_id.state = 'reconciled'
result = super(AccountMoveLine, self).write(vals) result = super(AccountMoveLine, self).write(vals)
if self._context.get('check_move_validity', True): if self._context.get('check_move_validity', True) and any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')):
move_ids = set() move_ids = set()
for line in self: for line in self:
if line.move_id.id not in move_ids: if line.move_id.id not in move_ids:
+2 -2
View File
@@ -576,9 +576,9 @@ class account_payment(models.Model):
writeoff_line['amount_currency'] = amount_currency_wo writeoff_line['amount_currency'] = amount_currency_wo
writeoff_line['currency_id'] = currency_id writeoff_line['currency_id'] = currency_id
writeoff_line = aml_obj.create(writeoff_line) writeoff_line = aml_obj.create(writeoff_line)
if counterpart_aml['debit'] or writeoff_line['credit']: if counterpart_aml['debit'] or (writeoff_line['credit'] and not counterpart_aml['credit']):
counterpart_aml['debit'] += credit_wo - debit_wo counterpart_aml['debit'] += credit_wo - debit_wo
if counterpart_aml['credit'] or writeoff_line['debit']: if counterpart_aml['credit'] or (writeoff_line['debit'] and not counterpart_aml['debit']):
counterpart_aml['credit'] += debit_wo - credit_wo counterpart_aml['credit'] += debit_wo - credit_wo
counterpart_aml['amount_currency'] -= amount_currency_wo counterpart_aml['amount_currency'] -= amount_currency_wo
@@ -526,17 +526,37 @@ var StatementModel = BasicModel.extend({
*/ */
togglePartialReconcile: function (handle) { togglePartialReconcile: function (handle) {
var line = this.getLine(handle); var line = this.getLine(handle);
var props = _.filter(line.reconciliation_proposition, {'invalid': false});
var prop = props[0]; // Retrieve the toggle proposition
if (props.length !== 1 || Math.abs(line.st_line.amount) >= Math.abs(prop.amount)) { var selected;
_.each(line.reconciliation_proposition, function (prop) {
if (!prop.invalid) {
if (((line.balance.amount < 0 || !line.partial_reconcile) && prop.amount > 0 && line.st_line.amount > 0 && line.st_line.amount < prop.amount) ||
((line.balance.amount > 0 || !line.partial_reconcile) && prop.amount < 0 && line.st_line.amount < 0 && line.st_line.amount > prop.amount)) {
selected = prop;
return false;
}
}
});
// If no toggled proposition found, reject it
if (selected == null)
return $.Deferred().reject(); return $.Deferred().reject();
}
prop.partial_reconcile = !prop.partial_reconcile; // Inverse partial_reconcile value
if (!prop.partial_reconcile) { selected.partial_reconcile = !selected.partial_reconcile;
if (!selected.partial_reconcile) {
return this._computeLine(line); return this._computeLine(line);
} }
// Compute the write_off
var format_options = { currency_id: line.st_line.currency_id };
selected.write_off_amount = selected.amount + line.balance.amount;
selected.write_off_amount_str = field_utils.format.monetary(Math.abs(selected.write_off_amount), {}, format_options);
selected.write_off_amount_str = selected.write_off_amount_str.replace('&nbsp;', ' ');
return this._computeLine(line).then(function () { return this._computeLine(line).then(function () {
if (prop.partial_reconcile) { if (selected.partial_reconcile) {
line.balance.amount = 0; line.balance.amount = 0;
line.balance.type = 1; line.balance.type = 1;
line.mode = 'inactive'; line.mode = 'inactive';
@@ -646,6 +666,7 @@ var StatementModel = BasicModel.extend({
return isNaN(prop.id); return isNaN(prop.id);
}), self._formatToProcessReconciliation.bind(self, line)), }), self._formatToProcessReconciliation.bind(self, line)),
}; };
// If the lines are not fully balanced, create an unreconciled amount. // If the lines are not fully balanced, create an unreconciled amount.
// line.st_line.currency_id is never false here because its equivalent to // line.st_line.currency_id is never false here because its equivalent to
// statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side). // statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side).
@@ -1078,8 +1099,9 @@ var StatementModel = BasicModel.extend({
// Do not forward port in master. @CSN will change this // Do not forward port in master. @CSN will change this
var amount = prop.computed_with_tax && -prop.base_amount || -prop.amount; var amount = prop.computed_with_tax && -prop.base_amount || -prop.amount;
if (prop.partial_reconcile === true) { if (prop.partial_reconcile === true) {
amount = -line.st_line.amount; amount = -prop.write_off_amount;
} }
var result = { var result = {
name : prop.label, name : prop.label,
debit : amount > 0 ? amount : 0, debit : amount > 0 ? amount : 0,
@@ -353,7 +353,21 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
// reconciliation_proposition // reconciliation_proposition
var $props = this.$('.accounting_view tbody').empty(); var $props = this.$('.accounting_view tbody').empty();
var props = _.filter(state.reconciliation_proposition, {'display': true});
// loop state propositions
var props = [];
var nb_debit_props = 0;
var nb_credit_props = 0;
_.each(state.reconciliation_proposition, function (prop) {
if (prop.display) {
props.push(prop);
if (prop.amount < 0)
nb_debit_props += 1;
else if (prop.amount > 0)
nb_credit_props += 1;
}
});
_.each(props, function (line) { _.each(props, function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state})); var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) { if (!isNaN(line.id)) {
@@ -361,18 +375,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
.appendTo($line.find('.cell_info_popover')) .appendTo($line.find('.cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); .attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
} }
if (line.already_paid === false &&
if ((state.balance.amount_currency !== 0 || line.partial_reconcile) && props.length === 1 && ((state.balance.amount_currency < 0 || line.partial_reconcile) && nb_credit_props == 1
line.already_paid === false && && line.amount > 0 && state.st_line.amount > 0 && state.st_line.amount < line.amount) ||
( ((state.balance.amount_currency > 0 || line.partial_reconcile) && nb_debit_props == 1
(state.st_line.amount > 0 && state.st_line.amount < props[0].amount) || && line.amount < 0 && state.st_line.amount < 0 && state.st_line.amount > line.amount)) {
(state.st_line.amount < 0 && state.st_line.amount > props[0].amount))
) {
var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left'); var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left');
var text; var text;
if (line.partial_reconcile) { if (line.partial_reconcile) {
text = _t("Undo the partial reconciliation."); text = _t("Undo the partial reconciliation.");
$cell.text(state.st_line.amount_str); $cell.text(line.write_off_amount_str);
} else { } else {
text = _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open."); text = _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open.");
} }
@@ -414,20 +426,24 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
this._renderCreate(state); this._renderCreate(state);
} }
var data = this.model.get(this.handleCreateRecord).data; var data = this.model.get(this.handleCreateRecord).data;
this.model.notifyChanges(this.handleCreateRecord, state.createForm).then(function () {
this.model.notifyChanges(this.handleCreateRecord, state.createForm); // FIXME can't it directly written REPLACE_WITH ids=state.createForm.analytic_tag_ids
this.model.notifyChanges(this.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}); this.model.notifyChanges(this.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}).then(function (){
_.each(state.createForm.analytic_tag_ids, function (tag) { var defs = [];
self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}}); _.each(state.createForm.analytic_tag_ids, function (tag) {
}); defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}}));
});
var record = this.model.get(this.handleCreateRecord); $.when.apply($, defs).then(function () {
_.each(this.fields, function (field, fieldName) { var record = self.model.get(self.handleCreateRecord);
if (self._avoidFieldUpdate[fieldName]) return; _.each(self.fields, function (field, fieldName) {
if (fieldName === "partner_id") return; if (self._avoidFieldUpdate[fieldName]) return;
if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { if (fieldName === "partner_id") return;
field.reset(record); if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
} field.reset(record);
}
});
});
});
}); });
} }
this.$('.create .add_line').toggle(!!state.balance.amount_currency); this.$('.create .add_line').toggle(!!state.balance.amount_currency);
@@ -639,7 +655,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
return; return;
} }
if(event.keyCode === 13) { if(event.keyCode === 13) {
if (_.findWhere(this.model.lines, {mode: 'create'}).balance.amount) { var created_lines = _.findWhere(this.model.lines, {mode: 'create'});
if (created_lines && created_lines.balance.amount) {
this._onCreateProposition(); this._onCreateProposition();
} }
return; return;
@@ -34,17 +34,22 @@ var db = {
fields: { fields: {
id: {string: "ID", type: 'integer'}, id: {string: "ID", type: 'integer'},
code: {string: "code", type: 'integer'}, code: {string: "code", type: 'integer'},
display_name: {string: "Displayed name", type: 'char'}, name: {string: "Displayed name", type: 'char'},
}, },
records: [ records: [
{id: 282, code: 100000, display_name: "100000 Fixed Asset Account"}, {id: 282, code: 100000, name: "100000 Fixed Asset Account"},
{id: 283, code: 101000, display_name: "101000 Current Assets"}, {id: 283, code: 101000, name: "101000 Current Assets"},
{id: 284, code: 101110, display_name: "101110 Stock Valuation Account"}, {id: 284, code: 101110, name: "101110 Stock Valuation Account"},
{id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)"}, {id: 285, code: 101120, name: "101120 Stock Interim Account (Received)"},
{id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)"}, {id: 286, code: 101130, name: "101130 Stock Interim Account (Delivered)"},
{id: 287, code: 101200, display_name: "101200 Account Receivable"}, {id: 287, code: 101200, name: "101200 Account Receivable"},
{id: 288, code: 101300, display_name: "101300 Tax Paid"}, {id: 288, code: 101300, name: "101300 Tax Paid"},
{id: 308, code: 101401, display_name: "101401 Bank"}, {id: 308, code: 101401, name: "101401 Bank"},
{id: 500, code: 500, name: "500 Account"},
{id: 501, code: 501, name: "501 Account"},
{id: 502, code: 502, name: "502 Account"},
{id: 503, code: 503, name: "503 Account"},
{id: 504, code: 504, name: "504 Account"},
], ],
mark_as_reconciled: function () { mark_as_reconciled: function () {
return $.when(); return $.when();
@@ -776,13 +781,7 @@ QUnit.module('account', {
partner_id: false, partner_id: false,
counterpart_aml_dicts:[], counterpart_aml_dicts:[],
payment_aml_ids: [392], payment_aml_ids: [392],
new_aml_dicts: [ new_aml_dicts: [],
{
"credit": 343.42,
"debit": 0,
"name": "Bank fees : Open balance"
}
],
}] }]
], "should call process_reconciliations with partial reconcile values"); ], "should call process_reconciliations with partial reconcile values");
} }
@@ -811,14 +810,14 @@ QUnit.module('account', {
assert.notOk( widget.$('.cell_left .line_info_button').length, "should not display the partial reconciliation alert"); assert.notOk( widget.$('.cell_left .line_info_button').length, "should not display the partial reconciliation alert");
widget.$('.accounting_view thead td:first').trigger('click'); widget.$('.accounting_view thead td:first').trigger('click');
widget.$('.match .cell_account_code:first').trigger('click'); widget.$('.match .cell_account_code:first').trigger('click');
assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 0, "should not display the partial reconciliation alert"); assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 1, "should display the partial reconciliation alert");
assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should not display the reconcile button"); assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should not display the reconcile button");
assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert"); assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert");
widget.$('.accounting_view .cell_left .line_info_button').trigger('click'); widget.$('.accounting_view .cell_left .line_info_button').trigger('click');
assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 0, "should not display a partial reconciliation alert"); assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 1, "should display a partial reconciliation alert");
assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should not display the partial reconciliation information"); assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_true'), "should display the partial reconciliation information");
assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button"); assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button");
assert.strictEqual( widget.$el.data('mode'), "match", "should be inactive mode"); assert.strictEqual( widget.$el.data('mode'), "inactive", "should be inactive mode");
widget.$('button.btn-default:not(hidden)').trigger('click'); widget.$('button.btn-default:not(hidden)').trigger('click');
clientAction.destroy(); clientAction.destroy();
@@ -1036,13 +1035,77 @@ QUnit.module('account', {
clientAction.destroy(); clientAction.destroy();
}); });
QUnit.test('Reconciliation create line (many2one test)', function (assert) {
assert.expect(5);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
var def = $.Deferred();
testUtils.addMockEnvironment(clientAction, {
data: this.params.data,
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
archs: {
"account.account,false,list": '<tree string="Account"><field name="code"/><field name="name"/></tree>',
"account.account,false,search": '<search string="Account"><field name="code"/></search>',
},
mockRPC: function (route, args) {
if (args.method === 'name_get') {
return def.then(this._super.bind(this, route, args));
}
return this._super(route, args);
},
});
clientAction.prependTo($('#qunit-fixture'));
var widget = clientAction.widgets[0];
// open the first line in write-off mode
widget.$('.accounting_view tfoot td:first').trigger('click');
// select an account with the many2one (drop down)
widget.$('.create .create_account_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(101200)').trigger('mouseenter').trigger('click');
assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Display the selected account");
assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "101200", "Display the code of the selected account");
// use the many2one select dialog to change the account
widget.$('.create .create_account_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(Search)').trigger('mouseenter').trigger('click');
// select the account who does not appear in the drop drown
$('.modal tr.o_data_row:contains(502)').click();
assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Selected account does not change");
// wait the name_get to render the changes
def.resolve();
assert.strictEqual(widget.$('.create .create_account_id input').val(), "502 Account", "Display the selected account");
assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "502", "Display the code of the selected account");
clientAction.destroy();
});
QUnit.test('Reconciliation create line with taxes', function (assert) { QUnit.test('Reconciliation create line with taxes', function (assert) {
assert.expect(13); assert.expect(13);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, { testUtils.addMockEnvironment(clientAction, {
'data': this.params.data, data: this.params.data,
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
}); });
clientAction.appendTo($('#qunit-fixture')); clientAction.appendTo($('#qunit-fixture'));
@@ -1054,26 +1117,26 @@ QUnit.module('account', {
widget.$('.create .create_label input').val('test1').trigger('input'); widget.$('.create .create_label input').val('test1').trigger('input');
widget.$('.create .create_amount input').val('1100').trigger('input'); widget.$('.create .create_amount input').val('1100').trigger('input');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "1100.00", "should display the value 1100.00 in left column"); assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$\u00a01100.00", "should display the value 1100.00 in left column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines");
widget.$('.create .create_tax_id input').trigger('click'); widget.$('.create .create_tax_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1000.00100.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01000.00$\u00a0100.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
widget.$('.create .create_tax_id input').trigger('click'); widget.$('.create .create_tax_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1100.00220.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "145.00", "should display 'Create Write-off' with 145.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
clientAction.destroy(); clientAction.destroy();
@@ -1085,7 +1148,7 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, { testUtils.addMockEnvironment(clientAction, {
'data': this.params.data, data: this.params.data,
}); });
clientAction.appendTo($('#qunit-fixture')); clientAction.appendTo($('#qunit-fixture'));
+50
View File
@@ -361,3 +361,53 @@ class TestPayment(AccountingTestCase):
{'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False}, {'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
{'account_id': inv_3.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0.0, 'currency_id': False}, {'account_id': inv_3.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0.0, 'currency_id': False},
]) ])
def test_payment_and_writeoff_in_other_currency(self):
# Use case:
# Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD.
# Mark invoice as fully paid with a write_off
# Check that all the aml are correctly created.
invoice = self.create_invoice(amount=25, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
# register payment on invoice
payment = self.payment_model.create({'payment_type': 'inbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'customer',
'partner_id': self.partner_agrolait.id,
'amount': 25,
'currency_id': self.currency_usd_id,
'payment_date': time.strftime('%Y') + '-07-15',
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.account_payable.id,
'journal_id': self.bank_journal_euro.id,
'invoice_ids': [(4, invoice.id, None)]
})
payment.post()
self.check_journal_items(payment.move_line_ids, [
{'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id},
{'account_id': self.account_payable.id, 'debit': 8.65, 'credit': 0.0, 'amount_currency': 13.22, 'currency_id': self.currency_usd_id},
{'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 25.0, 'amount_currency': -38.22, 'currency_id': self.currency_usd_id},
])
# Use case:
# Company is in EUR, create a vendor bill for 25 EUR and register payment of 25 USD.
# Mark invoice as fully paid with a write_off
# Check that all the aml are correctly created.
invoice = self.create_invoice(amount=25, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
# register payment on invoice
payment = self.payment_model.create({'payment_type': 'inbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'supplier',
'partner_id': self.partner_agrolait.id,
'amount': 25,
'currency_id': self.currency_usd_id,
'payment_date': time.strftime('%Y') + '-07-15',
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.account_payable.id,
'journal_id': self.bank_journal_euro.id,
'invoice_ids': [(4, invoice.id, None)]
})
payment.post()
self.check_journal_items(payment.move_line_ids, [
{'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id},
{'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id},
{'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id},
])
+10 -20
View File
@@ -537,10 +537,8 @@
<field name="number" string="Invoice" filter_domain="['|','|','|', ('number','ilike',self), ('origin','ilike',self), ('reference', 'ilike', self), ('partner_id', 'child_of', self)]"/> <field name="number" string="Invoice" filter_domain="['|','|','|', ('number','ilike',self), ('origin','ilike',self), ('reference', 'ilike', self), ('partner_id', 'child_of', self)]"/>
<field name="journal_id" /> <field name="journal_id" />
<filter name="draft" string="Draft" domain="[('state','=','draft')]"/> <filter name="draft" string="Draft" domain="[('state','=','draft')]"/>
<filter name="invoices" string="Invoices" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_invoice','in_invoice'))]"/> <filter name="unpaid" string="Open" domain="[('state', '=', 'open')]"/>
<filter name="refunds" string="Credit Notes" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_refund','in_refund'))]"/> <filter name="paid" string="Paid" domain="[('state', '=', 'paid')]"/>
<separator/>
<filter name="unpaid" string="Not Paid" domain="[('state','=','open')]"/>
<filter name="late" string="Overdue" domain="['&amp;', ('date_due', '&lt;', time.strftime('%%Y-%%m-%%d')), ('state', '=', 'open')]" help="Overdue invoices, maturity date passed"/> <filter name="late" string="Overdue" domain="['&amp;', ('date_due', '&lt;', time.strftime('%%Y-%%m-%%d')), ('state', '=', 'open')]" help="Overdue invoices, maturity date passed"/>
<separator/> <separator/>
<field name="partner_id" operator="child_of"/> <field name="partner_id" operator="child_of"/>
@@ -574,33 +572,25 @@
</field> </field>
</record> </record>
<!-- TODO: remove in master -->
<record id="view_account_invoice_filter_inherit_invoices" model="ir.ui.view"> <record id="view_account_invoice_filter_inherit_invoices" model="ir.ui.view">
<field name="name">account.invoice.select.invoices</field> <field name="name">account.invoice.select.invoices</field>
<field name="model">account.invoice</field> <field name="model">account.invoice</field>
<field name="mode">primary</field> <field name="mode">primary</field>
<field name="inherit_id" ref="view_account_invoice_filter"/> <field name="inherit_id" ref="view_account_invoice_filter"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<data> <data/>
<xpath expr="filter[@name='invoices']" position="attributes">
<attribute name="string">Not Draft</attribute>
</xpath>
<filter name="refunds" position="replace"/>
</data>
</field> </field>
</record> </record>
<!-- TODO: remove in master -->
<record id="view_account_invoice_filter_inherit_credit_notes" model="ir.ui.view"> <record id="view_account_invoice_filter_inherit_credit_notes" model="ir.ui.view">
<field name="name">account.invoice.select.credit.notes</field> <field name="name">account.invoice.select.credit.notes</field>
<field name="model">account.invoice</field> <field name="model">account.invoice</field>
<field name="mode">primary</field> <field name="mode">primary</field>
<field name="inherit_id" ref="view_account_invoice_filter"/> <field name="inherit_id" ref="view_account_invoice_filter"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<data> <data/>
<xpath expr="filter[@name='refunds']" position="attributes">
<attribute name="string">Not Draft</attribute>
</xpath>
<filter name="invoices" position="replace"/>
</data>
</field> </field>
</record> </record>
@@ -671,7 +661,7 @@
<field eval="False" name="view_id"/> <field eval="False" name="view_id"/>
<field name="domain">[('type','=','out_invoice')]</field> <field name="domain">[('type','=','out_invoice')]</field>
<field name="context">{'type':'out_invoice', 'journal_type': 'sale'}</field> <field name="context">{'type':'out_invoice', 'journal_type': 'sale'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_invoices"/> <field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Create a customer invoice Create a customer invoice
@@ -706,7 +696,7 @@
<field eval="False" name="view_id"/> <field eval="False" name="view_id"/>
<field name="domain">[('type','=','out_refund')]</field> <field name="domain">[('type','=','out_refund')]</field>
<field name="context">{'default_type': 'out_refund', 'type': 'out_refund', 'journal_type': 'sale'}</field> <field name="context">{'default_type': 'out_refund', 'type': 'out_refund', 'journal_type': 'sale'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_credit_notes"/> <field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Create a credit note Create a credit note
@@ -744,7 +734,7 @@
<field eval="False" name="view_id"/> <field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_invoice')]</field> <field name="domain">[('type','=','in_invoice')]</field>
<field name="context">{'default_type': 'in_invoice', 'type': 'in_invoice', 'journal_type': 'purchase'}</field> <field name="context">{'default_type': 'in_invoice', 'type': 'in_invoice', 'journal_type': 'purchase'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_invoices"/> <field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Record a new vendor bill Record a new vendor bill
@@ -780,7 +770,7 @@
<field eval="False" name="view_id"/> <field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_refund')]</field> <field name="domain">[('type','=','in_refund')]</field>
<field name="context">{'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'}</field> <field name="context">{'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_credit_notes"/> <field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Create a vendor credit note Create a vendor credit note
@@ -117,16 +117,12 @@
<span>View</span> <span>View</span>
</div> </div>
<div> <div>
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_invoice','in_invoice'))]}"> <a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_invoice')]}">Invoices</a>
<span t-if="journal_type == 'sale'">Invoices</span> <a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_invoice')]}">Bills</a>
<span t-if="journal_type == 'purchase'">Bills</span>
</a>
</div> </div>
<div> <div>
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_refund','in_refund'))], 'invoice_type': 'refund'}"> <a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
<span t-if="journal_type == 'sale'">Credit Notes</span> <a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
<span t-if="journal_type == 'purchase'">Credit Notes</span>
</a>
</div> </div>
<div> <div>
<a type="object" name="action_open_reconcile">Payments Matching</a> <a type="object" name="action_open_reconcile">Payments Matching</a>
+1
View File
@@ -56,6 +56,7 @@
id="action_report_financial" id="action_report_financial"
model="account.financial.report" model="account.financial.report"
string="Financial report" string="Financial report"
menu="False"
report_type="qweb-pdf" report_type="qweb-pdf"
name="account.report_financial" name="account.report_financial"
file="account.report_financial" file="account.report_financial"
+1 -1
View File
@@ -16,7 +16,7 @@
<field name="code" placeholder="code"/> <field name="code" placeholder="code"/>
<field name="name"/> <field name="name"/>
<field name="user_type_id" widget="selection"/> <field name="user_type_id" widget="selection"/>
<field name="tax_ids" widget="many2many_tags"/> <field name="tax_ids" widget="many2many_tags" domain="[('company_id','=',company_id)]"/>
<field name="tag_ids" widget="many2many_tags" domain="[('applicability', '!=', 'taxes')]" context="{'default_applicability': 'accounts'}" options="{'no_create_edit': True}"/> <field name="tag_ids" widget="many2many_tags" domain="[('applicability', '!=', 'taxes')]" context="{'default_applicability': 'accounts'}" options="{'no_create_edit': True}"/>
<field name="group_id"/> <field name="group_id"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
@@ -66,7 +66,7 @@ class AccountInvoiceLine(models.Model):
@api.onchange('product_id') @api.onchange('product_id')
def _onchange_product_id(self): def _onchange_product_id(self):
res = super(AccountInvoiceLine, self)._onchange_product_id() res = super(AccountInvoiceLine, self)._onchange_product_id()
rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.partner_id.id, self.env.uid, rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid,
fields.Date.today(), company_id=self.company_id.id) fields.Date.today(), company_id=self.company_id.id)
self.account_analytic_id = rec.analytic_id.id self.account_analytic_id = rec.analytic_id.id
self.analytic_tag_ids = rec.analytic_tag_ids.ids self.analytic_tag_ids = rec.analytic_tag_ids.ids
@@ -75,7 +75,7 @@ class AccountInvoiceLine(models.Model):
def _set_additional_fields(self, invoice): def _set_additional_fields(self, invoice):
if not self.account_analytic_id or not self.analytic_tag_ids: if not self.account_analytic_id or not self.analytic_tag_ids:
rec = self.env['account.analytic.default'].account_get( rec = self.env['account.analytic.default'].account_get(
self.product_id.id, self.invoice_id.partner_id.id, self.env.uid, self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid,
fields.Date.today(), company_id=self.company_id.id) fields.Date.today(), company_id=self.company_id.id)
if rec: if rec:
if self.account_analytic_id: if self.account_analytic_id:
+9
View File
@@ -11,3 +11,12 @@ def uninstall_hook(cr, registry):
('module', '=', 'account') ('module', '=', 'account')
]).mapped('res_id') ]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': False}) env['ir.ui.menu'].browse(res_ids).update({'active': False})
def post_init_hook(cr, registry):
env = Environment(cr, SUPERUSER_ID, {})
res_ids = env['ir.model.data'].search([
('model', '=', 'ir.ui.menu'),
('module', '=', 'account'),
]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': True})
+1
View File
@@ -28,4 +28,5 @@ You could use this simplified accounting in case you work with an (external) acc
], ],
'application': True, 'application': True,
'uninstall_hook': 'uninstall_hook', 'uninstall_hook': 'uninstall_hook',
'post_init_hook': 'post_init_hook',
} }
+1
View File
@@ -4,3 +4,4 @@ from . import res_config_settings
from . import ir_http from . import ir_http
from . import res_partner from . import res_partner
from . import res_users from . import res_users
from . import ir_model_fields
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from odoo import models
# This is a nasty hack, targeted for V11 only
class IrModelFields(models.Model):
_inherit = 'ir.model.fields'
def unlink(self):
# Prevent the deletion of some `shared` fields... -_-
self = self.filtered(
lambda rec: not (
rec.model == 'res.config.settings'
and rec.name == 'auth_signup_uninvited'
)
)
return super(IrModelFields, self).unlink()
@@ -63,7 +63,7 @@ class BaseGengoTranslations(models.TransientModel):
'view_type': 'form', 'view_type': 'form',
'view_mode': 'form', 'view_mode': 'form',
'res_model': 'res.config.settings', 'res_model': 'res.config.settings',
'target': 'current', 'target': 'inline',
'context': {'module' : 'general_settings'}, 'context': {'module' : 'general_settings'},
} }
@@ -89,6 +89,7 @@ _map_iban_template = {
'bg': 'BGkk BBBB SSSS DDCC CCCC CC', # Bulgaria 'bg': 'BGkk BBBB SSSS DDCC CCCC CC', # Bulgaria
'bh': 'BHkk BBBB CCCC CCCC CCCC CC', # Bahrain 'bh': 'BHkk BBBB CCCC CCCC CCCC CC', # Bahrain
'br': 'BRkk BBBB BBBB SSSS SCCC CCCC CCCT N', # Brazil 'br': 'BRkk BBBB BBBB SSSS SCCC CCCC CCCT N', # Brazil
'by': 'BYkk BBBB AAAA CCCC CCCC CCCC CCCC', # Belarus
'ch': 'CHkk BBBB BCCC CCCC CCCC C', # Switzerland 'ch': 'CHkk BBBB BCCC CCCC CCCC C', # Switzerland
'cr': 'CRkk BBBC CCCC CCCC CCCC C', # Costa Rica 'cr': 'CRkk BBBC CCCC CCCC CCCC C', # Costa Rica
'cy': 'CYkk BBBS SSSS CCCC CCCC CCCC CCCC', # Cyprus 'cy': 'CYkk BBBS SSSS CCCC CCCC CCCC CCCC', # Cyprus
+3 -1
View File
@@ -302,7 +302,8 @@ class Import(models.TransientModel):
try: try:
thousand_separator = decimal_separator = False thousand_separator = decimal_separator = False
for val in preview_values: for val in preview_values:
if val == '': val = val.strip()
if not val:
continue continue
# value might have the currency symbol left or right from the value # value might have the currency symbol left or right from the value
val = self._remove_currency_symbol(val) val = self._remove_currency_symbol(val)
@@ -587,6 +588,7 @@ class Import(models.TransientModel):
thousand_separator = options.get('float_thousand_separator', ' ') thousand_separator = options.get('float_thousand_separator', ' ')
decimal_separator = options.get('float_decimal_separator', '.') decimal_separator = options.get('float_decimal_separator', '.')
for line in data: for line in data:
line[index] = line[index].strip()
if not line[index]: if not line[index]:
continue continue
line[index] = line[index].replace(thousand_separator, '').replace(decimal_separator, '.') line[index] = line[index].replace(thousand_separator, '').replace(decimal_separator, '.')
@@ -145,12 +145,13 @@ def _is_studio_custom(path):
Returns True if any of the records contains a context with the key Returns True if any of the records contains a context with the key
studio in it, False if none of the records do studio in it, False if none of the records do
""" """
path = os.path.join(path, 'data') filepaths = []
filenames = next(iter(os.walk(path)))[2] for level in os.walk(path):
filenames = [f for f in filenames if f.lower().endswith('.xml')] filepaths += [os.path.join(level[0], fn) for fn in level[2]]
filepaths = [fp for fp in filepaths if fp.lower().endswith('.xml')]
for filename in filenames: for fp in filepaths:
root = lxml.etree.parse(os.path.join(path, filename)).getroot() root = lxml.etree.parse(fp).getroot()
for record in root: for record in root:
# there might not be a context if it's a non-studio module # there might not be a context if it's a non-studio module
+2 -2
View File
@@ -10,7 +10,7 @@ class ResConfigSettings(models.TransientModel):
crm_alias_prefix = fields.Char('Default Alias Name for Leads') crm_alias_prefix = fields.Char('Default Alias Name for Leads')
generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails', config_parameter='crm.generate_lead_from_alias') generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails', config_parameter='crm.generate_lead_from_alias')
group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead') group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead')
module_crm_phone_validation = fields.Boolean("Phone Validation") module_crm_phone_validation = fields.Boolean("Phone Formatting")
module_web_clearbit = fields.Boolean("Customer Autocomplete") module_web_clearbit = fields.Boolean("Customer Autocomplete")
def _find_default_lead_alias_id(self): def _find_default_lead_alias_id(self):
@@ -33,7 +33,7 @@ class ResConfigSettings(models.TransientModel):
@api.onchange('generate_lead_from_alias') @api.onchange('generate_lead_from_alias')
def _onchange_generate_lead_from_alias(self): def _onchange_generate_lead_from_alias(self):
self.crm_alias_prefix = 'info' if self.generate_lead_from_alias else False self.crm_alias_prefix = (self.crm_alias_prefix or 'info') if self.generate_lead_from_alias else False
@api.model @api.model
def get_values(self): def get_values(self):
@@ -62,7 +62,7 @@
<div class="o_setting_right_pane"> <div class="o_setting_right_pane">
<label for="module_crm_phone_validation"/> <label for="module_crm_phone_validation"/>
<div class="text-muted" id="module_crm_phone_validation"> <div class="text-muted" id="module_crm_phone_validation">
Formats phone numbers Format phone numbers based on national conventions
</div> </div>
</div> </div>
</div> </div>
+4 -1
View File
@@ -210,8 +210,11 @@ class DeliveryCarrier(models.Model):
carrier.product_id.list_price = carrier.fixed_price carrier.product_id.list_price = carrier.fixed_price
def fixed_rate_shipment(self, order): def fixed_rate_shipment(self, order):
price = self.fixed_price
if self.company_id.currency_id.id != order.currency_id.id:
price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price)
return {'success': True, return {'success': True,
'price': self.fixed_price, 'price': price,
'error_message': False, 'error_message': False,
'warning_message': False} 'warning_message': False}
+10 -12
View File
@@ -84,18 +84,6 @@
</tr> </tr>
<tr> <tr>
<td style="padding:15px 20px 0px 20px;"> <td style="padding:15px 20px 0px 20px;">
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
<td style="padding:25px 0px; text-align:center;">
<p>
Don't forget to <strong>add it to your calendar</strong>:
</p>
<a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
<a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
<a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
</td>
</tr>
</table>
<table style="width:100%;border-top:1px solid #e1e1e1;"> <table style="width:100%;border-top:1px solid #e1e1e1;">
<tr> <tr>
<td style="padding:25px 0px;vertical-align:top;"> <td style="padding:25px 0px;vertical-align:top;">
@@ -165,6 +153,16 @@
</tr> </tr>
</table> </table>
% endif % endif
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
<td style="padding:25px 0px;">
<strong>Add this event to your calendar</strong>
<a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
<a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
<a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
</td>
</tr>
</table>
</td> </td>
</tr> </tr>
</tbody> </tbody>
+3 -1
View File
@@ -247,9 +247,11 @@ class FleetVehicle(models.Model):
@return: the costs log view @return: the costs log view
""" """
self.ensure_one() self.ensure_one()
copy_context = dict(self.env.context)
copy_context.pop('group_by', None)
res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action') res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
res.update( res.update(
context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True), context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True),
domain=[('vehicle_id', '=', self.id)] domain=[('vehicle_id', '=', self.id)]
) )
return res return res
+2 -1
View File
@@ -190,7 +190,8 @@ class HrPayslip(models.Model):
leave_time = (interval[1] - interval[0]).seconds / 3600 leave_time = (interval[1] - interval[0]).seconds / 3600
current_leave_struct['number_of_hours'] += leave_time current_leave_struct['number_of_hours'] += leave_time
work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id) work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id)
current_leave_struct['number_of_days'] += leave_time / work_hours if work_hours:
current_leave_struct['number_of_days'] += leave_time / work_hours
# compute worked days # compute worked days
work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id) work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id)
@@ -95,7 +95,7 @@ class HrContract(models.Model):
+ 12.0 * contract.internet \ + 12.0 * contract.internet \
+ 12.0 * (contract.mobile + contract.mobile_plus) \ + 12.0 * (contract.mobile + contract.mobile_plus) \
+ 12.0 * contract.transport_employer_cost \ + 12.0 * contract.transport_employer_cost \
+ (1.326 / 1.05 * 12.0) * contract.commission_on_target \ + contract.warrants_cost \
+ 220.0 * contract.meal_voucher_paid_by_employer + 220.0 * contract.meal_voucher_paid_by_employer
contract.final_yearly_costs = yearly_cost / (1.0 - (contract.holidays - 20.0) / 231.0) contract.final_yearly_costs = yearly_cost / (1.0 - (contract.holidays - 20.0) / 231.0)
contract.wage = contract._get_gross_from_employer_costs(contract.final_yearly_costs) contract.wage = contract._get_gross_from_employer_costs(contract.final_yearly_costs)
@@ -116,8 +116,8 @@ class HrContract(models.Model):
@api.depends('commission_on_target') @api.depends('commission_on_target')
def _compute_warrants_cost(self): def _compute_warrants_cost(self):
for contract in self: for contract in self:
contract.warrants_cost = contract.commission_on_target * 1.05 * 12.0 contract.warrants_cost = contract.commission_on_target * 1.326 * 1.05 * 12.0
contract.warrant_value_employee = contract.commission_on_target * (1.00 - 0.535) * 12.0 contract.warrant_value_employee = contract.commission_on_target * 1.326 * (1.00 - 0.535) * 12.0
@api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost', @api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost',
'internet', 'mobile', 'mobile_plus') 'internet', 'mobile', 'mobile_plus')
@@ -2836,98 +2836,98 @@
<record id="pcg_501" model="account.account.template"> <record id="pcg_501" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Parts dans entreprises liées</field> <field name="name">Valeurs mobilières de placement - Parts dans entreprises liées</field>
<field name="code">501</field> <field name="code">501</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_502" model="account.account.template"> <record id="pcg_502" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Actions propres</field> <field name="name">Valeurs mobilières de placement - Actions propres</field>
<field name="code">502</field> <field name="code">502</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5031" model="account.account.template"> <record id="pcg_5031" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Titres cotés</field> <field name="name">Valeurs mobilières de placement - Titres cotés</field>
<field name="code">5031</field> <field name="code">5031</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5035" model="account.account.template"> <record id="pcg_5035" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Titres non cotés</field> <field name="name">Valeurs mobilières de placement - Titres non cotés</field>
<field name="code">5035</field> <field name="code">5035</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_504" model="account.account.template"> <record id="pcg_504" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Autres titres conférant un droit de propriété</field> <field name="name">Valeurs mobilières de placement - Autres titres conférant un droit de propriété</field>
<field name="code">504</field> <field name="code">504</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_505" model="account.account.template"> <record id="pcg_505" model="account.account.template">
<field name="name">Obligations et bons émis par la société et rachetés par elle</field> <field name="name">Obligations et bons émis par la société et rachetés par elle</field>
<field name="code">505</field> <field name="code">505</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5061" model="account.account.template"> <record id="pcg_5061" model="account.account.template">
<field name="name">Obligations cotés</field> <field name="name">Obligations cotés</field>
<field name="code">5061</field> <field name="code">5061</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5065" model="account.account.template"> <record id="pcg_5065" model="account.account.template">
<field name="name">Obligations non cotés</field> <field name="name">Obligations non cotés</field>
<field name="code">5065</field> <field name="code">5065</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_507" model="account.account.template"> <record id="pcg_507" model="account.account.template">
<field name="name">Bons du Trésor et bons de caisse à court terme</field> <field name="name">Bons du Trésor et bons de caisse à court terme</field>
<field name="code">507</field> <field name="code">507</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5081" model="account.account.template"> <record id="pcg_5081" model="account.account.template">
<field name="name">Autres valeurs mobilières de placement</field> <field name="name">Autres valeurs mobilières de placement</field>
<field name="code">5081</field> <field name="code">5081</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5082" model="account.account.template"> <record id="pcg_5082" model="account.account.template">
<field name="name">Bons de souscription</field> <field name="name">Bons de souscription</field>
<field name="code">5082</field> <field name="code">5082</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5088" model="account.account.template"> <record id="pcg_5088" model="account.account.template">
<field name="name">Intérêts courus sur obligations, bons et valeurs assimilées</field> <field name="name">Intérêts courus sur obligations, bons et valeurs assimilées</field>
<field name="code">5088</field> <field name="code">5088</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_509" model="account.account.template"> <record id="pcg_509" model="account.account.template">
<field name="name">Versements restant à effectuer sur valeurs mobilières de placement non libérées</field> <field name="name">Versements restant à effectuer sur valeurs mobilières de placement non libérées</field>
<field name="code">509</field> <field name="code">509</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5111" model="account.account.template"> <record id="pcg_5111" model="account.account.template">
<field name="name">Coupons échus à l'encaissement</field> <field name="name">Coupons échus à l'encaissement</field>
<field name="code">5111</field> <field name="code">5111</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/> <field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
@@ -2935,7 +2935,7 @@
<record id="pcg_5112" model="account.account.template"> <record id="pcg_5112" model="account.account.template">
<field name="name">Chèques à encaisser</field> <field name="name">Chèques à encaisser</field>
<field name="code">5112</field> <field name="code">5112</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/> <field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
@@ -2943,7 +2943,7 @@
<record id="pcg_5113" model="account.account.template"> <record id="pcg_5113" model="account.account.template">
<field name="name">Effets à l'encaissement</field> <field name="name">Effets à l'encaissement</field>
<field name="code">5113</field> <field name="code">5113</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/> <field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
@@ -2951,7 +2951,7 @@
<record id="pcg_5114" model="account.account.template"> <record id="pcg_5114" model="account.account.template">
<field name="name">Effets à l'escompte</field> <field name="name">Effets à l'escompte</field>
<field name="code">5114</field> <field name="code">5114</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/> <field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
@@ -2980,56 +2980,56 @@
<record id="pcg_516" model="account.account.template"> <record id="pcg_516" model="account.account.template">
<field name="name">Sociétés de bourse</field> <field name="name">Sociétés de bourse</field>
<field name="code">516</field> <field name="code">516</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_517" model="account.account.template"> <record id="pcg_517" model="account.account.template">
<field name="name">Autres organismes financiers</field> <field name="name">Autres organismes financiers</field>
<field name="code">517</field> <field name="code">517</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5181" model="account.account.template"> <record id="pcg_5181" model="account.account.template">
<field name="name">Intérêts courus à payer</field> <field name="name">Intérêts courus à payer</field>
<field name="code">5181</field> <field name="code">5181</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5188" model="account.account.template"> <record id="pcg_5188" model="account.account.template">
<field name="name">Intérêts courus à recevoir</field> <field name="name">Intérêts courus à recevoir</field>
<field name="code">5188</field> <field name="code">5188</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5191" model="account.account.template"> <record id="pcg_5191" model="account.account.template">
<field name="name">Concours bancaires courants - Crédit de mobilisation de créances commerciales (CMCC)</field> <field name="name">Concours bancaires courants - Crédit de mobilisation de créances commerciales (CMCC)</field>
<field name="code">5191</field> <field name="code">5191</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5193" model="account.account.template"> <record id="pcg_5193" model="account.account.template">
<field name="name">Concours bancaires courants - Mobilisation de créances nées à l'étranger</field> <field name="name">Concours bancaires courants - Mobilisation de créances nées à l'étranger</field>
<field name="code">5193</field> <field name="code">5193</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5198" model="account.account.template"> <record id="pcg_5198" model="account.account.template">
<field name="name">Concours bancaires courants - Intérêts courus sur concours bancaires courants</field> <field name="name">Concours bancaires courants - Intérêts courus sur concours bancaires courants</field>
<field name="code">5198</field> <field name="code">5198</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_52" model="account.account.template"> <record id="pcg_52" model="account.account.template">
<field name="name">Instruments de trésorerie</field> <field name="name">Instruments de trésorerie</field>
<field name="code">52</field> <field name="code">52</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
@@ -3064,35 +3064,35 @@
<record id="pcg_54" model="account.account.template"> <record id="pcg_54" model="account.account.template">
<field name="name">Régies d'avances et accréditifs</field> <field name="name">Régies d'avances et accréditifs</field>
<field name="code">54</field> <field name="code">54</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5903" model="account.account.template"> <record id="pcg_5903" model="account.account.template">
<field name="name">Provisions pour dépréciation des actions</field> <field name="name">Provisions pour dépréciation des actions</field>
<field name="code">5903</field> <field name="code">5903</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5904" model="account.account.template"> <record id="pcg_5904" model="account.account.template">
<field name="name">Provisions pour dépréciation des autres titres conférant un droit de propriété</field> <field name="name">Provisions pour dépréciation des autres titres conférant un droit de propriété</field>
<field name="code">5904</field> <field name="code">5904</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5906" model="account.account.template"> <record id="pcg_5906" model="account.account.template">
<field name="name">Provisions pour dépréciation des obligations</field> <field name="name">Provisions pour dépréciation des obligations</field>
<field name="code">5906</field> <field name="code">5906</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
<record id="pcg_5908" model="account.account.template"> <record id="pcg_5908" model="account.account.template">
<field name="name">Provisions pour dépréciation des autres valeurs mobilières de placement et créances assimilées (provisions)</field> <field name="name">Provisions pour dépréciation des autres valeurs mobilières de placement et créances assimilées (provisions)</field>
<field name="code">5908</field> <field name="code">5908</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record> </record>
+4
View File
@@ -18,6 +18,10 @@
<field name="name">DIOT: 16%</field> <field name="name">DIOT: 16%</field>
<field name="applicability">taxes</field> <field name="applicability">taxes</field>
</record> </record>
<record id="tag_diot_16_imp" model="account.account.tag">
<field name="name">DIOT: 16% IMP</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_diot_0" model="account.account.tag"> <record id="tag_diot_0" model="account.account.tag">
<field name="name">DIOT: 0%</field> <field name="name">DIOT: 0%</field>
<field name="applicability">taxes</field> <field name="applicability">taxes</field>
+12 -5
View File
@@ -5,18 +5,25 @@
<!-- Smileys --> <!-- Smileys -->
<record id="mail_smiley_normal_smile" model="mail.shortcode"> <record id="mail_smiley_normal_smile" model="mail.shortcode">
<field name="source">:)</field> <field name="source">:)</field>
<field name="unicode_source">😊</field> <field name="unicode_source">🙂</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/&gt;</field> <field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png' alt='🙂'/&gt;</field>
<field name="description">Smile</field> <field name="description">Smile</field>
<field name="shortcode_type">image</field> <field name="shortcode_type">image</field>
</record> </record>
<record id="mail_smiley_smile" model="mail.shortcode"> <record id="mail_smiley_smile" model="mail.shortcode">
<field name="source">:D</field> <field name="source">:D</field>
<field name="unicode_source">😄</field> <field name="unicode_source">😀</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png' alt='😄'/&gt;</field> <field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile_open_mouth.png' alt='😀'/&gt;</field>
<field name="description">Smile with open mouth</field> <field name="description">Smile with open mouth</field>
<field name="shortcode_type">image</field> <field name="shortcode_type">image</field>
</record> </record>
<record id="mail_smiley_smile_eyes" model="mail.shortcode">
<field name="source">^^</field>
<field name="unicode_source">😊</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/&gt;</field>
<field name="description">Smile with with smiling eyes</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_laugh" model="mail.shortcode"> <record id="mail_smiley_laugh" model="mail.shortcode">
<field name="source">xD</field> <field name="source">xD</field>
<field name="unicode_source">😆</field> <field name="unicode_source">😆</field>
@@ -25,7 +32,7 @@
<field name="shortcode_type">image</field> <field name="shortcode_type">image</field>
</record> </record>
<record id="mail_smiley_tears_of_joy" model="mail.shortcode"> <record id="mail_smiley_tears_of_joy" model="mail.shortcode">
<field name="source">:-)</field> <field name="source">:joy</field>
<field name="unicode_source">😂</field> <field name="unicode_source">😂</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/&gt;</field> <field name="substitution">&lt;img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/&gt;</field>
<field name="description">Tears of joy</field> <field name="description">Tears of joy</field>
+1 -1
View File
@@ -56,7 +56,7 @@ class Channel(models.Model):
('channel', 'Channel')], ('channel', 'Channel')],
'Channel Type', default='channel') 'Channel Type', default='channel')
description = fields.Text('Description') description = fields.Text('Description')
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4()) uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4(), copy=False)
email_send = fields.Boolean('Send messages by email', default=False) email_send = fields.Boolean('Send messages by email', default=False)
# multi users channel # multi users channel
channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen') channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen')
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+1 -1
View File
@@ -591,7 +591,7 @@ var BasicComposer = Widget.extend({
*/ */
_onKeydown: function (event) { _onKeydown: function (event) {
switch(event.which) { switch(event.which) {
// UP, DOWN: prevent moving cursor if navigation in mention propositions // UP, DOWN, TAB: prevent moving cursor if navigation in mention propositions
case $.ui.keyCode.UP: case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN: case $.ui.keyCode.DOWN:
case $.ui.keyCode.TAB: case $.ui.keyCode.TAB:
+34 -1
View File
@@ -17,6 +17,7 @@ var DocumentViewer = Widget.extend({
'click .o_viewer_video': '_onVideoClicked', 'click .o_viewer_video': '_onVideoClicked',
'click .move_next': '_onNext', 'click .move_next': '_onNext',
'click .move_previous': '_onPrevious', 'click .move_previous': '_onPrevious',
'click .o_rotate': '_onRotate',
'click .o_zoom_in': '_onZoomIn', 'click .o_zoom_in': '_onZoomIn',
'click .o_zoom_out': '_onZoomOut', 'click .o_zoom_out': '_onZoomOut',
'click .o_close_btn, .o_viewer_img_wrapper': '_onClose', 'click .o_close_btn, .o_viewer_img_wrapper': '_onClose',
@@ -103,6 +104,30 @@ var DocumentViewer = Widget.extend({
this.$('.o_viewer_img').load(_.bind(this._onImageLoaded, this)); this.$('.o_viewer_img').load(_.bind(this._onImageLoaded, this));
this._reset(); this._reset();
}, },
/**
* Get CSS transform property based on scale and angle
*
* @private
* @param {float} scale
* @param {float} angle
*/
_getTransform: function(scale, angle) {
return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)'
},
/**
* Rotate image clockwise by provided angle
*
* @private
* @param {float} angle
*/
_rotate: function (angle) {
this._reset();
var new_angle = (this.angle || 0) + angle;
this.$('.o_viewer_img').css('transform', this._getTransform(this.scale, new_angle));
this.$('.o_viewer_img').css('max-width', new_angle % 180 !== 0 ? $(document).height() : '100%');
this.$('.o_viewer_img').css('max-height', new_angle % 180 !== 0 ? $(document).width() : '100%');
this.angle = new_angle;
},
/** /**
* Zoom in/out image by provided scale * Zoom in/out image by provided scale
* *
@@ -111,7 +136,7 @@ var DocumentViewer = Widget.extend({
*/ */
_zoom: function (scale) { _zoom: function (scale) {
if (scale > 0.5) { if (scale > 0.5) {
this.$('.o_viewer_img').css('transform', 'scale3d(' + scale + ', ' + scale + ', 1)'); this.$('.o_viewer_img').css('transform', this._getTransform(scale, this.angle || 0));
this.scale = scale; this.scale = scale;
} }
}, },
@@ -280,6 +305,14 @@ var DocumentViewer = Widget.extend({
videoElement.pause(); videoElement.pause();
} }
}, },
/**
* @private
* @param {MouseEvent} e
*/
_onRotate: function (e) {
e.preventDefault();
this._rotate(90);
},
/** /**
* @private * @private
* @param {MouseEvent} e * @param {MouseEvent} e
@@ -264,6 +264,7 @@
img { img {
width: @o-mail-attachment-image-size; width: @o-mail-attachment-image-size;
height: @o-mail-attachment-image-size; height: @o-mail-attachment-image-size;
image-orientation: from-image; // Only supported in Firefox
} }
.o_image_overlay{ .o_image_overlay{
display: none; display: none;
@@ -365,6 +366,9 @@
.o-flex-display(); .o-flex-display();
justify-content: center; justify-content: center;
align-items: center; align-items: center;
img {
image-orientation: from-image; // Only supported in Firefox
}
img, video { img, video {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
+5
View File
@@ -50,6 +50,11 @@
</div> </div>
<div t-if="widget.activeAttachment.type == 'image'" class="o_viewer_toolbar"> <div t-if="widget.activeAttachment.type == 'image'" class="o_viewer_toolbar">
<ul class="list-inline"> <ul class="list-inline">
<li>
<a class="o_rotate" href="#">
<i class="fa fa-2x fa-repeat"></i>
</a>
</li>
<li> <li>
<a class="o_zoom_out" href="#"> <a class="o_zoom_out" href="#">
<i class="fa fa-2x fa-search-minus"></i> <i class="fa fa-2x fa-search-minus"></i>
+2 -11
View File
@@ -78,8 +78,8 @@ class PaymentAcquirer(models.Model):
help="Capture the amount from Odoo, when the delivery is completed.") help="Capture the amount from Odoo, when the delivery is completed.")
# Formerly associated to `generate_and_pay_invoice` option from auto_confirm # Formerly associated to `generate_and_pay_invoice` option from auto_confirm
journal_id = fields.Many2one( journal_id = fields.Many2one(
'account.journal', 'Payment Journal', domain=[('type', '=', 'bank')], 'account.journal', 'Payment Journal', domain=[('type', 'in', ['bank', 'cash'])],
default=lambda self: self.env['account.journal'].search([('type', '=', 'bank')], limit=1), default=lambda self: self.env['account.journal'].search([('type', 'in', ['bank', 'cash'])], limit=1),
help="""Payments will be registered into this journal. If you get paid straight on your bank account, help="""Payments will be registered into this journal. If you get paid straight on your bank account,
select your bank account. If you get paid in batch for several transactions, create a specific select your bank account. If you get paid in batch for several transactions, create a specific
payment journal for this payment acquirer to easily manage the bank reconciliation. You hold payment journal for this payment acquirer to easily manage the bank reconciliation. You hold
@@ -200,22 +200,13 @@ class PaymentAcquirer(models.Model):
@api.model @api.model
def create(self, vals): def create(self, vals):
image_resize_images(vals) image_resize_images(vals)
vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).create(vals) return super(PaymentAcquirer, self).create(vals)
@api.multi @api.multi
def write(self, vals): def write(self, vals):
image_resize_images(vals) image_resize_images(vals)
vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).write(vals) return super(PaymentAcquirer, self).write(vals)
def _check_journal_id(self, vals):
if not vals.get('journal_id', False):
default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
if default_journal:
vals.update({'journal_id': default_journal.id})
return vals
@api.multi @api.multi
def toggle_website_published(self): def toggle_website_published(self):
self.write({'website_published': not self.website_published}) self.write({'website_published': not self.website_published})
+1 -1
View File
@@ -81,7 +81,7 @@
</page> </page>
<page string="Configuration"> <page string="Configuration">
<group name="acquirer_config"> <group name="acquirer_config">
<field name="journal_id" context="{'default_type': 'bank'}"/> <field name="journal_id" context="{'default_type': 'bank'}" required="1"/>
<field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/> <field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/>
<field name="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/> <field name="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/>
<field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/> <field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/>
+4 -5
View File
@@ -15,9 +15,8 @@ from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_ogone.controllers.main import OgoneController from odoo.addons.payment_ogone.controllers.main import OgoneController
from odoo.addons.payment_ogone.data import ogone from odoo.addons.payment_ogone.data import ogone
from odoo.tools import float_round, DEFAULT_SERVER_DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, ustr
from odoo.tools.float_utils import float_compare, float_repr from odoo.tools.float_utils import float_compare, float_repr, float_round
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -476,7 +475,7 @@ class PaymentTxOgone(models.Model):
self.write({ self.write({
'state': new_state, 'state': new_state,
'acquirer_reference': tree.get('PAYID'), 'acquirer_reference': tree.get('PAYID'),
'html_3ds': base64.b64decode(tree.HTML_ANSWER.decode('ascii')), 'html_3ds': ustr(base64.b64decode(tree.HTML_ANSWER.text)),
}) })
elif status in self._ogone_wait_tx_status and tries > 0: elif status in self._ogone_wait_tx_status and tries > 0:
time.sleep(0.5) time.sleep(0.5)
@@ -585,4 +584,4 @@ class PaymentToken(models.Model):
'acquirer_ref': alias, 'acquirer_ref': alias,
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name']) 'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name'])
} }
return {} return {}
@@ -51,6 +51,7 @@
<field name="available_in_pos">True</field> <field name="available_in_pos">True</field>
<field name="list_price">2.83</field> <field name="list_price">2.83</field>
<field name="name">Orange Butterfly</field> <field name="name">Orange Butterfly</field>
<field name="taxes_id">False</field>
<field name="pos_categ_id" ref="fruits_vegetables"/> <field name="pos_categ_id" ref="fruits_vegetables"/>
<field name="to_weight">True</field> <field name="to_weight">True</field>
<field name="uom_id" ref="product.product_uom_kgm" /> <field name="uom_id" ref="product.product_uom_kgm" />
+1 -1
View File
@@ -732,7 +732,7 @@ class PosOrder(models.Model):
# when the pos.config has no picking_type_id set only the moves will be created # when the pos.config has no picking_type_id set only the moves will be created
if moves and not return_picking and not order_picking: if moves and not return_picking and not order_picking:
moves._action_assign() moves._action_assign()
moves.filtered(lambda m: m.state in ['confirmed', 'waiting']).force_assign() moves.filtered(lambda m: m.state in ['confirmed', 'waiting'])._force_assign()
moves.filtered(lambda m: m.product_id.tracking == 'none')._action_done() moves.filtered(lambda m: m.product_id.tracking == 'none')._action_done()
return True return True
+11 -5
View File
@@ -1305,6 +1305,7 @@ exports.Orderline = Backbone.Model.extend({
this.type = 'unit'; this.type = 'unit';
this.selected = false; this.selected = false;
this.id = orderline_id++; this.id = orderline_id++;
this.price_manually_set = false;
if (options.price) { if (options.price) {
this.set_unit_price(options.price); this.set_unit_price(options.price);
@@ -1345,6 +1346,7 @@ exports.Orderline = Backbone.Model.extend({
orderline.price = this.price; orderline.price = this.price;
orderline.type = this.type; orderline.type = this.type;
orderline.selected = false; orderline.selected = false;
orderline.price_manually_set = this.price_manually_set;
return orderline; return orderline;
}, },
set_product_lot: function(product){ set_product_lot: function(product){
@@ -1395,7 +1397,7 @@ exports.Orderline = Backbone.Model.extend({
} }
// just like in sale.order changing the quantity will recompute the unit price // just like in sale.order changing the quantity will recompute the unit price
if(! keep_price){ if(! keep_price && ! this.price_manually_set){
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity())); this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
this.order.fix_tax_included_price(this); this.order.fix_tax_included_price(this);
} }
@@ -2232,7 +2234,11 @@ exports.Order = Backbone.Model.extend({
set_pricelist: function (pricelist) { set_pricelist: function (pricelist) {
var self = this; var self = this;
this.pricelist = pricelist; this.pricelist = pricelist;
_.each(this.get_orderlines(), function (line) {
var lines_to_recompute = _.filter(this.get_orderlines(), function (line) {
return ! line.price_manually_set;
});
_.each(lines_to_recompute, function (line) {
line.set_unit_price(line.product.get_price(self.pricelist, line.get_quantity())); line.set_unit_price(line.product.get_price(self.pricelist, line.get_quantity()));
self.fix_tax_included_price(line); self.fix_tax_included_price(line);
}); });
@@ -2355,7 +2361,7 @@ exports.Order = Backbone.Model.extend({
this.assert_editable(); this.assert_editable();
var newPaymentline = new exports.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos}); var newPaymentline = new exports.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos});
if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){ if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){
newPaymentline.set_amount( Math.max(this.get_due(),0) ); newPaymentline.set_amount( this.get_due() );
} }
this.paymentlines.add(newPaymentline); this.paymentlines.add(newPaymentline);
this.select_paymentline(newPaymentline); this.select_paymentline(newPaymentline);
@@ -2519,10 +2525,10 @@ exports.Order = Backbone.Model.extend({
} }
} }
} }
return round_pr(Math.max(0,due), this.pos.currency.rounding); return round_pr(due, this.pos.currency.rounding);
}, },
is_paid: function(){ is_paid: function(){
return this.get_due() === 0; return this.get_due() <= 0;
}, },
is_paid_with_cash: function(){ is_paid_with_cash: function(){
return !!this.paymentlines.find( function(pl){ return !!this.paymentlines.find( function(pl){
+3 -12
View File
@@ -502,7 +502,9 @@ var OrderWidget = PosBaseWidget.extend({
}else if( mode === 'discount'){ }else if( mode === 'discount'){
order.get_selected_orderline().set_discount(val); order.get_selected_orderline().set_discount(val);
}else if( mode === 'price'){ }else if( mode === 'price'){
order.get_selected_orderline().set_unit_price(val); var selected_orderline = order.get_selected_orderline();
selected_orderline.price_manually_set = true;
selected_orderline.set_unit_price(val);
} }
} }
}, },
@@ -1973,17 +1975,6 @@ var PaymentScreenWidget = ScreenWidget.extend({
return false; return false;
} }
var plines = order.get_paymentlines();
for (var i = 0; i < plines.length; i++) {
if (plines[i].get_type() === 'bank' && plines[i].get_amount() < 0) {
this.gui.show_popup('error',{
'message': _t('Negative Bank Payment'),
'comment': _t('You cannot have a negative amount in a Bank payment. Use a cash payment method to return money to the customer.'),
});
return false;
}
}
if (!order.is_paid() || this.invoicing) { if (!order.is_paid() || this.invoicing) {
return false; return false;
} }
+15 -2
View File
@@ -166,6 +166,18 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
}, { }, {
content: "verify that unit price of oranges changed to $1", content: "verify that unit price of oranges changed to $1",
trigger: ".total > .value:contains('$ 2.00')", trigger: ".total > .value:contains('$ 2.00')",
}, {
content: "order different oranges",
trigger: ".product:contains('Orange Butterfly')",
}, {
content: "change to price mode",
trigger: ".numpad button:contains('Price')",
}, {
content: "manually override the unit price of these oranges to $5",
trigger: ".numpad button.input-button:visible:contains('5')",
}, {
content: "change back to qty mode",
trigger: ".numpad button:contains('Qty')",
}, { }, {
content: "click pricelist button", content: "click pricelist button",
trigger: ".control-button.o_pricelist_button", trigger: ".control-button.o_pricelist_button",
@@ -173,8 +185,9 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
content: "select public pricelist", content: "select public pricelist",
trigger: ".selection-item:contains('Public Pricelist')", trigger: ".selection-item:contains('Public Pricelist')",
}, { }, {
content: "verify that order has been recomputed", content: "verify that the boni oranges have been recomputed and the\
trigger: ".total > .value:contains('$ 3.96')", butterfly oranges have not (their price was manually overriden)",
trigger: ".total > .value:contains('$ 8.96')",
}, { }, {
content: "click pricelist button", content: "click pricelist button",
trigger: ".control-button.o_pricelist_button", trigger: ".control-button.o_pricelist_button",
@@ -45,4 +45,13 @@
<field name="context">{'search_default_available_in_pos': 1, 'default_type': 'cash'}</field> <field name="context">{'search_default_available_in_pos': 1, 'default_type': 'cash'}</field>
</record> </record>
<!-- View Taxes -->
<menuitem id="menu_action_tax_form_open"
action="account.action_tax_form"
parent="point_of_sale.menu_point_config_product"
groups="base.group_no_one"
sequence="40"/>
</odoo> </odoo>
@@ -6,6 +6,7 @@ var ajax = require('web.ajax');
var core = require('web.core'); var core = require('web.core');
var Widget = require('web.Widget'); var Widget = require('web.Widget');
var rpc = require('web.rpc'); var rpc = require('web.rpc');
var time = require('web.time');
var qweb = core.qweb; var qweb = core.qweb;
var _t = core._t; var _t = core._t;
@@ -105,7 +106,7 @@ var PortalChatter = Widget.extend({
preprocessMessages: function(messages){ preprocessMessages: function(messages){
_.each(messages, function(m){ _.each(messages, function(m){
m['author_avatar_url'] = _.str.sprintf('/web/image/%s/%s/author_avatar/50x50', 'mail.message', m.id); m['author_avatar_url'] = _.str.sprintf('/web/image/%s/%s/author_avatar/50x50', 'mail.message', m.id);
m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(m.date).format('MMMM Do YYYY, h:mm:ss a')); m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(time.str_to_datetime(m.date)).format('MMMM Do YYYY, h:mm:ss a'));
}); });
return messages; return messages;
}, },
+7 -6
View File
@@ -7,17 +7,18 @@ from odoo import api, fields, models
class PosConfig(models.Model): class PosConfig(models.Model):
_inherit = 'pos.config' _inherit = 'pos.config'
def _get_default_discount_product(self):
return self.env.ref('point_of_sale.product_product_consumable')
iface_discount = fields.Boolean(string='Order Discounts', help='Allow the cashier to give discounts on the whole order.') iface_discount = fields.Boolean(string='Order Discounts', help='Allow the cashier to give discounts on the whole order.')
discount_pc = fields.Float(string='Discount Percentage', default=10, help='The default discount percentage') discount_pc = fields.Float(string='Discount Percentage', help='The default discount percentage')
discount_product_id = fields.Many2one('product.product', string='Discount Product', domain="[('available_in_pos', '=', True)]", help='The product used to model the discount.', default=_get_default_discount_product) discount_product_id = fields.Many2one('product.product', string='Discount Product',
domain="[('available_in_pos', '=', True), ('sale_ok', '=', True)]", help='The product used to model the discount.')
@api.onchange('module_pos_discount') @api.onchange('module_pos_discount')
def _onchange_module_pos_discount(self): def _onchange_module_pos_discount(self):
if self.module_pos_discount: if self.module_pos_discount:
self.discount_product_id = self.env['product.product'].search([('available_in_pos', '=', True)], limit=1) self.discount_product_id = self.env.ref('point_of_sale.product_product_consumable', raise_if_not_found=False)
if not self.discount_product_id or not self.discount_product_id.available_in_pos or not self.discount_product_id.sale_ok:
domain = [('available_in_pos', '=', True), ('sale_ok', '=', True)]
self.discount_product_id = self.env['product.product'].search(domain, limit=1)
self.discount_pc = 10.0 self.discount_pc = 10.0
else: else:
self.discount_product_id = False self.discount_product_id = False
@@ -23,6 +23,13 @@ var DiscountButton = screens.ActionButtonWidget.extend({
var order = this.pos.get_order(); var order = this.pos.get_order();
var lines = order.get_orderlines(); var lines = order.get_orderlines();
var product = this.pos.db.get_product_by_id(this.pos.config.discount_product_id[0]); var product = this.pos.db.get_product_by_id(this.pos.config.discount_product_id[0]);
if (product === undefined) {
this.gui.show_popup('error', {
title : _t("No discount product found"),
body : _t("The discount product seems misconfigured. Make sure it is flagged as 'Can be Sold' and 'Available in Point of Sale'."),
});
return;
}
// Remove existing discounts // Remove existing discounts
var i = 0; var i = 0;
+88 -77
View File
@@ -146,82 +146,82 @@ var lookUpCodeTransaction = {
'000000': _t('Transaction approved'), '000000': _t('Transaction approved'),
}, },
'TimeoutError': { 'TimeoutError': {
'001006': _t('Global API Not Initialized'), '001006': 'Global API Not Initialized',
'001007': _t('Timeout on Response'), '001007': 'Timeout on Response',
'003003': _t('Socket Error sending request'), '003003': 'Socket Error sending request',
'003004': _t('Socket already open or in use'), '003004': 'Socket already open or in use',
'003005': _t('Socket Creation Failed'), '003005': 'Socket Creation Failed',
'003006': _t('Socket Connection Failed'), '003006': 'Socket Connection Failed',
'003007': _t('Connection Lost'), '003007': 'Connection Lost',
'003008': _t('TCP/IP Failed to Initialize'), '003008': 'TCP/IP Failed to Initialize',
'003010': _t('Time Out waiting for server response'), '003010': 'Time Out waiting for server response',
'003011': _t('Connect Canceled'), '003011': 'Connect Canceled',
'003053': _t('Initialize Failed'), '003053': 'Initialize Failed',
'009999': _t('Unknown Error'), '009999': 'Unknown Error',
}, },
'FatalError': { 'FatalError': {
'-1': _t('Timeout error'), '-1': 'Timeout error',
'001001': _t('General Failure'), '001001': 'General Failure',
'001003': _t('Invalid Command Format'), '001003': 'Invalid Command Format',
'001004': _t('Insufficient Fields'), '001004': 'Insufficient Fields',
'001011': _t('Empty Command String'), '001011': 'Empty Command String',
'002000': _t('Password Verified'), '002000': 'Password Verified',
'002001': _t('Queue Full'), '002001': 'Queue Full',
'002002': _t('Password Failed – Disconnecting'), '002002': 'Password Failed – Disconnecting',
'002003': _t('System Going Offline'), '002003': 'System Going Offline',
'002004': _t('Disconnecting Socket'), '002004': 'Disconnecting Socket',
'002006': _t('Refused ‘Max Connections’'), '002006': 'Refused ‘Max Connections’',
'002008': _t('Duplicate Serial Number Detected'), '002008': 'Duplicate Serial Number Detected',
'002009': _t('Password Failed (Client / Server)'), '002009': 'Password Failed (Client / Server)',
'002010': _t('Password failed (Challenge / Response)'), '002010': 'Password failed (Challenge / Response)',
'002011': _t('Internal Server Error – Call Provider'), '002011': 'Internal Server Error – Call Provider',
'003002': _t('In Process with server'), '003002': 'In Process with server',
'003009': _t('Control failed to find branded serial (password lookup failed)'), '003009': 'Control failed to find branded serial (password lookup failed)',
'003012': _t('128 bit CryptoAPI failed'), '003012': '128 bit CryptoAPI failed',
'003014': _t('Threaded Auth Started Expect Response'), '003014': 'Threaded Auth Started Expect Response',
'003017': _t('Failed to start Event Thread.'), '003017': 'Failed to start Event Thread.',
'003050': _t('XML Parse Error'), '003050': 'XML Parse Error',
'003051': _t('All Connections Failed'), '003051': 'All Connections Failed',
'003052': _t('Server Login Failed'), '003052': 'Server Login Failed',
'004001': _t('Global Response Length Error (Too Short)'), '004001': 'Global Response Length Error (Too Short)',
'004002': _t('Unable to Parse Response from Global (Indistinguishable)'), '004002': 'Unable to Parse Response from Global (Indistinguishable)',
'004003': _t('Global String Error'), '004003': 'Global String Error',
'004004': _t('Weak Encryption Request Not Supported'), '004004': 'Weak Encryption Request Not Supported',
'004005': _t('Clear Text Request Not Supported'), '004005': 'Clear Text Request Not Supported',
'004010': _t('Unrecognized Request Format'), '004010': 'Unrecognized Request Format',
'004011': _t('Error Occurred While Decrypting Request'), '004011': 'Error Occurred While Decrypting Request',
'004017': _t('Invalid Check Digit'), '004017': 'Invalid Check Digit',
'004018': _t('Merchant ID Missing'), '004018': 'Merchant ID Missing',
'004019': _t('TStream Type Missing'), '004019': 'TStream Type Missing',
'004020': _t('Could Not Encrypt Response- Call Provider'), '004020': 'Could Not Encrypt Response- Call Provider',
'100201': _t('Invalid Transaction Type'), '100201': 'Invalid Transaction Type',
'100202': _t('Invalid Operator ID'), '100202': 'Invalid Operator ID',
'100203': _t('Invalid Memo'), '100203': 'Invalid Memo',
'100204': _t('Invalid Account Number'), '100204': 'Invalid Account Number',
'100205': _t('Invalid Expiration Date'), '100205': 'Invalid Expiration Date',
'100206': _t('Invalid Authorization Code'), '100206': 'Invalid Authorization Code',
'100207': _t('Invalid Authorization Code'), '100207': 'Invalid Authorization Code',
'100208': _t('Invalid Authorization Amount'), '100208': 'Invalid Authorization Amount',
'100209': _t('Invalid Cash Back Amount'), '100209': 'Invalid Cash Back Amount',
'100210': _t('Invalid Gratuity Amount'), '100210': 'Invalid Gratuity Amount',
'100211': _t('Invalid Purchase Amount'), '100211': 'Invalid Purchase Amount',
'100212': _t('Invalid Magnetic Stripe Data'), '100212': 'Invalid Magnetic Stripe Data',
'100213': _t('Invalid PIN Block Data'), '100213': 'Invalid PIN Block Data',
'100214': _t('Invalid Derived Key Data'), '100214': 'Invalid Derived Key Data',
'100215': _t('Invalid State Code'), '100215': 'Invalid State Code',
'100216': _t('Invalid Date of Birth'), '100216': 'Invalid Date of Birth',
'100217': _t('Invalid Check Type'), '100217': 'Invalid Check Type',
'100218': _t('Invalid Routing Number'), '100218': 'Invalid Routing Number',
'100219': _t('Invalid TranCode'), '100219': 'Invalid TranCode',
'100220': _t('Invalid Merchant ID'), '100220': 'Invalid Merchant ID',
'100221': _t('Invalid TStream Type'), '100221': 'Invalid TStream Type',
'100222': _t('Invalid Batch Number'), '100222': 'Invalid Batch Number',
'100223': _t('Invalid Batch Item Count'), '100223': 'Invalid Batch Item Count',
'100224': _t('Invalid MICR Input Type'), '100224': 'Invalid MICR Input Type',
'100225': _t('Invalid Driver’s License'), '100225': 'Invalid Driver’s License',
'100226': _t('Invalid Sequence Number'), '100226': 'Invalid Sequence Number',
'100227': _t('Invalid Pass Data'), '100227': 'Invalid Pass Data',
'100228': _t('Invalid Card Type'), '100228': 'Invalid Card Type',
}, },
}; };
// Popup to show all transaction state for the payment. // Popup to show all transaction state for the payment.
@@ -339,6 +339,15 @@ PaymentScreenWidget.include({
// Handler to manage the card reader string // Handler to manage the card reader string
credit_code_transaction: function (parsed_result, old_deferred, retry_nr) { credit_code_transaction: function (parsed_result, old_deferred, retry_nr) {
var order = this.pos.get_order();
if (order.get_due(order.selected_paymentline) < 0) {
this.gui.show_popup('error',{
'title': _t('Refunds not supported'),
'body': _t('Credit card refunds are not supported. Instead select your credit card payment method, click \'Validate\' and refund the original charge manually through the Vantiv backend.'),
});
return;
}
if(this.pos.getOnlinePaymentJournals().length === 0) { if(this.pos.getOnlinePaymentJournals().length === 0) {
return; return;
} }
@@ -643,9 +652,11 @@ PaymentScreenWidget.include({
}); });
} else { } else {
this._super(id); this._super(id);
order.selected_paymentline.mercury_swipe_pending = true; if (order.get_due(order.selected_paymentline) > 0) {
this.render_paymentlines(); order.selected_paymentline.mercury_swipe_pending = true;
order.trigger('change', order); // needed so that export_to_JSON gets triggered this.render_paymentlines();
order.trigger('change', order); // needed so that export_to_JSON gets triggered
}
} }
} else { } else {
this._super(id); this._super(id);
+1 -1
View File
@@ -427,7 +427,7 @@ class ProductTemplate(models.Model):
] ]
# get the value (id) sets of existing variants # get the value (id) sets of existing variants
existing_variants = {frozenset(variant.attribute_value_ids.ids) for variant in tmpl_id.product_variant_ids} existing_variants = {frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant).ids) for variant in tmpl_id.product_variant_ids}
# -> for each value set, create a recordset of values to create a # -> for each value set, create a recordset of values to create a
# variant for if the value set isn't already a variant # variant for if the value set isn't already a variant
to_create_variants = [ to_create_variants = [
+20
View File
@@ -330,3 +330,23 @@ class TestVariantsNoCreate(common.TestProductCommon):
{variant.attribute_value_ids for variant in template.product_variant_ids}, {variant.attribute_value_ids for variant in template.product_variant_ids},
{self.prod_attr1_v1, self.prod_attr1_v2}, {self.prod_attr1_v1, self.prod_attr1_v2},
) )
def test_update_variant_with_nocreate(self):
""" update variants with a 'nocreate' value on variant """
template = self.env['product.template'].create({
'name': 'Sofax',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [
(0, 0, { # one variant for this one
'attribute_id': self.prod_att_1.id,
'value_ids': [(4, self.prod_attr1_v1.id)],
}),
],
})
self.assertEqual(len(template.product_variant_ids), 1)
for variant_id in template.product_variant_ids:
variant_id.attribute_value_ids += self.size_S
template.attribute_line_ids += template.attribute_line_ids.browse()
self.assertEqual(len(template.product_variant_ids), 1)
+2
View File
@@ -537,6 +537,8 @@ class Task(models.Model):
working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg") working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg")
working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg") working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg")
working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg") working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg")
# customer portal: include comment and incoming emails in communication history
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])
_constraints = [(models.BaseModel._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])] _constraints = [(models.BaseModel._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])]
@@ -23,10 +23,10 @@
font-weight: 800; font-weight: 800;
} }
> a { > div {
font-weight: 500; font-weight: 500;
&.o_needaction{ button.o_needaction {
font-size: small; font-size: small;
font-weight: 400; font-weight: 400;
margin-left: 4px; margin-left: 4px;
@@ -139,8 +139,9 @@
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table"> <div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
<thead> <thead>
<tr class="active"> <tr class="active">
<th class="col-md-10">Task</th> <th class="col-md-9">Task</th>
<th>Stage</th> <th class="col-md-2">Stage</th>
<th>Ref</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -152,6 +153,9 @@
<td> <td>
<span class="label label-info" title="Current stage of the task" t-esc="task.stage_id.name" /> <span class="label label-info" title="Current stage of the task" t-esc="task.stage_id.name" />
</td> </td>
<td>
<span t-esc="task.id"/>
</td>
</tr> </tr>
</t> </t>
</tbody> </tbody>
@@ -171,7 +175,7 @@
<div class="col-md-12"> <div class="col-md-12">
<h4> <h4>
<t t-call='portal.record_pager'/> <t t-call='portal.record_pager'/>
Task - <span t-field="task.name"/> Task - <span t-field="task.name"/> (#<span t-esc="task.id"/>)
<span t-field="task.stage_id.name" class="pull-right label label-info" title="Current stage of this task"/> <span t-field="task.stage_id.name" class="pull-right label label-info" title="Current stage of this task"/>
<a class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=project.task&amp;id=%s&amp;view_type=form' % (task.id)" groups="project.group_project_user">Edit Task</a> <a class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=project.task&amp;id=%s&amp;view_type=form' % (task.id)" groups="project.group_project_user">Edit Task</a>
</h4> </h4>
+2 -2
View File
@@ -349,9 +349,9 @@
<div> <div>
<span class="o_value"><t t-esc="record.task_count.value"/></span> <span class="o_value"><t t-esc="record.task_count.value"/></span>
<span class="o_label"><t t-esc="record.label_tasks.value"/></span> <span class="o_label"><t t-esc="record.label_tasks.value"/></span>
<a t-if="record.task_needaction_count.raw_value" class="o_needaction" name="%(act_project_project_2_project_task_all)d" type="action" context="{'search_default_message_needaction': 1}"> <button t-if="record.task_needaction_count.raw_value" class="btn-link o_needaction" name="%(act_project_project_2_project_task_all)d" type="action" context="{'search_default_message_needaction': 1}">
<t t-esc="record.task_needaction_count.raw_value"/> <t t-esc="record.task_needaction_count.raw_value"/>
</a> </button>
</div> </div>
</a> </a>
</div> </div>
+1 -1
View File
@@ -68,7 +68,7 @@ Please find in attachment a <strong>${object.state in ('draft', 'sent') and 'req
% if object.origin: % if object.origin:
(RFQ origin: ${object.origin}) (RFQ origin: ${object.origin})
% endif % endif
amounting <strong>${object.amount_total} ${object.currency_id.name}</strong> amounting <strong>${format_amount(object.amount_total, object.currency_id)}</strong>
from ${object.company_id.name}. from ${object.company_id.name}.
</p> </p>
+1 -1
View File
@@ -351,7 +351,7 @@ class PurchaseOrder(models.Model):
@api.multi @api.multi
def button_approve(self, force=False): def button_approve(self, force=False):
self.write({'state': 'purchase'}) self.write({'state': 'purchase', 'date_approve': fields.Date.context_today(self)})
self._create_picking() self._create_picking()
if self.company_id.po_lock == 'lock': if self.company_id.po_lock == 'lock':
self.write({'state': 'done'}) self.write({'state': 'done'})
+16
View File
@@ -181,3 +181,19 @@ class Orderpoint(models.Model):
for poline in self.env['purchase.order.line'].search([('state','in',('draft','sent','to approve')),('orderpoint_id','in',self.ids)]): for poline in self.env['purchase.order.line'].search([('state','in',('draft','sent','to approve')),('orderpoint_id','in',self.ids)]):
res[poline.orderpoint_id.id] += poline.product_uom._compute_quantity(poline.product_qty, poline.orderpoint_id.product_uom, round=False) res[poline.orderpoint_id.id] += poline.product_uom._compute_quantity(poline.product_qty, poline.orderpoint_id.product_uom, round=False)
return res return res
def action_view_purchase(self):
""" This function returns an action that display existing
purchase orders of given orderpoint.
"""
action = self.env.ref('purchase.purchase_rfq')
result = action.read()[0]
# Remvove the context since the action basically display RFQ and not PO.
result['context'] = {}
order_line_ids = self.env['purchase.order.line'].search([('orderpoint_id', '=', self.id)])
purchase_ids = order_line_ids.mapped('order_id')
result['domain'] = "[('id','in',%s)]" % (purchase_ids.ids)
return result
@@ -58,7 +58,7 @@
<field name="inherit_id" ref="account.invoice_supplier_form"/> <field name="inherit_id" ref="account.invoice_supplier_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="reference" position="after" > <field name="reference" position="after" >
<field name="purchase_id" attrs="{'readonly': [('state','not in',['draft'])], 'invisible': [('state', '=', 'purchase')]}" class="oe_edit_only" <field name="purchase_id" attrs="{'readonly': [('state','not in',['draft'])], 'invisible': ['|', ('state', '=', 'purchase'), ('type', '=', 'in_refund')]}" class="oe_edit_only"
options="{'no_create': True}" context="{'show_total_amount': True}"/> options="{'no_create': True}" context="{'show_total_amount': True}"/>
</field> </field>
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='company_id']" position="after"> <xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='company_id']" position="after">
+6 -3
View File
@@ -30,9 +30,12 @@
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_form"/> <field name="inherit_id" ref="stock.view_warehouse_orderpoint_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<div name="button_box" position="inside"> <div name="button_box" position="inside">
<button name="%(purchase.purchase_order_action_generic)d" type="action" <button type="object"
string="Purchases" class="oe_stat_button" icon="fa-refresh" name="action_view_purchase"
domain="[('orderpoint_id','=',active_id)]"/> class="oe_stat_button"
string="Purchases"
icon="fa-refresh">
</button>
</div> </div>
</field> </field>
</record> </record>
+1 -1
View File
@@ -743,7 +743,7 @@ class ResourceCalendarLeaves(models.Model):
date_from = fields.Datetime('Start Date', required=True) date_from = fields.Datetime('Start Date', required=True)
date_to = fields.Datetime('End Date', required=True) date_to = fields.Datetime('End Date', required=True)
tz = fields.Selection( tz = fields.Selection(
_tz_get, string='Timezone', default=lambda self: self._context.get('tz', self.env.user.tz or 'UTC'), _tz_get, string='Timezone', default=lambda self: self._context.get('tz') or self.env.user.tz or 'UTC',
help="Timezone used when encoding the leave. It is used to correctly " help="Timezone used when encoding the leave. It is used to correctly "
"localize leave hours when computing time intervals.") "localize leave hours when computing time intervals.")
resource_id = fields.Many2one( resource_id = fields.Many2one(
+4 -2
View File
@@ -57,7 +57,8 @@ class ResourceMixin(models.AbstractModel):
theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar)
work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
total_work_time += work_time total_work_time += work_time
days_count += float_utils.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 if theoric_hours:
days_count += float_utils.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
return { return {
'days': days_count, 'days': days_count,
'hours': total_work_time.total_seconds() / 3600, 'hours': total_work_time.total_seconds() / 3600,
@@ -80,7 +81,8 @@ class ResourceMixin(models.AbstractModel):
for day_intervals in calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id): for day_intervals in calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id):
theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar)
leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
days_count += float_utils.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 if theoric_hours:
days_count += float_utils.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
return days_count return days_count
def iter_leaves(self, from_datetime, to_datetime, calendar=None): def iter_leaves(self, from_datetime, to_datetime, calendar=None):
+1 -1
View File
@@ -11,12 +11,12 @@ import odoo
from odoo import api, SUPERUSER_ID from odoo import api, SUPERUSER_ID
def uninstall_hook(cr, registry): def uninstall_hook(cr, registry):
def update_dashboard_graph_model(dbname): def update_dashboard_graph_model(dbname):
db_registry = odoo.modules.registry.Registry.new(dbname) db_registry = odoo.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr: with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {}) env = api.Environment(cr, SUPERUSER_ID, {})
env['account.invoice.report'].init()
if 'crm.team' in env: if 'crm.team' in env:
recs = env['crm.team'].search([]) recs = env['crm.team'].search([])
for rec in recs: for rec in recs:
+1
View File
@@ -12,3 +12,4 @@ from . import sale
from . import res_config_settings from . import res_config_settings
from . import sale_layout from . import sale_layout
from . import sales_team from . import sales_team
from . import ir_model_fields
-10
View File
@@ -96,15 +96,6 @@ class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line' _inherit = 'account.invoice.line'
_order = 'invoice_id, layout_category_id, sequence, id' _order = 'invoice_id, layout_category_id, sequence, id'
@api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity',
'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id',
'invoice_id.date_invoice')
def _compute_total_price(self):
for line in self:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.invoice_line_tax_ids.compute_all(price, line.invoice_id.currency_id, line.quantity, product=line.product_id, partner=line.invoice_id.partner_id)
line.price_total = taxes['total_included']
sale_line_ids = fields.Many2many( sale_line_ids = fields.Many2many(
'sale.order.line', 'sale.order.line',
'sale_order_line_invoice_rel', 'sale_order_line_invoice_rel',
@@ -113,4 +104,3 @@ class AccountInvoiceLine(models.Model):
layout_category_id = fields.Many2one('sale.layout_category', string='Section') layout_category_id = fields.Many2one('sale.layout_category', string='Section')
layout_category_sequence = fields.Integer(string='Layout Sequence') layout_category_sequence = fields.Integer(string='Layout Sequence')
# TODO: remove layout_category_sequence in master or make it work properly # TODO: remove layout_category_sequence in master or make it work properly
price_total = fields.Monetary(compute='_compute_total_price', string='Total Amount', store=True)
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from odoo import models
# This is a nasty hack, targeted for V11 only
class IrModelFields(models.Model):
_inherit = 'ir.model.fields'
def unlink(self):
# Prevent the deletion of some `shared` fields... -_-
self = self.filtered(
lambda rec: not (
rec.model == 'res.config.settings'
and rec.name == 'auth_signup_uninvited'
)
)
return super(IrModelFields, self).unlink()
+2
View File
@@ -10,6 +10,8 @@ class ProductProduct(models.Model):
@api.multi @api.multi
def _sales_count(self): def _sales_count(self):
r = {} r = {}
if not self.user_has_groups('sales_team.group_sale_salesman'):
return r
domain = [ domain = [
('state', 'in', ['sale', 'done']), ('state', 'in', ['sale', 'done']),
('product_id', 'in', self.ids), ('product_id', 'in', self.ids),
+1 -1
View File
@@ -501,7 +501,7 @@ class SaleOrder(models.Model):
@api.returns('self', lambda value: value.id) @api.returns('self', lambda value: value.id)
def message_post(self, **kwargs): def message_post(self, **kwargs):
if self.env.context.get('mark_so_as_sent'): if self.env.context.get('mark_so_as_sent'):
self.filtered(lambda o: o.state == 'draft').write({'state': 'sent'}) self.filtered(lambda o: o.state == 'draft').with_context(tracking_disable=True).write({'state': 'sent'})
return super(SaleOrder, self.with_context(mail_post_autofollow=True)).message_post(**kwargs) return super(SaleOrder, self.with_context(mail_post_autofollow=True)).message_post(**kwargs)
@api.multi @api.multi
+3 -2
View File
@@ -644,7 +644,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<button name="toggle_active" position="before"> <button name="toggle_active" position="before">
<button class="oe_stat_button" name="%(action_product_sale_list)d" <button class="oe_stat_button" name="%(action_product_sale_list)d"
type="action" icon="fa-usd"> type="action" icon="fa-usd" groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" /> <field string="Sales" name="sales_count" widget="statinfo" />
</button> </button>
</button> </button>
@@ -666,7 +666,8 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<button name="toggle_active" position="before"> <button name="toggle_active" position="before">
<button class="oe_stat_button" name="action_view_sales" <button class="oe_stat_button" name="action_view_sales"
type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}"> type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}"
groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" /> <field string="Sales" name="sales_count" widget="statinfo" />
</button> </button>
</button> </button>
+9
View File
@@ -11,3 +11,12 @@ def uninstall_hook(cr, registry):
('module', '=', 'sale') ('module', '=', 'sale')
]).mapped('res_id') ]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': False}) env['ir.ui.menu'].browse(res_ids).update({'active': False})
def post_init_hook(cr, registry):
env = Environment(cr, SUPERUSER_ID, {})
res_ids = env['ir.model.data'].search([
('model', '=', 'ir.ui.menu'),
('module', '=', 'sale'),
]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': True})
+1
View File
@@ -48,4 +48,5 @@ The Dashboard for the Sales Manager will include
], ],
'application': True, 'application': True,
'uninstall_hook': 'uninstall_hook', 'uninstall_hook': 'uninstall_hook',
'post_init_hook': 'post_init_hook',
} }
+2 -1
View File
@@ -92,7 +92,8 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<button name="action_view_orderpoints" position="after"> <button name="action_view_orderpoints" position="after">
<button class="oe_stat_button" name="action_view_sales" <button class="oe_stat_button" name="action_view_sales"
type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}"> type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}"
groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" /> <field string="Sales" name="sales_count" widget="statinfo" />
</button> </button>
</button> </button>
+1 -1
View File
@@ -141,7 +141,7 @@ class AccountAnalyticLine(models.Model):
values['timesheet_invoice_type'] = 'billable_fixed' values['timesheet_invoice_type'] = 'billable_fixed'
# if the so line is already invoiced, and the delivered qty is still smaller than the ordered, then link the timesheet to the invoice # if the so line is already invoiced, and the delivered qty is still smaller than the ordered, then link the timesheet to the invoice
if so_line.invoice_status == 'invoiced': if so_line.invoice_status == 'invoiced':
values['timesheet_invoice_id'] = so_line.invoice_lines[0].invoice_id.id values['timesheet_invoice_id'] = so_line.invoice_lines and so_line.invoice_lines[0].invoice_id.id
elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type != 'timesheet': elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type != 'timesheet':
values['timesheet_invoice_type'] = 'billable_fixed' values['timesheet_invoice_type'] = 'billable_fixed'
@@ -52,9 +52,7 @@
<a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['timesheet_invoice_id'], 'pivot_column_groupby': ['date:month'],'pivot_measures': ['amount', 'timesheet_revenue']}">Profitability</a> <a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['timesheet_invoice_id'], 'pivot_column_groupby': ['date:month'],'pivot_measures': ['amount', 'timesheet_revenue']}">Profitability</a>
</th> </th>
<td class="o_timesheet_plan_dashboard_cell"> <td class="o_timesheet_plan_dashboard_cell">
<t t-if="currency.position == 'before'" t-esc="currency.symbol"/> <t t-esc="dashboard['money_amount']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<t t-esc="dashboard['money_amount']['invoiced']"/>
<t t-if="currency.position == 'after'" t-esc="currency.symbol"/>
</td> </td>
<td>Invoiced</td> <td>Invoiced</td>
</tr> </tr>
@@ -68,9 +66,7 @@
</td> </td>
<td>Fixed</td> <td>Fixed</td>
<td class="o_timesheet_plan_dashboard_cell"> <td class="o_timesheet_plan_dashboard_cell">
<t t-if="currency.position == 'before'" t-esc="currency.symbol"/> <t t-esc="dashboard['money_amount']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<t t-esc="dashboard['money_amount']['to_invoice']"/>
<t t-if="currency.position == 'after'" t-esc="currency.symbol"/>
</td> </td>
<td>To invoice</td> <td>To invoice</td>
</tr> </tr>
@@ -84,9 +80,7 @@
</td> </td>
<td>No task found</td> <td>No task found</td>
<td class="o_timesheet_plan_dashboard_cell"> <td class="o_timesheet_plan_dashboard_cell">
<t t-if="currency.position == 'before'" t-esc="currency.symbol"/> <t t-esc="dashboard['money_amount']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<t t-esc="dashboard['money_amount']['cost']"/>
<t t-if="currency.position == 'after'" t-esc="currency.symbol"/>
</td> </td>
<td>Cost</td> <td>Cost</td>
</tr> </tr>
@@ -110,9 +104,7 @@
<td></td> <td></td>
<td></td> <td></td>
<td class="o_timesheet_plan_dashboard_total"> <td class="o_timesheet_plan_dashboard_total">
<t t-if="currency.position == 'before'" t-esc="currency.symbol"/> <t t-esc="dashboard['money_amount']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<t t-esc="dashboard['money_amount']['total']"/>
<t t-if="currency.position == 'after'" t-esc="currency.symbol"/>
</td> </td>
<td><b>Total</b></td> <td><b>Total</b></td>
</tr> </tr>
+13 -1
View File
@@ -4,4 +4,16 @@
from . import controllers from . import controllers
from . import models from . import models
from . import report from . import report
from . import wizard from . import wizard
from odoo import api, SUPERUSER_ID
# TODO: Apply proper fix & remove in master
def pre_init_hook(cr):
env = api.Environment(cr, SUPERUSER_ID, {})
env['ir.model.data'].search([
('model', 'like', '%stock%'),
('module', '=', 'stock')
]).unlink()
+1
View File
@@ -74,4 +74,5 @@
'installable': True, 'installable': True,
'application': True, 'application': True,
'auto_install': False, 'auto_install': False,
'pre_init_hook': 'pre_init_hook',
} }
+6 -2
View File
@@ -11,10 +11,14 @@ class StockScrap(models.Model):
_order = 'id desc' _order = 'id desc'
def _get_default_scrap_location_id(self): def _get_default_scrap_location_id(self):
return self.env['stock.location'].search([('scrap_location', '=', True)], limit=1).id return self.env['stock.location'].search([('scrap_location', '=', True), ('company_id', 'in', [self.env.user.company_id.id, False])], limit=1).id
def _get_default_location_id(self): def _get_default_location_id(self):
return self.env.ref('stock.stock_location_stock', raise_if_not_found=False) company_user = self.env.user.company_id
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
if warehouse:
return warehouse.lot_stock_id.id
return None
name = fields.Char( name = fields.Char(
'Reference', default=lambda self: _('New'), 'Reference', default=lambda self: _('New'),
+1 -1
View File
@@ -117,7 +117,7 @@
</td> </td>
<td class="text-right" t-if="move_line.state == 'done'"> <td class="text-right" t-if="move_line.state == 'done'">
<t t-if="move_line.ordered_qty != move_line.qty_done"> <t t-if="move_line.ordered_qty != move_line.qty_done">
<t t-set="backorder" t-value="True"/> <t t-set="backorder" t-value="o.search([('backorder_id', '=', o.id), ('state', 'not in', ['cancel', 'done'])])"/>
</t> </t>
<span t-field="move_line.qty_done"/> <span t-field="move_line.qty_done"/>
<span t-field="move_line.product_uom_id"/> <span t-field="move_line.product_uom_id"/>
+7
View File
@@ -402,6 +402,13 @@ class StockMove(models.Model):
if remaining_value_before_vacuum < 0: if remaining_value_before_vacuum < 0:
corrected_value += remaining_value_before_vacuum corrected_value += remaining_value_before_vacuum
# If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We
# force the amount in the context, but in the case it is 0 it'll create an entry
# for the entire cost of the move. This case happens when the candidates moves
# entirely compensate the problematic move.
if not corrected_value:
continue
if move._is_in(): if move._is_in():
# If we just compensated an IN move that has a negative remaining # If we just compensated an IN move that has a negative remaining
# quantity, it means the move has returned more items than it received. # quantity, it means the move has returned more items than it received.
@@ -1330,7 +1330,8 @@ class TestStockValuation(TransactionCase):
def test_fifo_negative_2(self): def test_fifo_negative_2(self):
""" Receives 10 units, send more, the extra quantity should be valued at the last fifo """ Receives 10 units, send more, the extra quantity should be valued at the last fifo
price, running the vacuum should not do anything. price, running the vacuum should not do anything. Receive 2 units at the price the two
extra units were sent, check that no accounting entries are created.
""" """
self.product1.product_tmpl_id.cost_method = 'fifo' self.product1.product_tmpl_id.cost_method = 'fifo'
@@ -1429,9 +1430,6 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move1.account_move_ids), 1) self.assertEqual(len(move1.account_move_ids), 1)
self.assertEqual(len(move2.account_move_ids), 1) self.assertEqual(len(move2.account_move_ids), 1)
# ---------------------------------------------------------------------
# Ending
# ---------------------------------------------------------------------
self.assertEqual(self.product1.qty_available, -2) self.assertEqual(self.product1.qty_available, -2)
self.assertEqual(self.product1.stock_value, -20) self.assertEqual(self.product1.stock_value, -20)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0) self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
@@ -1441,6 +1439,64 @@ class TestStockValuation(TransactionCase):
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 120) self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 120)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0) self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
# Now receive exactly the extra units at exactly the price sent, no
# accounting entries should be created after the vacuum.
# ---------------------------------------------------------------------
# Receive 2@10
# ---------------------------------------------------------------------
move3 = self.env['stock.move'].create({
'name': '10 in',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 2.0,
'price_unit': 10,
'move_line_ids': [(0, 0, {
'product_id': self.product1.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 2.0,
})]
})
move3._action_confirm()
move3._action_done()
# ---------------------------------------------------------------------
# Run the vacuum
# ---------------------------------------------------------------------
self.env['stock.move']._run_fifo_vacuum()
# ---------------------------------------------------------------------
# Ending
# ---------------------------------------------------------------------
self.assertEqual(move1.value, 100.0)
self.assertEqual(move1.remaining_qty, 0.0)
self.assertEqual(move1.price_unit, 10.0)
self.assertEqual(move1.remaining_value, 0.0)
self.assertEqual(move2.value, -120.0)
self.assertEqual(move2.remaining_qty, 0)
self.assertEqual(move2.remaining_value, 0)
self.assertEqual(move3.value, 20)
self.assertEqual(move3.remaining_qty, 0.0)
self.assertEqual(move3.price_unit, 10.0)
self.assertEqual(move3.remaining_value, 0.0)
self.assertEqual(len(move1.account_move_ids), 1)
self.assertEqual(len(move2.account_move_ids), 1)
self.assertEqual(len(move3.account_move_ids), 1) # the created account move is due to the receipt
# nothing should have changed in the accounting regarding the output
self.assertEqual(self.product1.qty_available, 0)
self.assertEqual(self.product1.stock_value, 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 120)
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 120)
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 120)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 120)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
def test_fifo_negative_3(self): def test_fifo_negative_3(self):
""" Receives 10 units, send 10 units, then send more: the extra quantity should be valued """ Receives 10 units, send 10 units, then send more: the extra quantity should be valued
at the last fifo price, running the vacuum should not do anything. at the last fifo price, running the vacuum should not do anything.
@@ -1008,7 +1008,10 @@ var FieldX2Many = AbstractField.extend({
* @param {OdooEvent} ev * @param {OdooEvent} ev
*/ */
_onDiscardChanges: function (ev) { _onDiscardChanges: function (ev) {
ev.data.fieldName = this.name; if (ev.target !== this) {
ev.stopPropagation();
this.trigger_up('discard_changes', _.extend({}, ev.data, {fieldName: this.name}));
}
}, },
/** /**
* Called when the renderer asks to edit a line, in that case simply tells * Called when the renderer asks to edit a line, in that case simply tells
@@ -103,7 +103,7 @@ var CrashManager = core.Class.extend({
if (!this.active) { if (!this.active) {
return; return;
} }
new Dialog(this, { return new Dialog(this, {
size: 'medium', size: 'medium',
title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"), title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"),
subtitle: error.data.title, subtitle: error.data.title,
@@ -149,10 +149,10 @@ var CrashManager = core.Class.extend({
clipboard.destroy(); clipboard.destroy();
}); });
dialog.open(); return dialog.open();
}, },
show_message: function(exception) { show_message: function(exception) {
this.show_error({ return this.show_error({
type: _t("Odoo Client Error"), type: _t("Odoo Client Error"),
message: exception, message: exception,
data: {debug: ""} data: {debug: ""}
@@ -274,6 +274,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @returns {Deferred} * @returns {Deferred}
*/ */
_confirmChange: function (id, fields, e) { _confirmChange: function (id, fields, e) {
if (e.name === 'discard_changes' && e.target.reset) {
// the target of the discard event is a field widget. In that
// case, we simply want to reset the specific field widget,
// not the full view
return e.target.reset(this.model.get(e.target.dataPointID), e, true);
}
var state = this.model.get(this.handle); var state = this.model.get(this.handle);
return this.renderer.confirmChange(state, id, fields, e); return this.renderer.confirmChange(state, id, fields, e);
}, },
@@ -189,6 +189,90 @@ var BasicModel = AbstractModel.extend({
return id; return id;
}); });
}, },
/**
* Add and process default values for a given record. Those values are
* parsed and stored in the '_changes' key of the record. For relational
* fields, sub-dataPoints are created, and missing relational data is
* fetched. Also generate default values for fields with no given value.
* Typically, this function is called with the result of a 'default_get'
* RPC, to populate a newly created dataPoint. It may also be called when a
* one2many subrecord is open in a form view (dialog), to generate the
* default values for the fields displayed in the o2m form view, but not in
* the list or kanban (mainly to correctly create sub-dataPoints for
* relational fields).
*
* @param {string} recordID local id for a record
* @param {Object} values dict of default values for the given record
* @param {Object} [options]
* @param {string} [options.viewType] current viewType. If not set, we will
* assume main viewType from the record
* @param {Array} [options.fieldNames] list of field names for which a
* default value must be generated (used to complete the values dict)
* @returns {Deferred}
*/
applyDefaultValues: function (recordID, values, options) {
options = options || {};
var record = this.localData[recordID];
var viewType = options.viewType || record.viewType;
var fieldNames = options.fieldNames || Object.keys(record.fieldsInfo[viewType]);
var field;
var fieldName;
record._changes = record._changes || {};
// ignore values for non requested fields (for instance, fields that are
// not in the view)
values = _.pick(values, fieldNames);
// fill default values for missing fields
for (var i = 0; i < fieldNames.length; i++) {
fieldName = fieldNames[i];
if (!(fieldName in values) && !(fieldName in record._changes)) {
field = record.fields[fieldName];
if (field.type === 'float' ||
field.type === 'integer' ||
field.type === 'monetary') {
values[fieldName] = 0;
} else if (field.type === 'one2many' || field.type === 'many2many') {
values[fieldName] = [];
} else {
values[fieldName] = null;
}
}
}
// parse each value and create dataPoints for relational fields
var defs = [];
for (fieldName in values) {
field = record.fields[fieldName];
record.data[fieldName] = null;
var dp;
if (field.type === 'many2one' && values[fieldName]) {
dp = this._makeDataPoint({
context: record.context,
data: {id: values[fieldName]},
modelName: field.relation,
parentID: record.id,
});
record._changes[fieldName] = dp.id;
} else if (field.type === 'reference' && values[fieldName]) {
var ref = values[fieldName].split(',');
dp = this._makeDataPoint({
context: record.context,
data: {id: parseInt(ref[1])},
modelName: ref[0],
parentID: record.id,
});
defs.push(this._fetchNameGet(dp));
record._changes[fieldName] = dp.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
defs.push(this._processX2ManyCommands(record, fieldName, values[fieldName], options));
} else {
record._changes[fieldName] = this._parseServerValue(field, values[fieldName]);
}
}
return $.when.apply($, defs);
},
/** /**
* Onchange RPCs may return values for fields that are not in the current * Onchange RPCs may return values for fields that are not in the current
* view. Those fields might even be unknown when the onchange returns (e.g. * view. Those fields might even be unknown when the onchange returns (e.g.
@@ -1397,6 +1481,7 @@ var BasicModel = AbstractModel.extend({
list = self._makeDataPoint({ list = self._makeDataPoint({
fields: view ? view.fields : fieldInfo.relatedFields, fields: view ? view.fields : fieldInfo.relatedFields,
fieldsInfo: view ? view.fieldsInfo : fieldInfo.fieldsInfo, fieldsInfo: view ? view.fieldsInfo : fieldInfo.fieldsInfo,
limit: fieldInfo.limit,
modelName: field.relation, modelName: field.relation,
parentID: record.id, parentID: record.id,
static: true, static: true,
@@ -3228,27 +3313,8 @@ var BasicModel = AbstractModel.extend({
context: params.context, context: params.context,
}) })
.then(function (result) { .then(function (result) {
// fill default values for missing fields
for (var i = 0; i < fieldNames.length; i++) {
var fieldName = fieldNames[i];
if (!(fieldName in result)) {
var field = params.fields[fieldName];
if (field.type === 'float' ||
field.type === 'integer' ||
field.type === 'monetary') {
result[fieldName] = 0;
} else if (field.type === 'one2many' || field.type === 'many2many') {
result[fieldName] = [];
} else {
result[fieldName] = null;
}
}
}
var data = {};
var record = self._makeDataPoint({ var record = self._makeDataPoint({
modelName: modelName, modelName: modelName,
data: data,
fields: params.fields, fields: params.fields,
fieldsInfo: params.fieldsInfo, fieldsInfo: params.fieldsInfo,
context: params.context, context: params.context,
@@ -3257,37 +3323,7 @@ var BasicModel = AbstractModel.extend({
viewType: params.viewType, viewType: params.viewType,
}); });
var defs = []; return self.applyDefaultValues(record.id, result)
_.each(fieldNames, function (name) {
var field = params.fields[name];
data[name] = null;
record._changes = record._changes || {};
var dp;
if (field.type === 'many2one' && result[name]) {
dp = self._makeDataPoint({
context: record.context,
data: {id: result[name]},
modelName: field.relation,
parentID: record.id,
});
record._changes[name] = dp.id;
} else if (field.type === 'reference' && result[name]) {
var ref = result[name].split(',');
dp = self._makeDataPoint({
context: record.context,
data: {id: parseInt(ref[1])},
modelName: ref[0],
parentID: record.id,
});
defs.push(self._fetchNameGet(dp));
record._changes[name] = dp.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
defs.push(self._processX2ManyCommands(record, name, result[name]));
} else {
record._changes[name] = self._parseServerValue(field, result[name]);
}
});
return $.when.apply($, defs)
.then(function () { .then(function () {
var def = $.Deferred(); var def = $.Deferred();
self._performOnChange(record, fields_key).always(function () { self._performOnChange(record, fields_key).always(function () {
@@ -3470,13 +3506,17 @@ var BasicModel = AbstractModel.extend({
* @param {Object} record * @param {Object} record
* @param {string} fieldName * @param {string} fieldName
* @param {Array[Array]} commands * @param {Array[Array]} commands
* @param {Object} [options]
* @param {string} [options.viewType] current viewType. If not set, we will
* assume main viewType from the record
* @returns {Deferred} * @returns {Deferred}
*/ */
_processX2ManyCommands: function (record, fieldName, commands) { _processX2ManyCommands: function (record, fieldName, commands, options) {
var self = this; var self = this;
options = options || {};
var defs = []; var defs = [];
var field = record.fields[fieldName]; var field = record.fields[fieldName];
var fieldInfo = record.fieldsInfo[record.viewType][fieldName]; var fieldInfo = record.fieldsInfo[options.viewType || record.viewType][fieldName];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo; var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
var fields = view ? view.fields : fieldInfo.relatedFields; var fields = view ? view.fields : fieldInfo.relatedFields;
@@ -3770,14 +3810,14 @@ var BasicModel = AbstractModel.extend({
// for multi-pages list datapoints, we might need to read the // for multi-pages list datapoints, we might need to read the
// order field first to apply the order on all pages // order field first to apply the order on all pages
if (list.res_ids.length > list.limit) { if (list.res_ids.length > list.limit && list.orderedBy.length) {
if (!list.orderedResIDs && list.orderedBy.length) { if (!list.orderedResIDs) {
var fieldNames = _.pluck(list.orderedBy, 'name'); var fieldNames = _.pluck(list.orderedBy, 'name');
def = this._readMissingFields(list, _.filter(list.res_ids, _.isNumber), fieldNames); def = this._readMissingFields(list, _.filter(list.res_ids, _.isNumber), fieldNames);
def.then(function () {
self._sortList(list);
});
} }
def.then(function () {
self._sortList(list);
});
} }
return def.then(function () { return def.then(function () {
var resIDs = []; var resIDs = [];
@@ -161,7 +161,12 @@ var BasicView = AbstractView.extend({
// (because those fields were unknow at that time). So we ask // (because those fields were unknow at that time). So we ask
// the model to process them. // the model to process them.
def = this.model.applyRawChanges(record.id, viewType).then(function () { def = this.model.applyRawChanges(record.id, viewType).then(function () {
if (!self.model.isNew(record.id)) { if (self.model.isNew(record.id)) {
return self.model.applyDefaultValues(record.id, {}, {
fieldNames: fieldNames,
viewType: viewType,
});
} else {
return self.model.reload(record.id, { return self.model.reload(record.id, {
fieldNames: fieldNames, fieldNames: fieldNames,
keepChanges: true, keepChanges: true,
@@ -136,7 +136,7 @@ var CalendarController = AbstractController.extend({
// When clicking on a random day of a random other week, switch to week view // When clicking on a random day of a random other week, switch to week view
this.model.setScale('week'); this.model.setScale('week');
} }
this.model.setDate(event.data.date, true); this.model.setDate(event.data.date);
this.reload(); this.reload();
}, },
/** /**
@@ -216,9 +216,9 @@ return AbstractModel.extend({
filters: params.filters, filters: params.filters,
}; };
this.setDate(params.initialDate);
// Use mode attribute in xml file to specify zoom timeline (day,week,month) // Use mode attribute in xml file to specify zoom timeline (day,week,month)
// by default month. // by default month.
this.setDate(params.initialDate, true);
this.setScale(params.mode); this.setScale(params.mode);
_.each(this.data.filters, function (filter) { _.each(this.data.filters, function (filter) {
@@ -251,9 +251,8 @@ return AbstractModel.extend({
}, },
/** /**
* @param {Moment} start * @param {Moment} start
* @param {boolean} highlight
*/ */
setDate: function (start, highlight) { setDate: function (start) {
this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start; this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start;
this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes'); this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes');
@@ -270,9 +269,6 @@ return AbstractModel.extend({
this.data.start_date = this.data.start_date.clone().startOf('day'); this.data.start_date = this.data.start_date.clone().startOf('day');
this.data.end_date = this.data.end_date.clone().endOf('day'); this.data.end_date = this.data.end_date.clone().endOf('day');
} }
if (highlight) {
this.data.highlight_date = this.data.target_date;
}
}, },
setScale: function (scale) { setScale: function (scale) {
if (!_.contains(scales, scale)) { if (!_.contains(scales, scale)) {
@@ -422,11 +422,6 @@ return AbstractRenderer.extend({
this.target_date = this.state.target_date.toString(); this.target_date = this.state.target_date.toString();
} }
var highlightDate = moment(this.state.highlight_date).format('YYYY-MM-DD');
$calendar.find('.o_target_date').removeClass('o_target_date');
$calendar.find('.fc-bg .fc-day[data-date="'+highlightDate+'"]')
.addClass('o_target_date');
this.$small_calendar.datepicker("setDate", this.state.highlight_date.toDate()) this.$small_calendar.datepicker("setDate", this.state.highlight_date.toDate())
.find('.o_selected_range') .find('.o_selected_range')
.removeClass('o_color o_selected_range'); .removeClass('o_color o_selected_range');
@@ -40,7 +40,11 @@ return Dialog.extend({
this.domainSelector = new DomainSelector(this, model, domain, options); this.domainSelector = new DomainSelector(this, model, domain, options);
}, },
start: function () { start: function () {
this.$el.css("overflow", "visible").closest(".modal-dialog").css("height", "auto"); // This restores default modal height (bootstrap) and allows field selector to overflow var self = this;
this.opened().then(function () {
// this restores default modal height (bootstrap) and allows field selector to overflow
self.$el.css('overflow', 'visible').closest('.modal-dialog').css('height', 'auto');
});
return $.when( return $.when(
this._super.apply(this, arguments), this._super.apply(this, arguments),
this.domainSelector.appendTo(this.$el) this.domainSelector.appendTo(this.$el)
@@ -68,6 +68,13 @@ var Notification = Widget.extend({
self.$el.animate({height: 0}, 400, "swing", self.destroy.bind(self)); self.$el.animate({height: 0}, 400, "swing", self.destroy.bind(self));
}); });
}, },
/**
* @override
*/
start: function () {
this.$el.addClass('o_error');
return this._super.apply(this, arguments);
},
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Handlers // Handlers
@@ -71,11 +71,6 @@
} }
} }
} }
.o_target_date:not(.fc-today) {
background-color: @odoo-brand-primary;
opacity: 0.1;
}
} }
.o_calendar_sidebar_container { .o_calendar_sidebar_container {
-1
View File
@@ -125,7 +125,6 @@
</div> </div>
</t> </t>
<t t-name="Warning" t-extend="Notification"> <t t-name="Warning" t-extend="Notification">
<t t-jquery=".o_notification">$(this).addClass('o_error');</t>
<t t-jquery=".o_icon" t-operation="replace"> <t t-jquery=".o_icon" t-operation="replace">
<span class="o_icon fa fa-3x fa-exclamation"/> <span class="o_icon fa fa-3x fa-exclamation"/>
</t> </t>
@@ -368,10 +368,6 @@ QUnit.module('relational_fields', {
"should have an open record button"); "should have an open record button");
form.$('input.o_input').click(); form.$('input.o_input').click();
form.$('input.o_input').trigger($.Event('keyup', {
which: $.ui.keyCode.ESC,
keyCode: $.ui.keyCode.ESC,
}));
assert.strictEqual(form.$('button.o_external_button:visible').length, 1, assert.strictEqual(form.$('button.o_external_button:visible').length, 1,
"should still have an open record button"); "should still have an open record button");
@@ -3064,6 +3060,108 @@ QUnit.module('relational_fields', {
form.destroy(); form.destroy();
}); });
QUnit.test('onchange on one2many containing x2many in form view', function (assert) {
assert.expect(16);
this.data.partner.onchanges = {
foo: function (obj) {
obj.turtles = [[0, false, {turtle_foo: 'new record'}]];
},
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form>' +
'<field name="foo"/>' +
'<field name="turtles">' +
'<tree>' +
'<field name="turtle_foo"/>' +
'</tree>' +
'<form>' +
'<field name="partner_ids">' +
'<tree editable="top">' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</form>' +
'</field>' +
'</form>',
archs: {
'partner,false,list': '<tree><field name="foo"/></tree>',
'partner,false,search': '<search></search>',
},
});
assert.strictEqual(form.$('.o_data_row').length, 1,
"the onchange should have created one record in the relation");
// open the created o2m record in a form view, and add a m2m subrecord
// in its relation
form.$('.o_data_row').click();
assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
assert.strictEqual($('.modal .o_data_row').length, 0,
"there should be no record in the one2many in the dialog");
// add a many2many subrecord
$('.modal .o_field_x2many_list_row_add a').click();
assert.strictEqual($('.modal').length, 2,
"should have opened a second dialog");
// select a many2many subrecord
$('.modal:nth(1) .o_list_view .o_data_cell:first').click();
assert.strictEqual($('.modal').length, 1,
"second dialog should be closed");
assert.strictEqual($('.modal .o_data_row').length, 1,
"there should be one record in the one2many in the dialog");
assert.notOk($('.modal .o_x2m_control_panel .o_cp_pager div').is(':visible'),
'm2m pager should be hidden');
// click on 'Save & Close'
$('.modal .modal-footer .btn-primary:first').click();
assert.strictEqual($('.modal').length, 0, "dialog should be closed");
// reopen o2m record, and another m2m subrecord in its relation, but
// discard the changes
form.$('.o_data_row').click();
assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
assert.strictEqual($('.modal .o_data_row').length, 1,
"there should be one record in the one2many in the dialog");
// add another m2m subrecord
$('.modal .o_field_x2many_list_row_add a').click();
assert.strictEqual($('.modal').length, 2,
"should have opened a second dialog");
$('.modal:nth(1) .o_list_view .o_data_cell:first').click();
assert.strictEqual($('.modal').length, 1,
"second dialog should be closed");
assert.strictEqual($('.modal .o_data_row').length, 2,
"there should be two records in the one2many in the dialog");
// click on 'Discard'
$('.modal .modal-footer .btn-default').click();
assert.strictEqual($('.modal').length, 0, "dialog should be closed");
// reopen o2m record to check that second changes have properly been discarded
form.$('.o_data_row').click();
assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
assert.strictEqual($('.modal .o_data_row').length, 1,
"there should be one record in the one2many in the dialog");
form.destroy();
});
QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) { QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) {
var done = assert.async(); var done = assert.async();
assert.expect(20); assert.expect(20);
@@ -8017,6 +8115,80 @@ QUnit.module('relational_fields', {
form.destroy(); form.destroy();
}); });
QUnit.test('one2many with several pages, onchange and default order', function (assert) {
// This test reproduces a specific scenario where a one2many is displayed
// over several pages, and has a default order such that a record that
// would normally be on page 1 is actually on another page. Moreover,
// there is an onchange on that one2many which converts all commands 4
// (LINK_TO) into commands 1 (UPDATE), which is standard in the ORM.
// This test ensures that the record displayed on page 2 is never fully
// read.
assert.expect(8);
var data = this.data;
data.partner.records[0].turtles = [1, 2, 3];
data.turtle.records[0].partner_ids = [1];
data.partner.onchanges = {
turtles: function (obj) {
var res = _.map(obj.turtles, function (command) {
if (command[0] === 1) { // already an UPDATE command: do nothing
return command;
}
// convert LINK_TO commands to UPDATE commands
var id = command[1];
var record = _.findWhere(data.turtle.records, {id: id});
return [1, id, _.pick(record, ['turtle_int', 'turtle_foo', 'partner_ids'])];
});
obj.turtles = [[5]].concat(res);
},
};
var form = createView({
View: FormView,
model: 'partner',
data: data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="top" limit="2" default_order="turtle_foo">' +
'<field name="turtle_int"/>' +
'<field name="turtle_foo" class="foo"/>' +
'<field name="partner_ids" widget="many2many_tags"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
var ids = args.method === 'read' ? ' [' + args.args[0] + ']' : '';
assert.step(args.method + ids);
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_data_cell.foo').text(), 'blipkawa',
"should display two records out of three, in the correct order");
// edit turtle_int field of first row
form.$('.o_data_cell:first').click();
form.$('.o_field_widget[name=turtle_int]').val(3).trigger('input');
form.$el.click(); // leave edition
assert.strictEqual(form.$('.o_data_cell.foo').text(), 'blipkawa',
"should still display the same two records");
assert.verifySteps([
'read [1]', // main record
'read [1,2,3]', // one2many (turtle_foo, all records)
'read [2,3]', // one2many (all fields in view, records of first page)
'read [2,4]', // many2many inside one2many (partner_ids), first page only
'onchange',
]);
form.destroy();
});
QUnit.test('new record, with one2many with more default values than limit', function (assert) { QUnit.test('new record, with one2many with more default values than limit', function (assert) {
assert.expect(2); assert.expect(2);
@@ -565,9 +565,13 @@ var MockServer = Class.extend({
return record.display_name.indexOf(str) !== -1; return record.display_name.indexOf(str) !== -1;
}); });
} }
return _.map(records, function (record) { var result = _.map(records, function (record) {
return [record.id, record.display_name]; return [record.id, record.display_name];
}); });
if (args.limit) {
return result.slice(0, args.limit);
}
return result;
}, },
/** /**
* Simulate an 'onchange' rpc * Simulate an 'onchange' rpc
@@ -669,6 +669,42 @@ QUnit.module('Views', {
model.destroy(); model.destroy();
}); });
QUnit.test('default_get returning a non requested field', function (assert) {
// 'default_get' returns a default value for the fields given in
// arguments. It should not return a value for fields that have not be
// requested. However, it happens (e.g. res.users), and the webclient
// should not crash when this situation occurs (the field should simply
// be ignored).
assert.expect(2);
this.params.context = {};
this.params.fieldNames = ['category'];
this.params.res_id = undefined;
this.params.type = 'record';
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
var result = this._super(route, args);
if (args.method === 'default_get') {
result.product_ids = [[6, 0, [37, 41]]];
}
return result;
},
});
model.load(this.params).then(function (resultID) {
var record = model.get(resultID);
assert.ok('category' in record.data,
"should have processed 'category'");
assert.notOk('product_ids' in record.data,
"should have ignored 'product_ids'");
});
model.destroy();
});
QUnit.test('can make a default_record with default relational values', function (assert) { QUnit.test('can make a default_record with default relational values', function (assert) {
assert.expect(7); assert.expect(7);
@@ -66,6 +66,41 @@ QUnit.module('Views', {
parent.destroy(); parent.destroy();
}); });
QUnit.test('formviewdialog buttons in footer are not duplicated', function (assert) {
assert.expect(2);
this.data.partner.fields.poney_ids = {string: "Poneys", type: "one2many", relation: 'partner'};
this.data.partner.records[0].poney_ids = [];
var parent = createParent({
data: this.data,
archs: {
'partner,false,form':
'<form string="Partner">' +
'<field name="poney_ids"><tree editable="top"><field name="display_name"/></tree></field>' +
'<footer><button string="Custom Button" type="object" class="btn-primary"/></footer>' +
'</form>',
},
});
new dialogs.FormViewDialog(parent, {
res_model: 'partner',
res_id: 1,
}).open();
assert.strictEqual($('div.modal button.btn-primary').length, 1,
"should have 1 buttons in modal");
$('.o_field_x2many_list_row_add a').click();
$('input.o_input').trigger($.Event('keydown', {
which: $.ui.keyCode.ESCAPE,
keyCode: $.ui.keyCode.ESCAPE,
}));
assert.strictEqual($('div.modal button.btn-primary').length, 1,
"should still have 1 buttons in modal");
parent.destroy();
});
QUnit.test('SelectCreateDialog use domain, group_by and search default', function (assert) { QUnit.test('SelectCreateDialog use domain, group_by and search default', function (assert) {
assert.expect(3); assert.expect(3);

Some files were not shown because too many files have changed in this diff Show More