[MERGE] forward port branch 11.0 up to c2e878e34c
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(' ', ' ');
|
||||
|
||||
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'));
|
||||
|
||||
|
||||
@@ -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},
|
||||
])
|
||||
|
||||
@@ -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="['&', ('state','in',['draft','open','paid']),('type','in',('out_invoice','in_invoice'))]"/>
|
||||
<filter name="refunds" string="Credit Notes" domain="['&', ('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="['&', ('date_due', '<', 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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"><img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/></field>
|
||||
<field name="unicode_source">🙂</field>
|
||||
<field name="substitution"><img src='/mail/static/src/img/smiley/smile.png' alt='🙂'/></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"><img src='/mail/static/src/img/smiley/smile.png' alt='😄'/></field>
|
||||
<field name="unicode_source">😀</field>
|
||||
<field name="substitution"><img src='/mail/static/src/img/smiley/smile_open_mouth.png' alt='😀'/></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"><img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/></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"><img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/></field>
|
||||
<field name="description">Tears of joy</field>
|
||||
|
||||
@@ -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 |
@@ -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:
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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)]}"/>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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&model=project.task&id=%s&view_type=form' % (task.id)" groups="project.group_project_user">Edit Task</a>
|
||||
</h4>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -48,4 +48,5 @@ The Dashboard for the Sales Manager will include
|
||||
],
|
||||
'application': True,
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
'post_init_hook': 'post_init_hook',
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -74,4 +74,5 @@
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'pre_init_hook': 'pre_init_hook',
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user