[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 *.csv
recursive-include odoo *.doc
recursive-include odoo *.docx
recursive-include odoo *.eml
recursive-include odoo *.eot
recursive-include odoo *.gif
@@ -16,6 +17,8 @@ recursive-include odoo *.less
recursive-include odoo *.md
recursive-include odoo *.mp3
recursive-include odoo *.ogg
recursive-include odoo *.ods
recursive-include odoo *.odt
recursive-include odoo *.otf
recursive-include odoo *.pdf
recursive-include odoo *.png
@@ -35,8 +38,10 @@ recursive-include odoo *.wsdl
recursive-include odoo *.xls
recursive-include odoo *.xsd
recursive-include odoo *.xsl
recursive-include odoo *.xlsx
recursive-include odoo *.xml
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_reports *.xslt
recursive-exclude * *.py[co]
+1 -1
View File
@@ -14,7 +14,7 @@
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="xml">
<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>Best regards,
% if user and user.signature:
+5 -1
View File
@@ -443,7 +443,7 @@ class AccountJournal(models.Model):
for journal in self:
if journal.sequence_id and journal.sequence_number_next:
sequence = journal.sequence_id._get_current_sequence()
sequence.number_next = journal.sequence_number_next
sequence.sudo().number_next = journal.sequence_number_next
@api.multi
# 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 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.'))
if self.bank_account_id:
self.bank_account_id.company_id = vals['company_id']
if ('code' in vals and journal.code != vals['code']):
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.'))
@@ -538,6 +540,8 @@ class AccountJournal(models.Model):
self.default_debit_account_id.currency_id = vals['currency_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']
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'):
raise UserError(_('You cannot remove the bank account from the journal once set.'))
result = super(AccountJournal, self).write(vals)
@@ -340,7 +340,7 @@ class AccountBankStatement(models.Model):
def link_bank_to_partner(self):
for statement in self:
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
@@ -341,12 +341,13 @@ class account_journal(models.Model):
ctx.update({
'journal_type': self.type,
'default_journal_id': self.id,
'search_default_journal_id': self.id,
'default_type': invoice_type,
'type': invoice_type
})
[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['domain'] = self._context.get('use_domain', [])
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)
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_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(
'account.partial.reconcile',
string='Tax Cash Basis Entry of',
@@ -1361,7 +1361,7 @@ class AccountMoveLine(models.Model):
record.payment_id.state = 'reconciled'
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()
for line in self:
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['currency_id'] = currency_id
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
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['amount_currency'] -= amount_currency_wo
@@ -526,17 +526,37 @@ var StatementModel = BasicModel.extend({
*/
togglePartialReconcile: function (handle) {
var line = this.getLine(handle);
var props = _.filter(line.reconciliation_proposition, {'invalid': false});
var prop = props[0];
if (props.length !== 1 || Math.abs(line.st_line.amount) >= Math.abs(prop.amount)) {
// Retrieve the toggle proposition
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();
}
prop.partial_reconcile = !prop.partial_reconcile;
if (!prop.partial_reconcile) {
// Inverse partial_reconcile value
selected.partial_reconcile = !selected.partial_reconcile;
if (!selected.partial_reconcile) {
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 () {
if (prop.partial_reconcile) {
if (selected.partial_reconcile) {
line.balance.amount = 0;
line.balance.type = 1;
line.mode = 'inactive';
@@ -646,6 +666,7 @@ var StatementModel = BasicModel.extend({
return isNaN(prop.id);
}), self._formatToProcessReconciliation.bind(self, line)),
};
// If the lines are not fully balanced, create an unreconciled amount.
// line.st_line.currency_id is never false here because its equivalent to
// statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side).
@@ -1078,8 +1099,9 @@ var StatementModel = BasicModel.extend({
// Do not forward port in master. @CSN will change this
var amount = prop.computed_with_tax && -prop.base_amount || -prop.amount;
if (prop.partial_reconcile === true) {
amount = -line.st_line.amount;
amount = -prop.write_off_amount;
}
var result = {
name : prop.label,
debit : amount > 0 ? amount : 0,
@@ -353,7 +353,21 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
// reconciliation_proposition
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) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
@@ -361,18 +375,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
.appendTo($line.find('.cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
}
if ((state.balance.amount_currency !== 0 || line.partial_reconcile) && props.length === 1 &&
line.already_paid === false &&
(
(state.st_line.amount > 0 && state.st_line.amount < props[0].amount) ||
(state.st_line.amount < 0 && state.st_line.amount > props[0].amount))
) {
if (line.already_paid === false &&
((state.balance.amount_currency < 0 || line.partial_reconcile) && nb_credit_props == 1
&& 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
&& line.amount < 0 && state.st_line.amount < 0 && state.st_line.amount > line.amount)) {
var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left');
var text;
if (line.partial_reconcile) {
text = _t("Undo the partial reconciliation.");
$cell.text(state.st_line.amount_str);
$cell.text(line.write_off_amount_str);
} 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.");
}
@@ -414,20 +426,24 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
this._renderCreate(state);
}
var data = this.model.get(this.handleCreateRecord).data;
this.model.notifyChanges(this.handleCreateRecord, state.createForm);
this.model.notifyChanges(this.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}});
_.each(state.createForm.analytic_tag_ids, function (tag) {
self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}});
});
var record = this.model.get(this.handleCreateRecord);
_.each(this.fields, function (field, fieldName) {
if (self._avoidFieldUpdate[fieldName]) return;
if (fieldName === "partner_id") return;
if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
field.reset(record);
}
this.model.notifyChanges(this.handleCreateRecord, state.createForm).then(function () {
// 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: []}}).then(function (){
var defs = [];
_.each(state.createForm.analytic_tag_ids, function (tag) {
defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}}));
});
$.when.apply($, defs).then(function () {
var record = self.model.get(self.handleCreateRecord);
_.each(self.fields, function (field, fieldName) {
if (self._avoidFieldUpdate[fieldName]) return;
if (fieldName === "partner_id") return;
if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
field.reset(record);
}
});
});
});
});
}
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
@@ -639,7 +655,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
return;
}
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();
}
return;
@@ -34,17 +34,22 @@ var db = {
fields: {
id: {string: "ID", type: 'integer'},
code: {string: "code", type: 'integer'},
display_name: {string: "Displayed name", type: 'char'},
name: {string: "Displayed name", type: 'char'},
},
records: [
{id: 282, code: 100000, display_name: "100000 Fixed Asset Account"},
{id: 283, code: 101000, display_name: "101000 Current Assets"},
{id: 284, code: 101110, display_name: "101110 Stock Valuation Account"},
{id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)"},
{id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)"},
{id: 287, code: 101200, display_name: "101200 Account Receivable"},
{id: 288, code: 101300, display_name: "101300 Tax Paid"},
{id: 308, code: 101401, display_name: "101401 Bank"},
{id: 282, code: 100000, name: "100000 Fixed Asset Account"},
{id: 283, code: 101000, name: "101000 Current Assets"},
{id: 284, code: 101110, name: "101110 Stock Valuation Account"},
{id: 285, code: 101120, name: "101120 Stock Interim Account (Received)"},
{id: 286, code: 101130, name: "101130 Stock Interim Account (Delivered)"},
{id: 287, code: 101200, name: "101200 Account Receivable"},
{id: 288, code: 101300, name: "101300 Tax Paid"},
{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 () {
return $.when();
@@ -776,13 +781,7 @@ QUnit.module('account', {
partner_id: false,
counterpart_aml_dicts:[],
payment_aml_ids: [392],
new_aml_dicts: [
{
"credit": 343.42,
"debit": 0,
"name": "Bank fees : Open balance"
}
],
new_aml_dicts: [],
}]
], "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");
widget.$('.accounting_view thead td: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.$('.text-danger:not(hidden)').length, "should display counterpart alert");
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.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should not display the partial reconciliation information");
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_true'), "should display the partial reconciliation information");
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');
clientAction.destroy();
@@ -1036,13 +1035,77 @@ QUnit.module('account', {
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) {
assert.expect(13);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
'data': this.params.data,
data: this.params.data,
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
});
clientAction.appendTo($('#qunit-fixture'));
@@ -1054,26 +1117,26 @@ QUnit.module('account', {
widget.$('.create .create_label input').val('test1').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_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");
widget.$('.create .create_tax_id input').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_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 tbody tr').length, 2, "should have 2 created reconcile lines");
widget.$('.create .create_tax_id input').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_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");
clientAction.destroy();
@@ -1085,7 +1148,7 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
'data': this.params.data,
data: this.params.data,
});
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': 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="journal_id" />
<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="refunds" string="Credit Notes" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_refund','in_refund'))]"/>
<separator/>
<filter name="unpaid" string="Not Paid" domain="[('state','=','open')]"/>
<filter name="unpaid" string="Open" domain="[('state', '=', 'open')]"/>
<filter name="paid" string="Paid" domain="[('state', '=', 'paid')]"/>
<filter name="late" string="Overdue" domain="['&amp;', ('date_due', '&lt;', time.strftime('%%Y-%%m-%%d')), ('state', '=', 'open')]" help="Overdue invoices, maturity date passed"/>
<separator/>
<field name="partner_id" operator="child_of"/>
@@ -574,33 +572,25 @@
</field>
</record>
<!-- TODO: remove in master -->
<record id="view_account_invoice_filter_inherit_invoices" model="ir.ui.view">
<field name="name">account.invoice.select.invoices</field>
<field name="model">account.invoice</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="view_account_invoice_filter"/>
<field name="arch" type="xml">
<data>
<xpath expr="filter[@name='invoices']" position="attributes">
<attribute name="string">Not Draft</attribute>
</xpath>
<filter name="refunds" position="replace"/>
</data>
<data/>
</field>
</record>
<!-- TODO: remove in master -->
<record id="view_account_invoice_filter_inherit_credit_notes" model="ir.ui.view">
<field name="name">account.invoice.select.credit.notes</field>
<field name="model">account.invoice</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="view_account_invoice_filter"/>
<field name="arch" type="xml">
<data>
<xpath expr="filter[@name='refunds']" position="attributes">
<attribute name="string">Not Draft</attribute>
</xpath>
<filter name="invoices" position="replace"/>
</data>
<data/>
</field>
</record>
@@ -671,7 +661,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','out_invoice')]</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">
<p class="o_view_nocontent_smiling_face">
Create a customer invoice
@@ -706,7 +696,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','out_refund')]</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">
<p class="o_view_nocontent_smiling_face">
Create a credit note
@@ -744,7 +734,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_invoice')]</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">
<p class="o_view_nocontent_smiling_face">
Record a new vendor bill
@@ -780,7 +770,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_refund')]</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">
<p class="o_view_nocontent_smiling_face">
Create a vendor credit note
@@ -117,16 +117,12 @@
<span>View</span>
</div>
<div>
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_invoice','in_invoice'))]}">
<span t-if="journal_type == 'sale'">Invoices</span>
<span t-if="journal_type == 'purchase'">Bills</span>
</a>
<a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_invoice')]}">Invoices</a>
<a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_invoice')]}">Bills</a>
</div>
<div>
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_refund','in_refund'))], 'invoice_type': 'refund'}">
<span t-if="journal_type == 'sale'">Credit Notes</span>
<span t-if="journal_type == 'purchase'">Credit Notes</span>
</a>
<a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
<a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
</div>
<div>
<a type="object" name="action_open_reconcile">Payments Matching</a>
+1
View File
@@ -56,6 +56,7 @@
id="action_report_financial"
model="account.financial.report"
string="Financial report"
menu="False"
report_type="qweb-pdf"
name="account.report_financial"
file="account.report_financial"
+1 -1
View File
@@ -16,7 +16,7 @@
<field name="code" placeholder="code"/>
<field name="name"/>
<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="group_id"/>
<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')
def _onchange_product_id(self):
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)
self.account_analytic_id = rec.analytic_id.id
self.analytic_tag_ids = rec.analytic_tag_ids.ids
@@ -75,7 +75,7 @@ class AccountInvoiceLine(models.Model):
def _set_additional_fields(self, invoice):
if not self.account_analytic_id or not self.analytic_tag_ids:
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)
if rec:
if self.account_analytic_id:
+9
View File
@@ -11,3 +11,12 @@ def uninstall_hook(cr, registry):
('module', '=', 'account')
]).mapped('res_id')
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,
'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 res_partner
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_mode': 'form',
'res_model': 'res.config.settings',
'target': 'current',
'target': 'inline',
'context': {'module' : 'general_settings'},
}
@@ -89,6 +89,7 @@ _map_iban_template = {
'bg': 'BGkk BBBB SSSS DDCC CCCC CC', # Bulgaria
'bh': 'BHkk BBBB CCCC CCCC CCCC CC', # Bahrain
'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
'cr': 'CRkk BBBC CCCC CCCC CCCC C', # Costa Rica
'cy': 'CYkk BBBS SSSS CCCC CCCC CCCC CCCC', # Cyprus
+3 -1
View File
@@ -302,7 +302,8 @@ class Import(models.TransientModel):
try:
thousand_separator = decimal_separator = False
for val in preview_values:
if val == '':
val = val.strip()
if not val:
continue
# value might have the currency symbol left or right from the value
val = self._remove_currency_symbol(val)
@@ -587,6 +588,7 @@ class Import(models.TransientModel):
thousand_separator = options.get('float_thousand_separator', ' ')
decimal_separator = options.get('float_decimal_separator', '.')
for line in data:
line[index] = line[index].strip()
if not line[index]:
continue
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
studio in it, False if none of the records do
"""
path = os.path.join(path, 'data')
filenames = next(iter(os.walk(path)))[2]
filenames = [f for f in filenames if f.lower().endswith('.xml')]
filepaths = []
for level in os.walk(path):
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:
root = lxml.etree.parse(os.path.join(path, filename)).getroot()
for fp in filepaths:
root = lxml.etree.parse(fp).getroot()
for record in root:
# 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')
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')
module_crm_phone_validation = fields.Boolean("Phone Validation")
module_crm_phone_validation = fields.Boolean("Phone Formatting")
module_web_clearbit = fields.Boolean("Customer Autocomplete")
def _find_default_lead_alias_id(self):
@@ -33,7 +33,7 @@ class ResConfigSettings(models.TransientModel):
@api.onchange('generate_lead_from_alias')
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
def get_values(self):
@@ -62,7 +62,7 @@
<div class="o_setting_right_pane">
<label for="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>
+4 -1
View File
@@ -210,8 +210,11 @@ class DeliveryCarrier(models.Model):
carrier.product_id.list_price = carrier.fixed_price
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,
'price': self.fixed_price,
'price': price,
'error_message': False,
'warning_message': False}
+10 -12
View File
@@ -84,18 +84,6 @@
</tr>
<tr>
<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;">
<tr>
<td style="padding:25px 0px;vertical-align:top;">
@@ -165,6 +153,16 @@
</tr>
</table>
% 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>
</tr>
</tbody>
+3 -1
View File
@@ -247,9 +247,11 @@ class FleetVehicle(models.Model):
@return: the costs log view
"""
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.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)]
)
return res
+2 -1
View File
@@ -190,7 +190,8 @@ class HrPayslip(models.Model):
leave_time = (interval[1] - interval[0]).seconds / 3600
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)
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
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.mobile + contract.mobile_plus) \
+ 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
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)
@@ -116,8 +116,8 @@ class HrContract(models.Model):
@api.depends('commission_on_target')
def _compute_warrants_cost(self):
for contract in self:
contract.warrants_cost = contract.commission_on_target * 1.05 * 12.0
contract.warrant_value_employee = contract.commission_on_target * (1.00 - 0.535) * 12.0
contract.warrants_cost = contract.commission_on_target * 1.326 * 1.05 * 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',
'internet', 'mobile', 'mobile_plus')
@@ -2836,98 +2836,98 @@
<record id="pcg_501" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Parts dans entreprises liées</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"/>
</record>
<record id="pcg_502" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Actions propres</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"/>
</record>
<record id="pcg_5031" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Titres cotés</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"/>
</record>
<record id="pcg_5035" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Titres non cotés</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"/>
</record>
<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="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"/>
</record>
<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="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"/>
</record>
<record id="pcg_5061" model="account.account.template">
<field name="name">Obligations cotés</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"/>
</record>
<record id="pcg_5065" model="account.account.template">
<field name="name">Obligations non cotés</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"/>
</record>
<record id="pcg_507" model="account.account.template">
<field name="name">Bons du Trésor et bons de caisse à court terme</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"/>
</record>
<record id="pcg_5081" model="account.account.template">
<field name="name">Autres valeurs mobilières de placement</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"/>
</record>
<record id="pcg_5082" model="account.account.template">
<field name="name">Bons de souscription</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"/>
</record>
<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="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"/>
</record>
<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="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"/>
</record>
<record id="pcg_5111" model="account.account.template">
<field name="name">Coupons échus à l'encaissement</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="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2935,7 +2935,7 @@
<record id="pcg_5112" model="account.account.template">
<field name="name">Chèques à encaisser</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="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2943,7 +2943,7 @@
<record id="pcg_5113" model="account.account.template">
<field name="name">Effets à l'encaissement</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="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2951,7 +2951,7 @@
<record id="pcg_5114" model="account.account.template">
<field name="name">Effets à l'escompte</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="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2980,56 +2980,56 @@
<record id="pcg_516" model="account.account.template">
<field name="name">Sociétés de bourse</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"/>
</record>
<record id="pcg_517" model="account.account.template">
<field name="name">Autres organismes financiers</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"/>
</record>
<record id="pcg_5181" model="account.account.template">
<field name="name">Intérêts courus à payer</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"/>
</record>
<record id="pcg_5188" model="account.account.template">
<field name="name">Intérêts courus à recevoir</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"/>
</record>
<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="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"/>
</record>
<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="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"/>
</record>
<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="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"/>
</record>
<record id="pcg_52" model="account.account.template">
<field name="name">Instruments de trésorerie</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"/>
</record>
@@ -3064,35 +3064,35 @@
<record id="pcg_54" model="account.account.template">
<field name="name">Régies d'avances et accréditifs</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"/>
</record>
<record id="pcg_5903" model="account.account.template">
<field name="name">Provisions pour dépréciation des actions</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"/>
</record>
<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="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"/>
</record>
<record id="pcg_5906" model="account.account.template">
<field name="name">Provisions pour dépréciation des obligations</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"/>
</record>
<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="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"/>
</record>
+4
View File
@@ -18,6 +18,10 @@
<field name="name">DIOT: 16%</field>
<field name="applicability">taxes</field>
</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">
<field name="name">DIOT: 0%</field>
<field name="applicability">taxes</field>
+12 -5
View File
@@ -5,18 +5,25 @@
<!-- Smileys -->
<record id="mail_smiley_normal_smile" 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="unicode_source">🙂</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png' alt='🙂'/&gt;</field>
<field name="description">Smile</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_smile" model="mail.shortcode">
<field name="source">:D</field>
<field name="unicode_source">😄</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png' alt='😄'/&gt;</field>
<field name="unicode_source">😀</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="shortcode_type">image</field>
</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">
<field name="source">xD</field>
<field name="unicode_source">😆</field>
@@ -25,7 +32,7 @@
<field name="shortcode_type">image</field>
</record>
<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="substitution">&lt;img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/&gt;</field>
<field name="description">Tears of joy</field>
+1 -1
View File
@@ -56,7 +56,7 @@ class Channel(models.Model):
('channel', 'Channel')],
'Channel Type', default='channel')
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)
# multi users channel
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) {
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.DOWN:
case $.ui.keyCode.TAB:
+34 -1
View File
@@ -17,6 +17,7 @@ var DocumentViewer = Widget.extend({
'click .o_viewer_video': '_onVideoClicked',
'click .move_next': '_onNext',
'click .move_previous': '_onPrevious',
'click .o_rotate': '_onRotate',
'click .o_zoom_in': '_onZoomIn',
'click .o_zoom_out': '_onZoomOut',
'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._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
*
@@ -111,7 +136,7 @@ var DocumentViewer = Widget.extend({
*/
_zoom: function (scale) {
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;
}
},
@@ -280,6 +305,14 @@ var DocumentViewer = Widget.extend({
videoElement.pause();
}
},
/**
* @private
* @param {MouseEvent} e
*/
_onRotate: function (e) {
e.preventDefault();
this._rotate(90);
},
/**
* @private
* @param {MouseEvent} e
@@ -264,6 +264,7 @@
img {
width: @o-mail-attachment-image-size;
height: @o-mail-attachment-image-size;
image-orientation: from-image; // Only supported in Firefox
}
.o_image_overlay{
display: none;
@@ -365,6 +366,9 @@
.o-flex-display();
justify-content: center;
align-items: center;
img {
image-orientation: from-image; // Only supported in Firefox
}
img, video {
max-width: 100%;
max-height: 100%;
+5
View File
@@ -50,6 +50,11 @@
</div>
<div t-if="widget.activeAttachment.type == 'image'" class="o_viewer_toolbar">
<ul class="list-inline">
<li>
<a class="o_rotate" href="#">
<i class="fa fa-2x fa-repeat"></i>
</a>
</li>
<li>
<a class="o_zoom_out" href="#">
<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.")
# Formerly associated to `generate_and_pay_invoice` option from auto_confirm
journal_id = fields.Many2one(
'account.journal', 'Payment Journal', domain=[('type', '=', 'bank')],
default=lambda self: self.env['account.journal'].search([('type', '=', 'bank')], limit=1),
'account.journal', 'Payment Journal', domain=[('type', 'in', ['bank', 'cash'])],
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,
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
@@ -200,22 +200,13 @@ class PaymentAcquirer(models.Model):
@api.model
def create(self, vals):
image_resize_images(vals)
vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).create(vals)
@api.multi
def write(self, vals):
image_resize_images(vals)
vals = self._check_journal_id(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
def toggle_website_published(self):
self.write({'website_published': not self.website_published})
+1 -1
View File
@@ -81,7 +81,7 @@
</page>
<page string="Configuration">
<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="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/>
<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_ogone.controllers.main import OgoneController
from odoo.addons.payment_ogone.data import ogone
from odoo.tools import float_round, DEFAULT_SERVER_DATE_FORMAT
from odoo.tools.float_utils import float_compare, float_repr
from odoo.tools.safe_eval import safe_eval
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, ustr
from odoo.tools.float_utils import float_compare, float_repr, float_round
_logger = logging.getLogger(__name__)
@@ -476,7 +475,7 @@ class PaymentTxOgone(models.Model):
self.write({
'state': new_state,
'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:
time.sleep(0.5)
@@ -585,4 +584,4 @@ class PaymentToken(models.Model):
'acquirer_ref': alias,
'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="list_price">2.83</field>
<field name="name">Orange Butterfly</field>
<field name="taxes_id">False</field>
<field name="pos_categ_id" ref="fruits_vegetables"/>
<field name="to_weight">True</field>
<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
if moves and not return_picking and not order_picking:
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()
return True
+11 -5
View File
@@ -1305,6 +1305,7 @@ exports.Orderline = Backbone.Model.extend({
this.type = 'unit';
this.selected = false;
this.id = orderline_id++;
this.price_manually_set = false;
if (options.price) {
this.set_unit_price(options.price);
@@ -1345,6 +1346,7 @@ exports.Orderline = Backbone.Model.extend({
orderline.price = this.price;
orderline.type = this.type;
orderline.selected = false;
orderline.price_manually_set = this.price_manually_set;
return orderline;
},
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
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.order.fix_tax_included_price(this);
}
@@ -2232,7 +2234,11 @@ exports.Order = Backbone.Model.extend({
set_pricelist: function (pricelist) {
var self = this;
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()));
self.fix_tax_included_price(line);
});
@@ -2355,7 +2361,7 @@ exports.Order = Backbone.Model.extend({
this.assert_editable();
var newPaymentline = new exports.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos});
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.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(){
return this.get_due() === 0;
return this.get_due() <= 0;
},
is_paid_with_cash: function(){
return !!this.paymentlines.find( function(pl){
+3 -12
View File
@@ -502,7 +502,9 @@ var OrderWidget = PosBaseWidget.extend({
}else if( mode === 'discount'){
order.get_selected_orderline().set_discount(val);
}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;
}
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) {
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",
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",
trigger: ".control-button.o_pricelist_button",
@@ -173,8 +185,9 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
content: "select public pricelist",
trigger: ".selection-item:contains('Public Pricelist')",
}, {
content: "verify that order has been recomputed",
trigger: ".total > .value:contains('$ 3.96')",
content: "verify that the boni oranges have been recomputed and the\
butterfly oranges have not (their price was manually overriden)",
trigger: ".total > .value:contains('$ 8.96')",
}, {
content: "click 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>
</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>
@@ -6,6 +6,7 @@ var ajax = require('web.ajax');
var core = require('web.core');
var Widget = require('web.Widget');
var rpc = require('web.rpc');
var time = require('web.time');
var qweb = core.qweb;
var _t = core._t;
@@ -105,7 +106,7 @@ var PortalChatter = Widget.extend({
preprocessMessages: function(messages){
_.each(messages, function(m){
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;
},
+7 -6
View File
@@ -7,17 +7,18 @@ from odoo import api, fields, models
class PosConfig(models.Model):
_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.')
discount_pc = fields.Float(string='Discount Percentage', default=10, 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_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), ('sale_ok', '=', True)]", help='The product used to model the discount.')
@api.onchange('module_pos_discount')
def _onchange_module_pos_discount(self):
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
else:
self.discount_product_id = False
@@ -23,6 +23,13 @@ var DiscountButton = screens.ActionButtonWidget.extend({
var order = this.pos.get_order();
var lines = order.get_orderlines();
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
var i = 0;
+88 -77
View File
@@ -146,82 +146,82 @@ var lookUpCodeTransaction = {
'000000': _t('Transaction approved'),
},
'TimeoutError': {
'001006': _t('Global API Not Initialized'),
'001007': _t('Timeout on Response'),
'003003': _t('Socket Error sending request'),
'003004': _t('Socket already open or in use'),
'003005': _t('Socket Creation Failed'),
'003006': _t('Socket Connection Failed'),
'003007': _t('Connection Lost'),
'003008': _t('TCP/IP Failed to Initialize'),
'003010': _t('Time Out waiting for server response'),
'003011': _t('Connect Canceled'),
'003053': _t('Initialize Failed'),
'009999': _t('Unknown Error'),
'001006': 'Global API Not Initialized',
'001007': 'Timeout on Response',
'003003': 'Socket Error sending request',
'003004': 'Socket already open or in use',
'003005': 'Socket Creation Failed',
'003006': 'Socket Connection Failed',
'003007': 'Connection Lost',
'003008': 'TCP/IP Failed to Initialize',
'003010': 'Time Out waiting for server response',
'003011': 'Connect Canceled',
'003053': 'Initialize Failed',
'009999': 'Unknown Error',
},
'FatalError': {
'-1': _t('Timeout error'),
'001001': _t('General Failure'),
'001003': _t('Invalid Command Format'),
'001004': _t('Insufficient Fields'),
'001011': _t('Empty Command String'),
'002000': _t('Password Verified'),
'002001': _t('Queue Full'),
'002002': _t('Password Failed – Disconnecting'),
'002003': _t('System Going Offline'),
'002004': _t('Disconnecting Socket'),
'002006': _t('Refused ‘Max Connections’'),
'002008': _t('Duplicate Serial Number Detected'),
'002009': _t('Password Failed (Client / Server)'),
'002010': _t('Password failed (Challenge / Response)'),
'002011': _t('Internal Server Error – Call Provider'),
'003002': _t('In Process with server'),
'003009': _t('Control failed to find branded serial (password lookup failed)'),
'003012': _t('128 bit CryptoAPI failed'),
'003014': _t('Threaded Auth Started Expect Response'),
'003017': _t('Failed to start Event Thread.'),
'003050': _t('XML Parse Error'),
'003051': _t('All Connections Failed'),
'003052': _t('Server Login Failed'),
'004001': _t('Global Response Length Error (Too Short)'),
'004002': _t('Unable to Parse Response from Global (Indistinguishable)'),
'004003': _t('Global String Error'),
'004004': _t('Weak Encryption Request Not Supported'),
'004005': _t('Clear Text Request Not Supported'),
'004010': _t('Unrecognized Request Format'),
'004011': _t('Error Occurred While Decrypting Request'),
'004017': _t('Invalid Check Digit'),
'004018': _t('Merchant ID Missing'),
'004019': _t('TStream Type Missing'),
'004020': _t('Could Not Encrypt Response- Call Provider'),
'100201': _t('Invalid Transaction Type'),
'100202': _t('Invalid Operator ID'),
'100203': _t('Invalid Memo'),
'100204': _t('Invalid Account Number'),
'100205': _t('Invalid Expiration Date'),
'100206': _t('Invalid Authorization Code'),
'100207': _t('Invalid Authorization Code'),
'100208': _t('Invalid Authorization Amount'),
'100209': _t('Invalid Cash Back Amount'),
'100210': _t('Invalid Gratuity Amount'),
'100211': _t('Invalid Purchase Amount'),
'100212': _t('Invalid Magnetic Stripe Data'),
'100213': _t('Invalid PIN Block Data'),
'100214': _t('Invalid Derived Key Data'),
'100215': _t('Invalid State Code'),
'100216': _t('Invalid Date of Birth'),
'100217': _t('Invalid Check Type'),
'100218': _t('Invalid Routing Number'),
'100219': _t('Invalid TranCode'),
'100220': _t('Invalid Merchant ID'),
'100221': _t('Invalid TStream Type'),
'100222': _t('Invalid Batch Number'),
'100223': _t('Invalid Batch Item Count'),
'100224': _t('Invalid MICR Input Type'),
'100225': _t('Invalid Driver’s License'),
'100226': _t('Invalid Sequence Number'),
'100227': _t('Invalid Pass Data'),
'100228': _t('Invalid Card Type'),
'-1': 'Timeout error',
'001001': 'General Failure',
'001003': 'Invalid Command Format',
'001004': 'Insufficient Fields',
'001011': 'Empty Command String',
'002000': 'Password Verified',
'002001': 'Queue Full',
'002002': 'Password Failed – Disconnecting',
'002003': 'System Going Offline',
'002004': 'Disconnecting Socket',
'002006': 'Refused ‘Max Connections’',
'002008': 'Duplicate Serial Number Detected',
'002009': 'Password Failed (Client / Server)',
'002010': 'Password failed (Challenge / Response)',
'002011': 'Internal Server Error – Call Provider',
'003002': 'In Process with server',
'003009': 'Control failed to find branded serial (password lookup failed)',
'003012': '128 bit CryptoAPI failed',
'003014': 'Threaded Auth Started Expect Response',
'003017': 'Failed to start Event Thread.',
'003050': 'XML Parse Error',
'003051': 'All Connections Failed',
'003052': 'Server Login Failed',
'004001': 'Global Response Length Error (Too Short)',
'004002': 'Unable to Parse Response from Global (Indistinguishable)',
'004003': 'Global String Error',
'004004': 'Weak Encryption Request Not Supported',
'004005': 'Clear Text Request Not Supported',
'004010': 'Unrecognized Request Format',
'004011': 'Error Occurred While Decrypting Request',
'004017': 'Invalid Check Digit',
'004018': 'Merchant ID Missing',
'004019': 'TStream Type Missing',
'004020': 'Could Not Encrypt Response- Call Provider',
'100201': 'Invalid Transaction Type',
'100202': 'Invalid Operator ID',
'100203': 'Invalid Memo',
'100204': 'Invalid Account Number',
'100205': 'Invalid Expiration Date',
'100206': 'Invalid Authorization Code',
'100207': 'Invalid Authorization Code',
'100208': 'Invalid Authorization Amount',
'100209': 'Invalid Cash Back Amount',
'100210': 'Invalid Gratuity Amount',
'100211': 'Invalid Purchase Amount',
'100212': 'Invalid Magnetic Stripe Data',
'100213': 'Invalid PIN Block Data',
'100214': 'Invalid Derived Key Data',
'100215': 'Invalid State Code',
'100216': 'Invalid Date of Birth',
'100217': 'Invalid Check Type',
'100218': 'Invalid Routing Number',
'100219': 'Invalid TranCode',
'100220': 'Invalid Merchant ID',
'100221': 'Invalid TStream Type',
'100222': 'Invalid Batch Number',
'100223': 'Invalid Batch Item Count',
'100224': 'Invalid MICR Input Type',
'100225': 'Invalid Driver’s License',
'100226': 'Invalid Sequence Number',
'100227': 'Invalid Pass Data',
'100228': 'Invalid Card Type',
},
};
// Popup to show all transaction state for the payment.
@@ -339,6 +339,15 @@ PaymentScreenWidget.include({
// Handler to manage the card reader string
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) {
return;
}
@@ -643,9 +652,11 @@ PaymentScreenWidget.include({
});
} else {
this._super(id);
order.selected_paymentline.mercury_swipe_pending = true;
this.render_paymentlines();
order.trigger('change', order); // needed so that export_to_JSON gets triggered
if (order.get_due(order.selected_paymentline) > 0) {
order.selected_paymentline.mercury_swipe_pending = true;
this.render_paymentlines();
order.trigger('change', order); // needed so that export_to_JSON gets triggered
}
}
} else {
this._super(id);
+1 -1
View File
@@ -427,7 +427,7 @@ class ProductTemplate(models.Model):
]
# 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
# variant for if the value set isn't already a variant
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},
{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_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")
# 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'])]
@@ -23,10 +23,10 @@
font-weight: 800;
}
> a {
> div {
font-weight: 500;
&.o_needaction{
button.o_needaction {
font-size: small;
font-weight: 400;
margin-left: 4px;
@@ -139,8 +139,9 @@
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
<thead>
<tr class="active">
<th class="col-md-10">Task</th>
<th>Stage</th>
<th class="col-md-9">Task</th>
<th class="col-md-2">Stage</th>
<th>Ref</th>
</tr>
</thead>
<tbody>
@@ -152,6 +153,9 @@
<td>
<span class="label label-info" title="Current stage of the task" t-esc="task.stage_id.name" />
</td>
<td>
<span t-esc="task.id"/>
</td>
</tr>
</t>
</tbody>
@@ -171,7 +175,7 @@
<div class="col-md-12">
<h4>
<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"/>
<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>
+2 -2
View File
@@ -349,9 +349,9 @@
<div>
<span class="o_value"><t t-esc="record.task_count.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"/>
</a>
</button>
</div>
</a>
</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:
(RFQ origin: ${object.origin})
% 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}.
</p>
+1 -1
View File
@@ -351,7 +351,7 @@ class PurchaseOrder(models.Model):
@api.multi
def button_approve(self, force=False):
self.write({'state': 'purchase'})
self.write({'state': 'purchase', 'date_approve': fields.Date.context_today(self)})
self._create_picking()
if self.company_id.po_lock == 'lock':
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)]):
res[poline.orderpoint_id.id] += poline.product_uom._compute_quantity(poline.product_qty, poline.orderpoint_id.product_uom, round=False)
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="arch" type="xml">
<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}"/>
</field>
<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="arch" type="xml">
<div name="button_box" position="inside">
<button name="%(purchase.purchase_order_action_generic)d" type="action"
string="Purchases" class="oe_stat_button" icon="fa-refresh"
domain="[('orderpoint_id','=',active_id)]"/>
<button type="object"
name="action_view_purchase"
class="oe_stat_button"
string="Purchases"
icon="fa-refresh">
</button>
</div>
</field>
</record>
+1 -1
View File
@@ -743,7 +743,7 @@ class ResourceCalendarLeaves(models.Model):
date_from = fields.Datetime('Start Date', required=True)
date_to = fields.Datetime('End Date', required=True)
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 "
"localize leave hours when computing time intervals.")
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)
work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
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 {
'days': days_count,
'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):
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())
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
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
def uninstall_hook(cr, registry):
def update_dashboard_graph_model(dbname):
db_registry = odoo.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
env['account.invoice.report'].init()
if 'crm.team' in env:
recs = env['crm.team'].search([])
for rec in recs:
+1
View File
@@ -12,3 +12,4 @@ from . import sale
from . import res_config_settings
from . import sale_layout
from . import sales_team
from . import ir_model_fields
-10
View File
@@ -96,15 +96,6 @@ class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
_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.order.line',
'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_sequence = fields.Integer(string='Layout Sequence')
# 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
def _sales_count(self):
r = {}
if not self.user_has_groups('sales_team.group_sale_salesman'):
return r
domain = [
('state', 'in', ['sale', 'done']),
('product_id', 'in', self.ids),
+1 -1
View File
@@ -501,7 +501,7 @@ class SaleOrder(models.Model):
@api.returns('self', lambda value: value.id)
def message_post(self, **kwargs):
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)
@api.multi
+3 -2
View File
@@ -644,7 +644,7 @@
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<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" />
</button>
</button>
@@ -666,7 +666,8 @@
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<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" />
</button>
</button>
+9
View File
@@ -11,3 +11,12 @@ def uninstall_hook(cr, registry):
('module', '=', 'sale')
]).mapped('res_id')
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,
'uninstall_hook': 'uninstall_hook',
'post_init_hook': 'post_init_hook',
}
+2 -1
View File
@@ -92,7 +92,8 @@
<field name="arch" type="xml">
<button name="action_view_orderpoints" position="after">
<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" />
</button>
</button>
+1 -1
View File
@@ -141,7 +141,7 @@ class AccountAnalyticLine(models.Model):
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 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':
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>
</th>
<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 t-if="currency.position == 'after'" t-esc="currency.symbol"/>
<t t-esc="dashboard['money_amount']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Invoiced</td>
</tr>
@@ -68,9 +66,7 @@
</td>
<td>Fixed</td>
<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 t-if="currency.position == 'after'" t-esc="currency.symbol"/>
<t t-esc="dashboard['money_amount']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>To invoice</td>
</tr>
@@ -84,9 +80,7 @@
</td>
<td>No task found</td>
<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 t-if="currency.position == 'after'" t-esc="currency.symbol"/>
<t t-esc="dashboard['money_amount']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Cost</td>
</tr>
@@ -110,9 +104,7 @@
<td></td>
<td></td>
<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 t-if="currency.position == 'after'" t-esc="currency.symbol"/>
<t t-esc="dashboard['money_amount']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td><b>Total</b></td>
</tr>
+13 -1
View File
@@ -4,4 +4,16 @@
from . import controllers
from . import models
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,
'application': True,
'auto_install': False,
'pre_init_hook': 'pre_init_hook',
}
+6 -2
View File
@@ -11,10 +11,14 @@ class StockScrap(models.Model):
_order = 'id desc'
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):
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(
'Reference', default=lambda self: _('New'),
+1 -1
View File
@@ -117,7 +117,7 @@
</td>
<td class="text-right" t-if="move_line.state == '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>
<span t-field="move_line.qty_done"/>
<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:
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 we just compensated an IN move that has a negative remaining
# 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):
""" 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'
@@ -1429,9 +1430,6 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move1.account_move_ids), 1)
self.assertEqual(len(move2.account_move_ids), 1)
# ---------------------------------------------------------------------
# Ending
# ---------------------------------------------------------------------
self.assertEqual(self.product1.qty_available, -2)
self.assertEqual(self.product1.stock_value, -20)
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('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):
""" 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.
@@ -1008,7 +1008,10 @@ var FieldX2Many = AbstractField.extend({
* @param {OdooEvent} 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
@@ -103,7 +103,7 @@ var CrashManager = core.Class.extend({
if (!this.active) {
return;
}
new Dialog(this, {
return new Dialog(this, {
size: 'medium',
title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"),
subtitle: error.data.title,
@@ -149,10 +149,10 @@ var CrashManager = core.Class.extend({
clipboard.destroy();
});
dialog.open();
return dialog.open();
},
show_message: function(exception) {
this.show_error({
return this.show_error({
type: _t("Odoo Client Error"),
message: exception,
data: {debug: ""}
@@ -274,6 +274,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @returns {Deferred}
*/
_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);
return this.renderer.confirmChange(state, id, fields, e);
},
@@ -189,6 +189,90 @@ var BasicModel = AbstractModel.extend({
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
* view. Those fields might even be unknown when the onchange returns (e.g.
@@ -1397,6 +1481,7 @@ var BasicModel = AbstractModel.extend({
list = self._makeDataPoint({
fields: view ? view.fields : fieldInfo.relatedFields,
fieldsInfo: view ? view.fieldsInfo : fieldInfo.fieldsInfo,
limit: fieldInfo.limit,
modelName: field.relation,
parentID: record.id,
static: true,
@@ -3228,27 +3313,8 @@ var BasicModel = AbstractModel.extend({
context: params.context,
})
.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({
modelName: modelName,
data: data,
fields: params.fields,
fieldsInfo: params.fieldsInfo,
context: params.context,
@@ -3257,37 +3323,7 @@ var BasicModel = AbstractModel.extend({
viewType: params.viewType,
});
var defs = [];
_.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)
return self.applyDefaultValues(record.id, result)
.then(function () {
var def = $.Deferred();
self._performOnChange(record, fields_key).always(function () {
@@ -3470,13 +3506,17 @@ var BasicModel = AbstractModel.extend({
* @param {Object} record
* @param {string} fieldName
* @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}
*/
_processX2ManyCommands: function (record, fieldName, commands) {
_processX2ManyCommands: function (record, fieldName, commands, options) {
var self = this;
options = options || {};
var defs = [];
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 fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
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
// order field first to apply the order on all pages
if (list.res_ids.length > list.limit) {
if (!list.orderedResIDs && list.orderedBy.length) {
if (list.res_ids.length > list.limit && list.orderedBy.length) {
if (!list.orderedResIDs) {
var fieldNames = _.pluck(list.orderedBy, 'name');
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 () {
var resIDs = [];
@@ -161,7 +161,12 @@ var BasicView = AbstractView.extend({
// (because those fields were unknow at that time). So we ask
// the model to process them.
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, {
fieldNames: fieldNames,
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
this.model.setScale('week');
}
this.model.setDate(event.data.date, true);
this.model.setDate(event.data.date);
this.reload();
},
/**
@@ -216,9 +216,9 @@ return AbstractModel.extend({
filters: params.filters,
};
this.setDate(params.initialDate);
// Use mode attribute in xml file to specify zoom timeline (day,week,month)
// by default month.
this.setDate(params.initialDate, true);
this.setScale(params.mode);
_.each(this.data.filters, function (filter) {
@@ -251,9 +251,8 @@ return AbstractModel.extend({
},
/**
* @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.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.end_date = this.data.end_date.clone().endOf('day');
}
if (highlight) {
this.data.highlight_date = this.data.target_date;
}
},
setScale: function (scale) {
if (!_.contains(scales, scale)) {
@@ -422,11 +422,6 @@ return AbstractRenderer.extend({
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())
.find('.o_selected_range')
.removeClass('o_color o_selected_range');
@@ -40,7 +40,11 @@ return Dialog.extend({
this.domainSelector = new DomainSelector(this, model, domain, options);
},
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(
this._super.apply(this, arguments),
this.domainSelector.appendTo(this.$el)
@@ -68,6 +68,13 @@ var Notification = Widget.extend({
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
@@ -71,11 +71,6 @@
}
}
}
.o_target_date:not(.fc-today) {
background-color: @odoo-brand-primary;
opacity: 0.1;
}
}
.o_calendar_sidebar_container {
-1
View File
@@ -125,7 +125,6 @@
</div>
</t>
<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">
<span class="o_icon fa fa-3x fa-exclamation"/>
</t>
@@ -368,10 +368,6 @@ QUnit.module('relational_fields', {
"should have an open record button");
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,
"should still have an open record button");
@@ -3064,6 +3060,108 @@ QUnit.module('relational_fields', {
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) {
var done = assert.async();
assert.expect(20);
@@ -8017,6 +8115,80 @@ QUnit.module('relational_fields', {
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) {
assert.expect(2);
@@ -565,9 +565,13 @@ var MockServer = Class.extend({
return record.display_name.indexOf(str) !== -1;
});
}
return _.map(records, function (record) {
var result = _.map(records, function (record) {
return [record.id, record.display_name];
});
if (args.limit) {
return result.slice(0, args.limit);
}
return result;
},
/**
* Simulate an 'onchange' rpc
@@ -669,6 +669,42 @@ QUnit.module('Views', {
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) {
assert.expect(7);
@@ -66,6 +66,41 @@ QUnit.module('Views', {
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) {
assert.expect(3);

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