[MERGE] forward port branch saas-11.2 up to 42659dc70c

This commit is contained in:
Christophe Simonis
2018-03-09 16:25:58 +01:00
251 changed files with 4690 additions and 1140 deletions
+5
View File
@@ -4,6 +4,7 @@ include README.md
recursive-include odoo *.css
recursive-include odoo *.csv
recursive-include odoo *.doc
recursive-include odoo *.docx
recursive-include odoo *.eml
recursive-include odoo *.eot
recursive-include odoo *.gif
@@ -16,6 +17,8 @@ recursive-include odoo *.less
recursive-include odoo *.md
recursive-include odoo *.mp3
recursive-include odoo *.ogg
recursive-include odoo *.ods
recursive-include odoo *.odt
recursive-include odoo *.otf
recursive-include odoo *.pdf
recursive-include odoo *.png
@@ -35,8 +38,10 @@ recursive-include odoo *.wsdl
recursive-include odoo *.xls
recursive-include odoo *.xsd
recursive-include odoo *.xsl
recursive-include odoo *.xlsx
recursive-include odoo *.xml
recursive-include odoo *.yml
recursive-include odoo *.zip
recursive-include odoo/addons/l10n_mx_edi *.xslt *.key *.cer *.txt
recursive-include odoo/addons/l10n_mx_reports *.xslt
recursive-exclude * *.py[co]
-1
View File
@@ -17,7 +17,6 @@ Core mechanisms for the accounting modules. To display the menuitems, install th
'security/ir.model.access.csv',
'data/data_account_type.xml',
'data/account_data.xml',
'data/ir_cron_data.xml',
'views/account_menuitem.xml',
'views/account_payment_view.xml',
'wizard/account_reconcile_view.xml',
+1
View File
@@ -11,6 +11,7 @@ from odoo.tools.misc import consteq
class MailController(MailController):
@classmethod
def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the invoice, but provided
# a valid access token, redirect him to the front-end view.
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Cron job for creating recurring vendor bills -->
<record id="recurrent_vendor_bills_cron" model="ir.cron">
<field name="name">Account: generate recurring vendor bills</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="state">code</field>
<field name="active">False</field>
<field name="code">model._cron_generate_recurring_vendor_bill()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
</record>
</odoo>
+1 -1
View File
@@ -14,7 +14,7 @@
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="xml">
<p>Dear ${object.partner_id.name},</p>
<p>Thank you for your payment.<br />Here is your payment receipt <strong>${(object.name or '').replace('/','-')}</strong> amounting to <strong>${object.amount} ${object.currency_id.name}</strong> from ${object.company_id.name}.</p>
<p>Thank you for your payment.<br />Here is your payment receipt <strong>${(object.name or '').replace('/','-')}</strong> amounting to <strong>${format_amount(object.amount, object.currency_id)}</strong> from ${object.company_id.name}.</p>
<p>If you have any questions, please do not hesitate to contact us.</p>
<p>Best regards,
% if user and user.signature:
+5 -1
View File
@@ -441,7 +441,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
@@ -523,6 +523,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.'))
@@ -536,6 +538,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)
+1 -64
View File
@@ -229,12 +229,6 @@ class AccountInvoice(models.Model):
payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id'))
self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines))
@api.depends('date_invoice', 'is_recurrency_enabled', 'recurrency_interval', 'recurrency_type')
def _compute_next_recurrency_date(self):
#initially set the recurrency_date
for invoice in self.filtered(lambda inv: inv.is_recurrency_enabled and inv.recurrency_interval and inv.recurrency_type and inv.date_invoice and not inv.next_recurrency_date):
invoice.next_recurrency_date = invoice._get_next_recurrency_date(invoice.date_invoice)
name = fields.Char(string='Reference/Description', index=True,
readonly=True, states={'draft': [('readonly', False)]}, copy=False, help='The name that will be used on account move lines')
@@ -373,18 +367,6 @@ class AccountInvoice(models.Model):
#fields use to set the sequence, on the first invoice of the journal
sequence_number_next = fields.Char(string='Next Number', compute="_get_sequence_number_next", inverse="_set_sequence_next")
sequence_number_next_prefix = fields.Char(string='Next Number Prefix', compute="_get_sequence_prefix")
#recurrent vendor bills fields
is_recurrency_enabled = fields.Boolean(string="Generate Recurring Document(s)", copy=False, help="Tick this box to generate recurring bills. The recurring bills will only be generated if this bill is validated.")
recurrency_interval = fields.Integer(string='Every', default=1, copy=False, help="Define the recurrency of the vendor bill.")
recurrency_type = fields.Selection([
('days', 'Day(s)'),
('weeks', 'Week(s)'),
('months', 'Month(s)'),
('years', 'Year(s)'),
], string="Recurrency Type", default='months', copy=False)
# Technical field depicting whether the document is generated by cron or by user
is_recurring_document = fields.Boolean(string="Recurring Document", copy=False)
next_recurrency_date = fields.Date(compute=_compute_next_recurrency_date, _string='Next Recurrency Date', copy=False, store=True)
_sql_constraints = [
('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
@@ -468,52 +450,6 @@ class AccountInvoice(models.Model):
self.type == 'in_refund' and self.state == 'draft' and _('Vendor Credit Note') or \
self.type == 'in_refund' and _('Vendor Credit Note - %s') % (self.number)
def _get_next_recurrency_date(self, current_date):
"""
This method is used to return next date based on recurrency configuration of the record.
:param current_date: Origin date from which next date is to be generated.
:returns: future date on which a duplicated document will be generated by crons.
"""
assert self.recurrency_type, "Invalid `Recurrency` interval! Please set it to either of these: 'Day(s)', 'Week(s)', 'Month(s)' or 'Year(s)'."
self.ensure_one()
recurring_next_date = fields.Date.from_string(current_date) + relativedelta(**{self.recurrency_type: self.recurrency_interval})
# In case of monthly recurrent invoice, future invoices should always be created on same day as of it's main invoice but
# if the reference invoice was made on last day of the month, then the recurring invoice should be created on last day of next month.
# example, main invoice is set to 31st Jan, the next invoices should be created on 28/29th Feb, 31st Mar, 30 Apr and so on
if self.recurrency_type == 'months':
reference_invoice_day = fields.Date.from_string(self.date_invoice).day
recurring_next_date += relativedelta(day=reference_invoice_day)
if reference_invoice_day == fields.Date.from_string(self.date_invoice) + relativedelta(day=31):
recurring_next_date += relativedelta(day=31)
return recurring_next_date
@api.model
def _cron_generate_recurring_vendor_bill(self):
today = fields.Date.today()
domain = [
('is_recurrency_enabled', '=', True),
('state', 'not in', ['draft', 'cancel']),
('next_recurrency_date', '<=', today)
]
for invoice in self.search(domain):
while invoice.next_recurrency_date <= today:
invoice._create_recurrent_invoice()
invoice.next_recurrency_date = invoice._get_next_recurrency_date(invoice.next_recurrency_date)
def _create_recurrent_invoice(self):
self.ensure_one()
invoice = self.copy({
'origin': '',
'date_invoice': self.next_recurrency_date,
'is_recurring_document': True,
})
# set the due date
#invoice._onchange_payment_term_date_invoice()
link = '<a href=# data-oe-model=account.invoice data-oe-id=%d>%s</a>' % (self.id, self.number)
invoice.message_post(body=_('This bill has been generated by %s') % link)
return invoice
@api.model
def create(self, vals):
onchanges = {
@@ -1862,6 +1798,7 @@ class AccountPaymentTermLine(models.Model):
if self.value == 'percent' and (self.value_amount < 0.0 or self.value_amount > 100.0):
raise ValidationError(_('Percentages for Payment Terms Line must be between 0 and 100.'))
@api.one
@api.constrains('days')
def _check_days(self):
if self.option in ('day_following_month', 'day_current_month') and self.days <= 0:
@@ -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)
+3 -3
View File
@@ -106,7 +106,7 @@ class AccountMove(models.Model):
default=lambda self: self.env.user.company_id)
matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method")
# Dummy Account field to search on account.move by account_id
dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False)
dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True)
tax_cash_basis_rec_id = fields.Many2one(
'account.partial.reconcile',
string='Tax Cash Basis Entry of',
@@ -318,7 +318,7 @@ class AccountMoveLine(models.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)')
@api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state')
@api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state')
def _amount_residual(self):
""" Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency.
This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount
@@ -983,7 +983,7 @@ class AccountMoveLine(models.Model):
record.payment_id.state = 'reconciled'
result = super(AccountMoveLine, self).write(vals)
if self._context.get('check_move_validity', True):
if self._context.get('check_move_validity', True) and any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')):
move_ids = set()
for line in self:
if line.move_id.id not in move_ids:
+2 -2
View File
@@ -588,9 +588,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
+7 -1
View File
@@ -4,7 +4,7 @@ from datetime import timedelta, datetime
import calendar
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
from odoo.exceptions import ValidationError, UserError
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_round, float_is_zero
@@ -130,6 +130,12 @@ Best Regards,'''))
if values.get('cash_account_code_prefix'):
new_cash_code = values.get('cash_account_code_prefix') or company.cash_account_code_prefix
company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code)
#forbid the change of currency_id if there are already some accounting entries existing
if 'currency_id' in values and values['currency_id'] != company.currency_id.id:
if self.env['account.move.line'].search([('company_id', '=', company.id)]):
raise UserError(_('You cannot change the currency of the company since some journal items already exist'))
return super(ResCompany, self).write(values)
@api.model
@@ -57,12 +57,10 @@ class ResConfigSettings(models.TransientModel):
tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility')
tax_cash_basis_journal_id = fields.Many2one('account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal")
account_hide_setup_bar = fields.Boolean(string='Hide Setup Bar', related='company_id.account_setup_bar_closed',help="Tick if you wish to hide the setup bar on the dashboard")
group_allow_recurring = fields.Boolean(string="Recurring Vendor Bills", implied_group="account.group_allow_recurring")
@api.multi
def set_values(self):
super(ResConfigSettings, self).set_values()
self.env.ref('account.recurrent_vendor_bills_cron').write({'active': self.group_allow_recurring})
if self.group_multi_currency:
self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('product.group_sale_pricelist').id)]})
""" install a chart of accounts for the given company (if required) """
@@ -32,11 +32,6 @@
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="group_allow_recurring" model="res.groups">
<field name="name">Allow to create recurring vendor bills</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="group_cash_rounding" model="res.groups">
<field name="name">Allow the cash rounding management</field>
<field name="category_id" ref="base.module_category_hidden"/>
@@ -426,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
self.model.notifyChanges(self.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);
@@ -655,7 +659,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;
@@ -43,18 +43,23 @@ 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'},
company_id: {string: "Company", type: 'many2one', relation: 'res.company'},
},
records: [
{id: 282, code: 100000, display_name: "100000 Fixed Asset Account", company_id: 1},
{id: 283, code: 101000, display_name: "101000 Current Assets", company_id: 1},
{id: 284, code: 101110, display_name: "101110 Stock Valuation Account", company_id: 1},
{id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)", company_id: 1},
{id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)", company_id: 1},
{id: 287, code: 101200, display_name: "101200 Account Receivable", company_id: 1},
{id: 288, code: 101300, display_name: "101300 Tax Paid", company_id: 1},
{id: 308, code: 101401, display_name: "101401 Bank", company_id: 1},
{id: 282, code: 100000, name: "100000 Fixed Asset Account", company_id: 1},
{id: 283, code: 101000, name: "101000 Current Assets", company_id: 1},
{id: 284, code: 101110, name: "101110 Stock Valuation Account", company_id: 1},
{id: 285, code: 101120, name: "101120 Stock Interim Account (Received)", company_id: 1},
{id: 286, code: 101130, name: "101130 Stock Interim Account (Delivered)", company_id: 1},
{id: 287, code: 101200, name: "101200 Account Receivable", company_id: 1},
{id: 288, code: 101300, name: "101300 Tax Paid", company_id: 1},
{id: 308, code: 101401, name: "101401 Bank", company_id: 1},
{id: 500, code: 500, name: "500 Account", company_id: 1},
{id: 501, code: 501, name: "501 Account", company_id: 1},
{id: 502, code: 502, name: "502 Account", company_id: 1},
{id: 503, code: 503, name: "503 Account", company_id: 1},
{id: 504, code: 504, name: "504 Account", company_id: 1},
],
mark_as_reconciled: function () {
return $.when();
@@ -1050,13 +1055,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'));
@@ -1068,26 +1137,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();
@@ -1099,7 +1168,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'));
-1
View File
@@ -6,7 +6,6 @@ from . import test_account_move_closed_period
from . import test_account_supplier_invoice
from . import test_account_validate_account_move
from . import test_account_invoice_rounding
from . import test_account_supplier_invoice_recurrent
from . import test_bank_statement_reconciliation
from . import test_fiscal_position
from . import test_reconciliation_widget
@@ -1,104 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestAccountRecurrent(AccountingTestCase):
def apply_vendor_cron(self):
self.env.ref('account.recurrent_vendor_bills_cron').method_direct_trigger()
def test_create_recurring_vendor_bills(self):
AccountInvoice = self.env['account.invoice']
invoice_account_id = self.env['account.account'].search([('user_type_id', '=', self.ref('account.data_account_type_receivable'))], limit=1).id
invoice_line_account_id = self.env['account.account'].search([('user_type_id', '=', self.ref('account.data_account_type_expenses'))], limit=1).id
purchase_journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1)
invoice_line_data = [
(0, 0,
{
'product_id': self.ref('product.product_product_4'),
'name': 'product 4 that cost 100',
'quantity': 1.0,
'price_unit': 100.0,
'account_id': invoice_line_account_id,
}
),
(0, 0,
{
'product_id': self.ref('product.product_product_5'),
'name': 'product 5 that cost 200',
'quantity': 2.0,
'price_unit': 200.0,
'account_id': invoice_line_account_id,
}
)
]
# Create an invoice dated 2 months before today
invoice = AccountInvoice.create({
'partner_id': self.ref('base.res_partner_2'),
'account_id': invoice_account_id,
'name': 'invoice test recurrent',
'type': 'in_invoice',
'reference_type': 'none',
'date_invoice': datetime.today() + relativedelta(months=-2),
'journal_id': purchase_journal.id,
'is_recurrency_enabled': True,
'recurrency_interval': 1,
'recurrency_type': 'months',
'invoice_line_ids': invoice_line_data
})
recurring_domain = [('type', '=', 'in_invoice'), ('state', '=', 'draft'), ('is_recurring_document', '=', True)]
# After executing cron, verify that no bill is auto generated if the recurrent bill is in `draft` state
self.apply_vendor_cron()
recurring_invoice_count = AccountInvoice.search_count(recurring_domain)
self.assertEquals(recurring_invoice_count, 0, 'Recurring bills should not be generated when reference bill is still `draft`.')
# After validating bill, run cron and check that 2 recurring bill have been generated
invoice.action_invoice_open() # Validate invoice
self.apply_vendor_cron()
recurring_invoice_count = previous_recurring_invoice_count = AccountInvoice.search_count(recurring_domain)
self.assertEquals(recurring_invoice_count, 2, '2 recurring bills should be generated.')
# verify that bills aren't generated if reference bill is in `cancel` state
purchase_journal.write({'update_posted': True}) # Allow to cancel the invoice
inv2_data = {
'name': 'invoice test recurrent 2',
'date_invoice': datetime.today() + relativedelta(months=-2),
'is_recurrency_enabled': True,
'recurrency_interval': 1,
'recurrency_type': 'months',
}
invoice2 = invoice.copy(default=inv2_data)
invoice2.action_invoice_cancel() # Cancel the invoice
self.apply_vendor_cron()
recurring_invoice_count = AccountInvoice.search_count(recurring_domain)
self.assertEquals(previous_recurring_invoice_count, recurring_invoice_count, 'Recurring bills should not be generated when reference bill is in `cancel` state.')
# verify that vendor bills are generated even when invoice is in `paid` state
inv3_data = {
'name': 'invoice test recurrent 3',
'date_invoice': datetime.today() + relativedelta(months=-2),
'is_recurrency_enabled': True,
'recurrency_interval': 1,
'recurrency_type': 'months',
}
invoice3 = invoice.copy(default=inv3_data)
invoice3.action_invoice_open() # Validate invoice
Payment = self.env['account.payment']
payment_vals = Payment.with_context(default_invoice_ids=[(4, invoice3.id, None)]).default_get(Payment._fields.keys())
payment_vals.update({
'payment_method_id': self.ref("account.account_payment_method_manual_out"),
'journal_id': self.env['account.journal'].search([('type', '=', 'bank')], limit=1).id
})
Payment.create(payment_vals).action_validate_invoice_payment() # Register the payment
self.apply_vendor_cron()
recurring_invoice_count = AccountInvoice.search_count(recurring_domain)
self.assertEquals(recurring_invoice_count, 4, '4 recurring bills should be generated.')
+53 -1
View File
@@ -20,7 +20,9 @@ class TestPayment(AccountingTestCase):
self.currency_chf_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_eur_id = self.env.ref("base.EUR").id
self.env.ref('base.main_company').write({'currency_id': self.currency_eur_id})
company = self.env.ref('base.main_company')
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_eur_id, company.id])
self.product = self.env.ref("product.product_product_4")
self.payment_method_manual_in = self.env.ref("account.account_payment_method_manual_in")
self.payment_method_manual_out = self.env.ref("account.account_payment_method_manual_out")
@@ -361,3 +363,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},
])
+2 -1
View File
@@ -27,7 +27,8 @@ class TestReconciliation(AccountingTestCase):
self.currency_swiss_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_euro_id = self.env.ref("base.EUR").id
self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id})
company = self.env.ref('base.main_company')
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id])
self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1)
self.product = self.env.ref("product.product_product_4")
+6 -46
View File
@@ -339,12 +339,6 @@
<field name="move_id" groups="account.group_account_user" attrs="{'invisible': [('move_id', '=', False)]}"/>
<field name="date" domain="[('state', '=', 'draft'), ('company_id', '=', company_id)]" groups="account.group_account_user"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<field name="is_recurrency_enabled" string="Recurring Bills" help="Tick this box to generate recurring bills. The recurring bills will only be generated if this bill is validated." groups="account.group_allow_recurring" attrs="{'invisible': [('type', '!=', 'in_invoice')]}"/>
<label for="recurrency_interval" attrs="{'invisible': [('is_recurrency_enabled', '=', False)]}" groups="account.group_allow_recurring"/>
<div class="o_row" attrs="{'invisible': [('is_recurrency_enabled', '=', False)]}" groups="account.group_allow_recurring">
<field name="recurrency_interval" help="Define the recurrency of the vendor bill." attrs="{'required': [('is_recurrency_enabled', '=', True)]}"/>
<field name="recurrency_type" attrs="{'required': [('is_recurrency_enabled', '=', True)]}"/>
</div>
</group>
</group>
</page>
@@ -541,10 +535,8 @@
<field name="number" string="Invoice" filter_domain="['|','|','|', ('number','ilike',self), ('origin','ilike',self), ('reference', 'ilike', self), ('partner_id', 'child_of', self)]"/>
<field name="journal_id" />
<filter name="draft" string="Draft" domain="[('state','=','draft')]"/>
<filter name="invoices" string="Invoices" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_invoice','in_invoice'))]"/>
<filter name="refunds" string="Credit Notes" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_refund','in_refund'))]"/>
<separator/>
<filter name="unpaid" string="Not Paid" domain="[('state','=','open')]"/>
<filter name="unpaid" string="Open" domain="[('state', '=', 'open')]"/>
<filter name="paid" string="Paid" domain="[('state', '=', 'paid')]"/>
<filter name="late" string="Overdue" domain="['&amp;', ('date_due', '&lt;', time.strftime('%%Y-%%m-%%d')), ('state', '=', 'open')]" help="Overdue invoices, maturity date passed"/>
<separator/>
<field name="partner_id" operator="child_of"/>
@@ -552,8 +544,6 @@
<field name="date" string="Period"/>
<separator/>
<filter domain="[('user_id','=',uid)]" name="myinvoices" help="My Invoices"/>
<filter domain="[('is_recurrency_enabled','=',True)]" name="is_recurrency_enabled" help="Bills Generating Recurring Bills" groups="account.group_allow_recurring" invisible="context.get('type') != 'in_invoice'"/>
<filter domain="[('is_recurring_document','=',True)]" name="is_recurring_document" help="Bills Generated Automatically" groups="account.group_allow_recurring" invisible="context.get('type') != 'in_invoice'"/>
<separator/>
<filter string="My Activities" name="activities_my"
domain="[('activity_ids.user_id', '=', uid)]"/>
@@ -578,36 +568,6 @@
</field>
</record>
<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>
</field>
</record>
<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>
</field>
</record>
<record id="action_invoice_tree" model="ir.actions.act_window">
<field name="name">Invoices</field>
<field name="res_model">account.invoice</field>
@@ -675,7 +635,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
@@ -710,7 +670,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
@@ -748,7 +708,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
@@ -784,7 +744,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_refund')]</field>
<field name="context">{'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_credit_notes"/>
<field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a vendor credit note
@@ -117,16 +117,12 @@
<span>View</span>
</div>
<div>
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_invoice','in_invoice'))]}">
<span t-if="journal_type == 'sale'">Invoices</span>
<span t-if="journal_type == 'purchase'">Bills</span>
</a>
<a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_invoice')]}">Invoices</a>
<a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_invoice')]}">Bills</a>
</div>
<div>
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_refund','in_refund'))], 'invoice_type': 'refund'}">
<span t-if="journal_type == 'sale'">Credit Notes</span>
<span t-if="journal_type == 'purchase'">Credit Notes</span>
</a>
<a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
<a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
</div>
<div>
<a type="object" name="action_open_reconcile">Payments Matching</a>
+1
View File
@@ -56,6 +56,7 @@
id="action_report_financial"
model="account.financial.report"
string="Financial report"
menu="False"
report_type="qweb-pdf"
name="account.report_financial"
file="account.report_financial"
+1 -1
View File
@@ -16,7 +16,7 @@
<field name="code" placeholder="code"/>
<field name="name"/>
<field name="user_type_id" widget="selection"/>
<field name="tax_ids" widget="many2many_tags"/>
<field name="tax_ids" widget="many2many_tags" domain="[('company_id','=',company_id)]"/>
<field name="tag_ids" widget="many2many_tags" domain="[('applicability', '!=', 'taxes')]" context="{'default_applicability': 'accounts'}" options="{'no_create_edit': True}"/>
<field name="group_id"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
@@ -446,20 +446,9 @@
</div>
</div>
</div>
<h2>Automated Entries</h2>
<div class="row mt16 o_settings_container" id="recommended_apps">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_allow_recurring"/>
</div>
<div class="o_setting_right_pane">
<label for="group_allow_recurring"/>
<div class="text-muted">
Create recurring vendor bills
</div>
</div>
</div>
<div groups="account.group_account_user" class="col-xs-12 col-md-6 o_setting_box" id="asset_types" title="This allows you to manage the assets owned by a company or a person. It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.">
<h2 groups="account.group_account_user">Automated Entries</h2>
<div class="row mt16 o_settings_container" id="recommended_apps" groups="account.group_account_user">
<div class="col-xs-12 col-md-6 o_setting_box" id="asset_types" title="This allows you to manage the assets owned by a company or a person. It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.">
<div class="o_setting_left_pane">
<field name="module_account_asset"/>
</div>
@@ -475,7 +464,7 @@
</div>
</div>
</div>
<div groups="account.group_account_user" class="col-xs-12 col-md-6 o_setting_box" title="This allows you to manage the revenue recognition on selling products. It keeps track of the installments occurred on those revenue recognitions, and creates account moves for those installment lines.">
<div class="col-xs-12 col-md-6 o_setting_box" title="This allows you to manage the revenue recognition on selling products. It keeps track of the installments occurred on those revenue recognitions, and creates account moves for those installment lines.">
<div class="o_setting_left_pane">
<field name="module_account_deferred_revenue" widget="upgrade_boolean"/>
</div>
@@ -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:
@@ -13,4 +13,5 @@ class SaleOrderLine(models.Model):
default_analytic_account = self.env['account.analytic.default'].account_get(self.product_id.id, self.order_id.partner_id.id, self.order_id.user_id.id, fields.Date.today())
if default_analytic_account:
res.update({'account_analytic_id': default_analytic_account.analytic_id.id})
res.update({'analytic_tag_ids' : [(6, 0, default_analytic_account.analytic_tag_ids.ids)]})
return res
+9
View File
@@ -11,3 +11,12 @@ def uninstall_hook(cr, registry):
('module', '=', 'account')
]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': False})
def post_init_hook(cr, registry):
env = Environment(cr, SUPERUSER_ID, {})
res_ids = env['ir.model.data'].search([
('model', '=', 'ir.ui.menu'),
('module', '=', 'account'),
]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': True})
+1
View File
@@ -28,4 +28,5 @@ You could use this simplified accounting in case you work with an (external) acc
],
'application': True,
'uninstall_hook': 'uninstall_hook',
'post_init_hook': 'post_init_hook',
}
+1
View File
@@ -4,3 +4,4 @@ from . import res_config_settings
from . import ir_http
from . import res_partner
from . import res_users
from . import ir_model_fields
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from odoo import models
# This is a nasty hack, targeted for V11 only
class IrModelFields(models.Model):
_inherit = 'ir.model.fields'
def unlink(self):
# Prevent the deletion of some `shared` fields... -_-
self = self.filtered(
lambda rec: not (
rec.model == 'res.config.settings'
and rec.name == 'auth_signup_uninvited'
)
)
return super(IrModelFields, self).unlink()
@@ -155,7 +155,7 @@ FormController.include({
id: candidate.id,
data: candidateChanges,
};
return this.model.notifyChanges(this.handle, changes);
return this.model.notifyChanges(this.handle, changes, {notifyChange: activeBarcode.notifyChange});
},
/**
* @private
@@ -211,6 +211,8 @@ FormController.include({
* @param {OdooEvent} event
* @param {string} event.data.name: the current field name
* @param {string} [event.data.fieldName] optional for x2many sub field
* @param {boolean} [event.data.notifyChange] optional for x2many sub field
* do not trigger on change server side if a candidate has been found
* @param {string} [event.data.quantity] optional field to increase quantity
* @param {Object} [event.data.commands] optional added methods
* can use comand with specific barcode (with ReservedBarcodePrefixes)
@@ -225,7 +227,9 @@ FormController.include({
handle: this.handle,
target: event.target,
widget: event.target.attrs && event.target.attrs.widget,
setQuantityWithKeypress: !! event.data.setQuantityWithKeypress,
fieldName: event.data.fieldName,
notifyChange: (event.data.notifyChange !== undefined) ? event.data.notifyChange : true,
quantity: event.data.quantity,
commands: event.data.commands || {},
candidate: this.activeBarcode[name] && this.activeBarcode[name].handle === this.handle ?
@@ -319,7 +323,7 @@ FormController.include({
// only catch the event if we're not focused in
// another field and it's a number
if (!$(event.target).is('body') || !/[0-9]/.test(character)) {
if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) {
return;
}
@@ -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'},
}
@@ -91,6 +91,7 @@ _map_iban_template = {
'bg': 'BGkk BBBB SSSS DDCC CCCC CC', # Bulgaria
'bh': 'BHkk BBBB CCCC CCCC CCCC CC', # Bahrain
'br': 'BRkk BBBB BBBB SSSS SCCC CCCC CCCT N', # Brazil
'by': 'BYkk BBBB AAAA CCCC CCCC CCCC CCCC', # Belarus
'ch': 'CHkk BBBB BCCC CCCC CCCC C', # Switzerland
'cr': 'CRkk BBBC CCCC CCCC CCCC C', # Costa Rica
'cy': 'CYkk BBBS SSSS CCCC CCCC CCCC CCCC', # Cyprus
+3 -1
View File
@@ -302,7 +302,8 @@ class Import(models.TransientModel):
try:
thousand_separator = decimal_separator = False
for val in preview_values:
if val == '':
val = val.strip()
if not val:
continue
# value might have the currency symbol left or right from the value
val = self._remove_currency_symbol(val)
@@ -587,6 +588,7 @@ class Import(models.TransientModel):
thousand_separator = options.get('float_thousand_separator', ' ')
decimal_separator = options.get('float_decimal_separator', '.')
for line in data:
line[index] = line[index].strip()
if not line[index]:
continue
line[index] = line[index].replace(thousand_separator, '').replace(decimal_separator, '.')
@@ -145,12 +145,13 @@ def _is_studio_custom(path):
Returns True if any of the records contains a context with the key
studio in it, False if none of the records do
"""
path = os.path.join(path, 'data')
filenames = next(iter(os.walk(path)))[2]
filenames = [f for f in filenames if f.lower().endswith('.xml')]
filepaths = []
for level in os.walk(path):
filepaths += [os.path.join(level[0], fn) for fn in level[2]]
filepaths = [fp for fp in filepaths if fp.lower().endswith('.xml')]
for filename in filenames:
root = lxml.etree.parse(os.path.join(path, filename)).getroot()
for fp in filepaths:
root = lxml.etree.parse(fp).getroot()
for record in root:
# there might not be a context if it's a non-studio module
+1 -1
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Calendar SMS",
'name': "Calendar - SMS",
'summary': 'Send text messages as event reminders',
'description': "Send text messages as event reminders",
'category': 'Hidden',
+14 -12
View File
@@ -973,6 +973,8 @@ class Lead(models.Model):
'nb_opportunities': 0,
}
today = fields.Date.from_string(fields.Date.context_today(self))
opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)])
for opp in opportunities:
@@ -980,28 +982,28 @@ class Lead(models.Model):
if opp.activity_date_deadline:
if opp.date_deadline:
date_deadline = fields.Date.from_string(opp.date_deadline)
if date_deadline == date.today():
if date_deadline == today:
result['closing']['today'] += 1
if date.today() <= date_deadline <= date.today() + timedelta(days=7):
if today <= date_deadline <= today + timedelta(days=7):
result['closing']['next_7_days'] += 1
if date_deadline < date.today() and not opp.date_closed:
if date_deadline < today and not opp.date_closed:
result['closing']['overdue'] += 1
# Next activities
for activity in opp.activity_ids:
date_deadline = fields.Date.from_string(activity.date_deadline)
if date_deadline == date.today():
if date_deadline == today:
result['activity']['today'] += 1
if date.today() <= date_deadline <= date.today() + timedelta(days=7):
if today <= date_deadline <= today + timedelta(days=7):
result['activity']['next_7_days'] += 1
if date_deadline < date.today():
if date_deadline < today:
result['activity']['overdue'] += 1
# Won in Opportunities
if opp.date_closed and opp.stage_id.probability == 100:
date_closed = fields.Date.from_string(opp.date_closed)
if date.today().replace(day=1) <= date_closed <= date.today():
if today.replace(day=1) <= date_closed <= today:
if opp.planned_revenue:
result['won']['this_month'] += opp.planned_revenue
elif date.today() + relativedelta(months=-1, day=1) <= date_closed < date.today().replace(day=1):
elif today + relativedelta(months=-1, day=1) <= date_closed < today.replace(day=1):
if opp.planned_revenue:
result['won']['last_month'] += opp.planned_revenue
@@ -1026,9 +1028,9 @@ class Lead(models.Model):
for activity in activites_done:
if activity['date']:
date_act = fields.Date.from_string(activity['date'])
if date.today().replace(day=1) <= date_act <= date.today():
if today.replace(day=1) <= date_act <= today:
result['done']['this_month'] += 1
elif date.today() + relativedelta(months=-1, day=1) <= date_act < date.today().replace(day=1):
elif today + relativedelta(months=-1, day=1) <= date_act < today.replace(day=1):
result['done']['last_month'] += 1
# Meetings
@@ -1043,9 +1045,9 @@ class Lead(models.Model):
for meeting in meetings:
if meeting['start']:
start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
if start == date.today():
if start == today:
result['meeting']['today'] += 1
if date.today() <= start <= date.today() + timedelta(days=7):
if today <= start <= today + timedelta(days=7):
result['meeting']['next_7_days'] += 1
result['done']['target'] = self.env.user.target_sales_done
+2 -2
View File
@@ -10,7 +10,7 @@ class ResConfigSettings(models.TransientModel):
crm_alias_prefix = fields.Char('Default Alias Name for Leads')
generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails', config_parameter='crm.generate_lead_from_alias')
group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead')
module_crm_phone_validation = fields.Boolean("Phone Validation")
module_crm_phone_validation = fields.Boolean("Phone Formatting")
module_web_clearbit = fields.Boolean("Customer Autocomplete")
def _find_default_lead_alias_id(self):
@@ -33,7 +33,7 @@ class ResConfigSettings(models.TransientModel):
@api.onchange('generate_lead_from_alias')
def _onchange_generate_lead_from_alias(self):
self.crm_alias_prefix = 'info' if self.generate_lead_from_alias else False
self.crm_alias_prefix = (self.crm_alias_prefix or 'info') if self.generate_lead_from_alias else False
@api.model
def get_values(self):
@@ -62,7 +62,7 @@
<div class="o_setting_right_pane">
<label for="module_crm_phone_validation"/>
<div class="text-muted" id="module_crm_phone_validation">
Formats phone numbers
Format phone numbers based on national conventions
</div>
</div>
</div>
+1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import res_config_settings
from . import delivery_carrier
from . import delivery_grid
from . import product_template
+4 -1
View File
@@ -210,8 +210,11 @@ class DeliveryCarrier(models.Model):
carrier.product_id.list_price = carrier.fixed_price
def fixed_rate_shipment(self, order):
price = self.fixed_price
if self.company_id.currency_id.id != order.currency_id.id:
price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price)
return {'success': True,
'price': self.fixed_price,
'price': price,
'error_message': False,
'warning_message': False}
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
def set_values(self):
super(ResConfigSettings, self).set_values()
rule = self.env.ref('delivery.delivery_carrier_comp_rule', False)
if rule:
rule.write({'active': not bool(self.company_share_product)})
@@ -6,6 +6,7 @@
<field name="model_id" ref="model_delivery_carrier"/>
<field name="global" eval="True"/>
<field name="domain_force"> ['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
<field name="active" eval="False"/>
</record>
</data>
</odoo>
+10 -12
View File
@@ -84,18 +84,6 @@
</tr>
<tr>
<td style="padding:15px 20px 0px 20px;">
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
<td style="padding:25px 0px; text-align:center;">
<p>
Don't forget to <strong>add it to your calendar</strong>:
</p>
<a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
<a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
<a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
</td>
</tr>
</table>
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
<td style="padding:25px 0px;vertical-align:top;">
@@ -165,6 +153,16 @@
</tr>
</table>
% endif
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
<td style="padding:25px 0px;">
<strong>Add this event to your calendar</strong>
<a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
<a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
<a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
+3 -1
View File
@@ -247,9 +247,11 @@ class FleetVehicle(models.Model):
@return: the costs log view
"""
self.ensure_one()
copy_context = dict(self.env.context)
copy_context.pop('group_by', None)
res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
res.update(
context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True),
context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True),
domain=[('vehicle_id', '=', self.id)]
)
return res
+1 -1
View File
@@ -48,5 +48,5 @@ a notification in you Odoo which ever the page you are.
Google Calendar
---------------
With the plugin Google\_calendar, you can synchronize your Odoo calendar with Google Calendar.
With this plugin, you can synchronize your Odoo calendar with Google Calendar.
+8 -4
View File
@@ -416,7 +416,8 @@ class HrExpense(models.Model):
product = default_product
else:
expense_description = expense_description.replace(product_code.group(), '')
product = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product
products = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product
product = products.filtered(lambda p: p.default_code == product_code.group(1)) or products[0]
pattern = '[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?'
# Match the last occurence of a float in the string
@@ -591,10 +592,13 @@ class HrExpenseSheet(models.Model):
MailFollowers = self.env['mail.followers']
for partner in users.mapped('partner_id'):
values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {partner.id: None}, {})[0]
if values.get('user_id') and values.get('user_id') != employee.user_id.id:
if values.get('user_id') and values.get('user_id') != employee.user_id.id and values.get('user_id') != self.env.uid:
resp_partner = self.env['res.users'].browse(values['user_id'])
values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {resp_partner.partner_id.id: None}, {})[0]
# Parsing ORM command to avoid adding several times the same partner
# TDE note: a better implementation should come in saas 11.3 or 11.4, allowing to normally remove that code
if resp_partner.partner_id.id not in (x[2].get('partner_id') for x in values['message_follower_ids'] if len(x) == 3):
values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {resp_partner.partner_id.id: None}, {})[0]
# --------------------------------------------
# Actions
+26 -25
View File
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<xpath expr="//group[@name='active_group']" position="before">
<group string="Expenses" name="timesheet">
<field name="expense_manager_id"/>
<field name="expense_manager_id"/>
</group>
</xpath>
</field>
@@ -35,14 +35,29 @@
</field>
</record>
<record id="hr_expense_view_form_without_header" model="ir.ui.view">
<record id="hr_expense_view_form" model="ir.ui.view">
<field name="name">hr.expense.view.form</field>
<field name="model">hr.expense</field>
<field eval="35" name="priority"/>
<field name="arch" type="xml">
<form string="Expenses" class="o_expense_form">
<field name="state" invisible="True"/>
<header>
<widget name="attach_document" string="Attach Document" action="message_post" attrs="{'invisible': [('attachment_number', '&lt;=', 0)]}"/>
<widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '&gt;=', 1)]}"/>
<button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&gt;=', 1), ('sheet_id', '!=', False)]}"/>
<button name="action_submit_expenses" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&lt;=', 0), ('sheet_id', '!=', False)]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,reported,approved,done,refused"/>
<button name="action_view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
</header>
<sheet>
<div class="oe_button_box">
<button name="action_get_attachment_view"
class="oe_stat_button"
icon="fa-book"
type="object"
attrs="{'invisible': [('attachment_number', '=', 0)]}">
<field name="attachment_number" widget="statinfo" string="Documents" options="{'reload_on_button': true}"/>
</button>
</div>
<div class="oe_title">
<label for="name"/>
<h1>
@@ -91,32 +106,18 @@
</field>
</record>
<record id="hr_expense_view_form" model="ir.ui.view">
<record id="hr_expense_view_form_without_header" model="ir.ui.view">
<field name="name">hr.expense.view.form</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.hr_expense_view_form_without_header"/>
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
<field eval="35" name="priority"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="/form/*" position="before">
<header>
<widget name="attach_document" string="Attach Document" action="message_post" attrs="{'invisible': [('attachment_number', '&lt;=', 0)]}"/>
<widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '&gt;=', 1)]}"/>
<button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&gt;=', 1), ('sheet_id', '!=', False)]}"/>
<button name="action_submit_expenses" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&lt;=', 0), ('sheet_id', '!=', False)]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,reported,approved,done,refused"/>
<button name="action_view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
</header>
<xpath expr="/form/header" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="/form/sheet/*" position="before">
<div class="oe_button_box">
<button name="action_get_attachment_view"
class="oe_stat_button"
icon="fa-book"
type="object"
attrs="{'invisible': [('attachment_number', '=', 0)]}">
<field name="attachment_number" widget="statinfo" string="Documents" options="{'reload_on_button': true}"/>
</button>
</div>
<xpath expr="/form/sheet/div[hasclass('oe_button_box')]" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
+1 -1
View File
@@ -100,7 +100,7 @@ class HolidaysRequest(models.Model):
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.manager_id = self.employee_id and self.employee_id.parent_id
self.department_id = self.employee_id and self.employee_id.department_id
self.department_id = self.employee_id.department_id
@api.multi
@api.depends('number_of_days_temp')
+2 -1
View File
@@ -190,7 +190,8 @@ class HrPayslip(models.Model):
leave_time = (interval[1] - interval[0]).seconds / 3600
current_leave_struct['number_of_hours'] += leave_time
work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id)
current_leave_struct['number_of_days'] += leave_time / work_hours
if work_hours:
current_leave_struct['number_of_days'] += leave_time / work_hours
# compute worked days
work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id)
@@ -35,6 +35,7 @@ class HrPayslipEmployees(models.TransientModel):
'date_from': from_date,
'date_to': to_date,
'credit_note': run_data.get('credit_note'),
'company_id': employee.company_id.id,
}
payslips += self.env['hr.payslip'].create(res)
payslips.compute_sheet()
+1 -1
View File
@@ -14,7 +14,7 @@ class AccountAnalyticLine(models.Model):
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
return result
task_id = fields.Many2one('project.task', 'Task')
task_id = fields.Many2one('project.task', 'Task', index=True)
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
employee_id = fields.Many2one('hr.employee', "Employee")
+12 -3
View File
@@ -15,13 +15,18 @@ class Project(models.Model):
class Task(models.Model):
_inherit = "project.task"
remaining_hours = fields.Float("Remaining Hours", compute='_compute_progress_hours', store=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
remaining_hours = fields.Float("Remaining Hours", compute='_compute_progress_hours', inverse='_inverse_remaining_hours', store=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.")
total_hours_spent = fields.Float("Total Hours", compute='_compute_progress_hours', store=True, help="Computed as: Time Spent + Sub-tasks Hours.")
progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task. In case if the total spent hours exceeds planned hours then the progress bar may go above 100%")
progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.")
subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)", oldname='children_hours')
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
@api.onchange('remaining_hours')
def _inverse_remaining_hours(self):
for task in self:
task.planned_hours = task.remaining_hours + task.effective_hours + task.subtask_effective_hours
@api.depends('timesheet_ids.unit_amount')
def _compute_effective_hours(self):
for task in self:
@@ -31,7 +36,11 @@ class Task(models.Model):
def _compute_progress_hours(self):
for task in self:
if (task.planned_hours > 0.0):
task.progress = round(100.0 * (task.effective_hours + task.subtask_effective_hours) / task.planned_hours, 2)
task_total_hours = task.effective_hours + task.subtask_effective_hours
if task_total_hours > task.planned_hours:
task.progress = 100
else:
task.progress = round(100.0 * task_total_hours / task.planned_hours, 2)
else:
task.progress = 0.0
+6 -3
View File
@@ -62,13 +62,16 @@
<group>
<group>
<field name="planned_hours" widget="float_time"/>
<field name="subtask_planned_hours" widget="float_time" groups="project.group_subtask_project" attrs="{'invisible': [('subtask_count', '=', 0)]}"/>
<label for="subtask_planned_hours" groups="project.group_subtask_project" attrs="{'invisible': [('subtask_count', '=', 0)]}"/>
<div class="o_row" groups="project.group_subtask_project" attrs="{'invisible': [('subtask_count', '=', 0)]}">
<field name="subtask_planned_hours" widget="float_time"/><span> planned hours</span>
</div>
</group>
<group>
<field name="progress" widget="progressbar"/>
</group>
</group>
<field name="timesheet_ids" context="{'default_project_id': project_id}">
<field name="timesheet_ids" context="{'default_project_id': project_id, 'default_name':''}">
<tree editable="bottom" string="Timesheet Activities" default_order="date">
<field name="date"/>
<field name="user_id" invisible="1"/>
@@ -83,7 +86,7 @@
<field name="effective_hours" widget="float_time" />
<field name="subtask_effective_hours" widget="float_time" groups="project.group_subtask_project" attrs="{'invisible' : [('subtask_count', '=', 0)]}" />
<field name="total_hours_spent" widget="float_time" class="oe_subtotal_footer_separator" attrs="{'invisible' : [('subtask_effective_hours', '=', 0.0)]}" />
<field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" readonly="1"/>
<field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator"/>
</group>
</group>
</page>
+2 -1
View File
@@ -110,7 +110,8 @@ class LivechatController(http.Controller):
@http.route('/im_livechat/history', type="json", auth="public")
def history_pages(self, pid, channel_uuid, page_history=None):
channel = request.env['mail.channel'].search([('uuid', '=', channel_uuid)])
partner_ids = (pid, request.env.user.partner_id.id)
channel = request.env['mail.channel'].sudo().search([('uuid', '=', channel_uuid), ('channel_partner_ids', 'in', partner_ids)])
if channel:
channel._send_history_message(pid, page_history)
return True
@@ -238,7 +238,7 @@ class ImLivechatChannelRule(models.Model):
"""
def _match(rules):
for rule in rules:
if re.search(rule.regex_url, url):
if re.search(rule.regex_url or '', url):
return rule
return False
# first, search the country specific rules (the first match is returned)
@@ -155,8 +155,8 @@ var LivechatButton = Widget.extend({
self.send_welcome_message();
self.render_messages();
this.busBus.add_channel(channel.uuid);
this.busBus.start_polling();
self.busBus.add_channel(channel.uuid);
self.busBus.start_polling();
utils.set_cookie('im_livechat_session', JSON.stringify(channel), 60*60);
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
@@ -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')
@@ -15,9 +15,9 @@
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label string="New Car Request"/>
<div class="text-muted o_row">
<div class="text-muted content-group mt16">
<span>An employee cannot request a new car if there are more than </span>
<field name="max_unused_cars"/>
<field name="max_unused_cars" class="text-center oe_inline" />
<span> existing vehicles availables.</span>
</div>
</div>
@@ -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>
+5 -5
View File
@@ -73,21 +73,21 @@
<td class="text-right" t-if="is_cgst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_cgst_id in tax_data['tag_ids']">
<t t-esc="tax_data['amount']"/>
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
<td class="text-right" t-if="is_cgst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_sgst_id in tax_data['tag_ids']">
<t t-esc="tax_data['amount']"/>
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
<td class="text-right" t-if="is_igst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_igst_id in tax_data['tag_ids']">
<t t-esc="tax_data['amount']"/>
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
@@ -95,10 +95,10 @@
<td class="text-right" t-if="is_cess">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_cess_id in tax_data['tag_ids']">
<t t-set="total_cess" t-value="total_cess + tax_data['amount']"/>
<t t-set="total_cess" t-value="total_cess + tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
<t t-esc="total_cess"/>
<t t-esc="total_cess" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</td>
</t>
</tr>
+4
View File
@@ -18,6 +18,10 @@
<field name="name">DIOT: 16%</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_diot_16_imp" model="account.account.tag">
<field name="name">DIOT: 16% IMP</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_diot_0" model="account.account.tag">
<field name="name">DIOT: 0%</field>
<field name="applicability">taxes</field>
+12 -5
View File
@@ -5,18 +5,25 @@
<!-- Smileys -->
<record id="mail_smiley_normal_smile" model="mail.shortcode">
<field name="source">:)</field>
<field name="unicode_source">😊</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/&gt;</field>
<field name="unicode_source">🙂</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png' alt='🙂'/&gt;</field>
<field name="description">Smile</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_smile" model="mail.shortcode">
<field name="source">:D</field>
<field name="unicode_source">😄</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png' alt='😄'/&gt;</field>
<field name="unicode_source">😀</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile_open_mouth.png' alt='😀'/&gt;</field>
<field name="description">Smile with open mouth</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_smile_eyes" model="mail.shortcode">
<field name="source">^^</field>
<field name="unicode_source">😊</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/&gt;</field>
<field name="description">Smile with with smiling eyes</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_laugh" model="mail.shortcode">
<field name="source">xD</field>
<field name="unicode_source">😆</field>
@@ -25,7 +32,7 @@
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_tears_of_joy" model="mail.shortcode">
<field name="source">:-)</field>
<field name="source">:joy</field>
<field name="unicode_source">😂</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/&gt;</field>
<field name="description">Tears of joy</field>
+9
View File
@@ -357,6 +357,15 @@ class MailActivityMixin(models.AbstractModel):
def _search_activity_summary(self, operator, operand):
return [('activity_ids.summary', operator, operand)]
@api.multi
def write(self, vals):
# Delete activities of archived record.
if 'active' in vals and vals['active'] is False:
self.env['mail.activity'].sudo().search(
[('res_model', '=', self._name), ('res_id', 'in', self.ids)]
).unlink()
return super(MailActivityMixin, self).write(vals)
@api.multi
def unlink(self):
""" Override unlink to delete records activities through (res_model, res_id). """
+1 -1
View File
@@ -56,7 +56,7 @@ class Channel(models.Model):
('channel', 'Channel')],
'Channel Type', default='channel')
description = fields.Text('Description')
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4())
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4(), copy=False)
email_send = fields.Boolean('Send messages by email', default=False)
# multi users channel
channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen')
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+83 -64
View File
@@ -2,7 +2,7 @@ odoo.define('mail.Activity', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
var concurrency = require('web.concurrency');
var BasicModel = require('web.BasicModel');
var core = require('web.core');
var field_registry = require('web.field_registry');
var time = require('web.time');
@@ -11,6 +11,63 @@ var utils = require('mail.utils');
var QWeb = core.qweb;
var _t = core._t;
/**
* Fetches activities and postprocesses them.
*
* This standalone function performs an RPC, but to do so, it needs an instance
* of a widget that implements the _rpc() function.
*
* @todo i'm not very proud of the widget instance given in arguments, we should
* probably try to do it a better way in the future.
*
* @param {Widget} self a widget instance that can perform RPCs
* @param {Array} ids the ids of activities to read
* @return {Deferred<Array>} resolved with the activities
*/
function _readActivities(self, ids) {
if (!ids.length) {
return $.when([]);
}
return self._rpc({
model: 'mail.activity',
method: 'read',
args: [ids],
}).then(function (activities) {
// convert create_date and date_deadline to moments
_.each(activities, function (activity) {
activity.create_date = moment(time.auto_str_to_date(activity.create_date));
activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline));
});
// sort activities by due date
activities = _.sortBy(activities, 'date_deadline');
return activities;
});
}
BasicModel.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Fetches the activities displayed by the activity field widget in form
* views.
*
* @private
* @param {Object} record - an element from the localData
* @param {string} fieldName
* @return {Deferred<Array>} resolved with the activities
*/
_fetchSpecialActivity: function (record, fieldName) {
var localID = (record._changes && fieldName in record._changes) ?
record._changes[fieldName] :
record.data[fieldName];
return _readActivities(this, this.localData[localID].res_ids);
},
});
/**
* Set the 'label_delay' entry in activity data according to the deadline date
* @param {Array} activities list of activity Object
@@ -44,12 +101,6 @@ var setDelayLabel = function(activities){
};
var AbstractActivityField = AbstractField.extend({
// inherited
init: function () {
this._super.apply(this, arguments);
this.activities = [];
},
// private
_markActivityDone: function (id, feedback) {
return this._rpc({
@@ -59,33 +110,6 @@ var AbstractActivityField = AbstractField.extend({
kwargs: {feedback: feedback},
});
},
_readActivities: function () {
var self = this;
var missing_ids = _.difference(this.value.res_ids, _.pluck(this.activities, 'id'));
var fetch_def;
if (missing_ids.length) {
fetch_def = this._rpc({
model: 'mail.activity',
method: 'read',
args: [missing_ids],
});
}
return $.when(fetch_def).then(function (results) {
// convert create_date and date_deadline to moments
_.each(results, function (activity) {
activity.create_date = moment(time.auto_str_to_date(activity.create_date));
activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline));
});
// filter out activities that are no longer linked to this record
self.activities = _.filter(self.activities.concat(results || []), function (activity) {
return _.contains(self.value.res_ids, activity.id);
});
// sort activities by due date
self.activities = _.sortBy(self.activities, 'date_deadline');
});
},
_scheduleActivity: function (id, previous_activity_type_id, callback) {
var action = {
type: 'ir.actions.act_window',
@@ -116,41 +140,37 @@ var Activity = AbstractActivityField.extend({
'click .o_activity_unlink': '_onUnlinkActivity',
'click .o_activity_done': '_onMarkActivityDone',
},
specialData: '_fetchSpecialActivity',
// inherited
init: function () {
this._super.apply(this, arguments);
this.dp = new concurrency.DropPrevious();
this.activities = this.record.specialData[this.name];
},
_render: function () {
// note: the rendering of this widget is asynchronous as it needs to
// fetch the details of the linked activities
var self = this;
var fetch_def = this.dp.add(this._readActivities());
return fetch_def.then(function () {
_.each(self.activities, function (activity) {
if (activity.note) {
activity.note = utils.parse_and_transform(activity.note, utils.add_link);
}
});
var activities = setDelayLabel(self.activities);
if (activities.length) {
var nbActivities = _.countBy(activities, 'state');
self.$el.html(QWeb.render('mail.activity_items', {
activities: activities,
nbPlannedActivities: nbActivities.planned,
nbTodayActivities: nbActivities.today,
nbOverdueActivities: nbActivities.overdue,
date_format: time.getLangDateFormat(),
datetime_format: time.getLangDatetimeFormat(),
}));
} else {
self.$el.empty();
_.each(this.activities, function (activity) {
if (activity.note) {
activity.note = utils.parse_and_transform(activity.note, utils.add_link);
}
});
var activities = setDelayLabel(this.activities);
if (activities.length) {
var nbActivities = _.countBy(activities, 'state');
this.$el.html(QWeb.render('mail.activity_items', {
activities: activities,
nbPlannedActivities: nbActivities.planned,
nbTodayActivities: nbActivities.today,
nbOverdueActivities: nbActivities.overdue,
date_format: time.getLangDateFormat(),
datetime_format: time.getLangDatetimeFormat(),
}));
} else {
this.$el.empty();
}
},
_reset: function (record) {
this._super.apply(this, arguments);
this.activities = this.record.specialData[this.name];
// the mail widgets being persistent, one need to update the res_id on reset
this.res_id = record.res_id;
},
@@ -196,9 +216,6 @@ var Activity = AbstractActivityField.extend({
});
return this.do_action(action, {
on_close: function () {
// remove the edited activity from the array of fetched activities to
// force a reload of that activity
self.activities = _.reject(self.activities, {id: activity_id});
self._reload({activity: true, thread: true});
},
});
@@ -312,10 +329,10 @@ var KanbanActivity = AbstractActivityField.extend({
_renderDropdown: function () {
var self = this;
this.$('.o_activity').html(QWeb.render("mail.KanbanActivityLoading"));
return this._readActivities().then(function () {
return _readActivities(this, this.value.res_ids).then(function (activities) {
self.$('.o_activity').html(QWeb.render("mail.KanbanActivityDropdown", {
selection: self.selection,
records: _.groupBy(setDelayLabel(self.activities), 'state'),
records: _.groupBy(setDelayLabel(activities), 'state'),
uid: self.getSession().uid,
}));
});
@@ -328,7 +345,9 @@ var KanbanActivity = AbstractActivityField.extend({
// handlers
_onButtonClicked: function (event) {
event.preventDefault();
this._renderDropdown();
if (!this.$el.hasClass('open')) {
this._renderDropdown();
}
},
_onMarkActivityDone: function (event) {
event.stopPropagation();
+1 -1
View File
@@ -591,7 +591,7 @@ var BasicComposer = Widget.extend({
*/
_onKeydown: function (event) {
switch(event.which) {
// UP, DOWN: prevent moving cursor if navigation in mention propositions
// UP, DOWN, TAB: prevent moving cursor if navigation in mention propositions
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
case $.ui.keyCode.TAB:
+2 -2
View File
@@ -41,7 +41,7 @@ Discuss.include({
*/
on_attach_callback: function () {
if (this.channel && this._isInInboxTab()) {
this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]});
this.thread.scroll_to({offset: this.channelsScrolltop[this.channel.id]});
}
},
/**
@@ -49,7 +49,7 @@ Discuss.include({
*/
on_detach_callback: function () {
if (this._isInInboxTab()) {
this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
this.channelsScrolltop[this.channel.id] = this.thread.get_scrolltop();
}
},
+34 -1
View File
@@ -17,6 +17,7 @@ var DocumentViewer = Widget.extend({
'click .o_viewer_video': '_onVideoClicked',
'click .move_next': '_onNext',
'click .move_previous': '_onPrevious',
'click .o_rotate': '_onRotate',
'click .o_zoom_in': '_onZoomIn',
'click .o_zoom_out': '_onZoomOut',
'click .o_close_btn, .o_viewer_img_wrapper': '_onClose',
@@ -103,6 +104,30 @@ var DocumentViewer = Widget.extend({
this.$('.o_viewer_img').load(_.bind(this._onImageLoaded, this));
this._reset();
},
/**
* Get CSS transform property based on scale and angle
*
* @private
* @param {float} scale
* @param {float} angle
*/
_getTransform: function(scale, angle) {
return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)'
},
/**
* Rotate image clockwise by provided angle
*
* @private
* @param {float} angle
*/
_rotate: function (angle) {
this._reset();
var new_angle = (this.angle || 0) + angle;
this.$('.o_viewer_img').css('transform', this._getTransform(this.scale, new_angle));
this.$('.o_viewer_img').css('max-width', new_angle % 180 !== 0 ? $(document).height() : '100%');
this.$('.o_viewer_img').css('max-height', new_angle % 180 !== 0 ? $(document).width() : '100%');
this.angle = new_angle;
},
/**
* Zoom in/out image by provided scale
*
@@ -111,7 +136,7 @@ var DocumentViewer = Widget.extend({
*/
_zoom: function (scale) {
if (scale > 0.5) {
this.$('.o_viewer_img').css('transform', 'scale3d(' + scale + ', ' + scale + ', 1)');
this.$('.o_viewer_img').css('transform', this._getTransform(scale, this.angle || 0));
this.scale = scale;
}
},
@@ -280,6 +305,14 @@ var DocumentViewer = Widget.extend({
videoElement.pause();
}
},
/**
* @private
* @param {MouseEvent} e
*/
_onRotate: function (e) {
e.preventDefault();
this._rotate(90);
},
/**
* @private
* @param {MouseEvent} e
@@ -264,6 +264,7 @@
img {
width: @o-mail-attachment-image-size;
height: @o-mail-attachment-image-size;
image-orientation: from-image; // Only supported in Firefox
}
.o_image_overlay{
display: none;
@@ -365,6 +366,9 @@
.o-flex-display();
justify-content: center;
align-items: center;
img {
image-orientation: from-image; // Only supported in Firefox
}
img, video {
max-width: 100%;
max-height: 100%;
+2 -6
View File
@@ -321,15 +321,11 @@
.o_mail_chat_content {
.o-flex(1, 0, 0);
margin-left: 0;
margin-left: 10px;
margin-right: 10px;
background-color: transparent;
.o_mail_no_content {
.o-position-absolute(@top: 30%);
text-align: center;
}
}
.o_mail_chat_tab_pane {
overflow-x: scroll;
.o_mail_add_channel {
+5 -2
View File
@@ -208,11 +208,14 @@
.o_thread_title {
margin-top: 20px;
margin-bottom: 20px;
font-size: 20px;
font-weight: bold;
font-size: 125%;
}
.o_mail_no_content {
.o-webclient-padding();
.o-position-absolute(30%, 0, 0, 0);
text-align: center;
font-size: 115%;
}
.o_thread_message .o_thread_message_core .o_mail_read_more {
+14 -13
View File
@@ -50,6 +50,11 @@
</div>
<div t-if="widget.activeAttachment.type == 'image'" class="o_viewer_toolbar">
<ul class="list-inline">
<li>
<a class="o_rotate" href="#">
<i class="fa fa-2x fa-repeat"></i>
</a>
</li>
<li>
<a class="o_zoom_out" href="#">
<i class="fa fa-2x fa-search-minus"></i>
@@ -105,18 +110,14 @@
<!-- When a channel is empty-->
<div t-name="mail.EmptyChannel" class="o_mail_no_content">
<div class="o_view_nocontent">
<div class="o_nocontent_help">
<t t-if="options.channel_id==='channel_inbox'">
<p>Congratulations, your inbox is empty</p>
<p>New messages appear here.</p>
</t>
<t t-if="options.channel_id==='channel_starred'">
<p>No starred message</p>
<p>You can mark any message as 'starred', and it shows up in this channel.</p>
</t>
</div>
</div>
<t t-if="options.channel_id==='channel_inbox'">
<div class="o_thread_title">Congratulations, your inbox is empty</div>
<div>New messages appear here.</div>
</t>
<t t-if="options.channel_id==='channel_starred'">
<div class="o_thread_title">No starred message</div>
<div>You can mark any message as 'starred', and it shows up in this channel.</div>
</t>
</div>
<t t-name="mail.ChatThread.Content">
@@ -266,7 +267,7 @@
</t>
<t t-name="mail.Attachment">
<t t-set="type" t-value="attachment.mimetype.split('/').shift()"/>
<t t-set="type" t-value="attachment.mimetype and attachment.mimetype.split('/').shift()"/>
<div t-attf-class="o_attachment #{attachment.upload ? 'o_attachment_uploading' : ''}" t-att-title="attachment.name">
<t t-if="type == 'image'">
<div class="o_image_box">
+125 -3
View File
@@ -351,12 +351,12 @@ QUnit.test('kanban activity widget with an activity', function (assert) {
// click on the activity button to re-open dropdown
$record.find('.o_activity_btn').click();
assert.strictEqual(rpcCount, 2, 'no RPC should be done as the activities are now in cache');
assert.strictEqual(rpcCount, 3, 'should have reloaded the activities');
// mark activity as done
$record.find('.o_mark_as_done').click();
$record = kanban.$('.o_kanban_record').first(); // the record widget has been reset
assert.strictEqual(rpcCount, 4, 'should have done an RPC to mark activity as done, and a read');
assert.strictEqual(rpcCount, 5, 'should have done an RPC to mark activity as done, and a read');
assert.ok($record.find('.o_mail_activity .o_activity_color_default:not(.o_activity_color_today)').length,
"activity widget should have been updated correctly");
assert.strictEqual($record.find('.o_mail_activity.open').length, 1,
@@ -707,6 +707,128 @@ QUnit.test('chatter: Attachment viewer', function (assert) {
form.destroy();
});
QUnit.test('form activity widget: read RPCs', function (assert) {
// Records of model 'mail.activity' may be updated in business flows (e.g.
// the date of a 'Meeting' activity is updated when the associated meeting
// is dragged&dropped in the Calendar view). Because of that, the activities
// must be reloaded when the form is reloaded (the widget can't keep an
// internal cache).
assert.expect(6);
this.data.partner.records[0].activity_ids = [1];
this.data.partner.records[0].activity_state = 'today';
this.data['mail.activity'].records = [{
id: 1,
display_name: "An activity",
date_deadline: moment().format("YYYY-MM-DD"), // now
state: "today",
user_id: 2,
activity_type_id: 2,
}];
var nbReads = 0;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<div class="oe_chatter">' +
'<field name="activity_ids" widget="mail_activity"/>' +
'</div>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'read' && args.model === 'mail.activity') {
nbReads++;
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(nbReads, 1, "should have read the activities");
assert.strictEqual(form.$('.o_mail_activity .o_thread_message').length, 1,
"should display an activity");
assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(),
'Today', "the activity should be today");
form.$buttons.find('.o_form_button_edit').click();
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(nbReads, 1, "should not have re-read the activities");
// simulate a date change, and a reload of the form view
var tomorrow = moment().add(1, 'day').format("YYYY-MM-DD");
this.data['mail.activity'].records[0].date_deadline = tomorrow;
form.reload();
assert.strictEqual(nbReads, 2, "should have re-read the activities");
assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(),
'Tomorrow', "the activity should be tomorrow");
form.destroy();
});
QUnit.test('form activity widget on a new record', function (assert) {
assert.expect(0);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<div class="oe_chatter">' +
'<field name="activity_ids" widget="mail_activity"/>' +
'</div>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'read' && args.model === 'mail.activity') {
throw new Error("should not do a read on mail.activity");
}
return this._super.apply(this, arguments);
},
});
form.destroy();
});
QUnit.test('form activity widget with another x2many field in view', function (assert) {
assert.expect(1);
this.data.partner.fields.m2m = {string: "M2M", type: 'many2many', relation: 'partner'};
this.data.partner.records[0].m2m = [2];
this.data.partner.records[0].activity_ids = [1];
this.data.partner.records[0].activity_state = 'today';
this.data['mail.activity'].records = [{
id: 1,
display_name: "An activity",
date_deadline: moment().format("YYYY-MM-DD"), // now
state: "today",
user_id: 2,
activity_type_id: 2,
}];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<field name="m2m" widget="many2many_tags"/>' +
'<div class="oe_chatter">' +
'<field name="activity_ids" widget="mail_activity"/>' +
'</div>' +
'</form>',
res_id: 2,
});
assert.strictEqual(form.$('.o_mail_activity .o_thread_message').length, 1,
"should display an activity");
form.destroy();
});
QUnit.test('form activity widget: schedule next activity', function (assert) {
assert.expect(5);
this.data.partner.records[0].activity_ids = [1];
@@ -746,7 +868,7 @@ QUnit.test('form activity widget: schedule next activity', function (assert) {
"the feedback should be sent correctly");
return $.when();
}
if (args.method === 'read' && checkReadArgs) {
if (args.method === 'read' && args.model === 'partner' && checkReadArgs) {
assert.deepEqual(args.args[1], ['activity_ids', 'message_ids', 'display_name'],
"should only read the mail fields");
}
@@ -65,6 +65,29 @@ QUnit.test('mobile basic rendering', function (assert) {
});
});
QUnit.test('on_{attach/detach}_callback', function (assert) {
assert.expect(2);
var done = assert.async();
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
}).then(function (discuss) {
try {
discuss.on_attach_callback();
assert.ok(true, 'should not crash on attach callback');
discuss.on_detach_callback();
assert.ok(true, 'should not crash on detach callback');
} finally {
discuss.destroy();
done();
}
});
});
});
});
+2 -3
View File
@@ -44,10 +44,9 @@ QUnit.test('basic rendering', function (assert) {
assert.strictEqual($sidebar.length, 1,
"should have rendered a sidebar");
var $nocontent = discuss.$('.o_mail_chat_content');
assert.strictEqual($nocontent.length, 1,
assert.strictEqual(discuss.$('.o_mail_chat_content').length, 1,
"should have rendered the content");
assert.strictEqual($nocontent.find('.o_mail_no_content').length, 1,
assert.strictEqual(discuss.$('.o_mail_no_content').length, 1,
"should display no content message");
var $inbox = $sidebar.find('.o_mail_chat_channel_item[data-channel-id=channel_inbox]');
@@ -77,6 +77,15 @@ function createDiscuss(params) {
var selector = params.debug ? 'body' : '#qunit-fixture';
discuss.appendTo($(selector));
// override 'destroy' of discuss so that it calls 'destroy' on the parent
// instead, which is the parent of discuss and the mockServer.
discuss.destroy = function () {
// remove the override to properly destroy discuss and its children
// when it will be called the second time (by its parent)
delete discuss.destroy;
parent.destroy();
};
return discuss.call('chat_manager', 'isReady').then(function () {
return discuss;
});
+1 -1
View File
@@ -199,7 +199,7 @@ class MrpProductProduceLine(models.TransientModel):
res = {}
if self.product_id.tracking == 'serial':
if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name
res['warning'] = {'title': _('Warning'), 'message': message}
return res
+1 -1
View File
@@ -11,4 +11,4 @@ class NotePad(models.Model):
_pad_fields = ['note_pad']
note_pad_url = fields.Char('Pad Url', pad_content_field='memo')
note_pad_url = fields.Char('Pad Url', pad_content_field='memo', copy=False)
+9 -22
View File
@@ -101,16 +101,19 @@ class PadCommon(models.AbstractModel):
@api.multi
def write(self, vals):
self._set_pad_value(vals)
self._set_field_to_pad(vals)
self._set_pad_to_field(vals)
return super(PadCommon, self).write(vals)
@api.model
def create(self, vals):
self._set_pad_value(vals)
# Case of a regular creation: we receive the pad url, so we need to update the
# corresponding field
self._set_pad_to_field(vals)
pad = super(PadCommon, self).create(vals)
# In case the pad is created programmatically, the content is not filled in yet since it is
# normally initialized by the JS layer
# Case of a programmatical creation (e.g. copy): we receive the field content, so we need
# to create the corresponding pad
if self.env.context.get('pad_no_create', False):
return pad
for k, field in self._fields.items():
@@ -124,9 +127,7 @@ class PadCommon(models.AbstractModel):
pad[k] = pad_info.get('url')
return pad
# Set the pad content in vals
def _set_pad_value(self, vals):
def _set_field_to_pad(self, vals):
# Update the pad if the `pad_content_field` is modified
for k, field in self._fields.items():
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
@@ -135,23 +136,9 @@ class PadCommon(models.AbstractModel):
path = self[k].split('/p/')[1]
myPad.setHtmlFallbackText(path, vals[field.pad_content_field])
def _set_pad_to_field(self, vals):
# Update the `pad_content_field` if the pad is modified
for k, v in list(vals.items()):
field = self._fields.get(k)
if hasattr(field, 'pad_content_field'):
vals[field.pad_content_field] = self.pad_get_content(v)
@api.multi
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
self.ensure_one()
if not default:
default = {}
for k, field in self._fields.items():
if hasattr(field, 'pad_content_field'):
if self.env.context.get('pad_no_create', False):
default[k] = ''
else:
pad = self.pad_generate_url()
default[k] = pad.get('url')
return super(PadCommon, self).copy(default)
+1 -8
View File
@@ -8,7 +8,7 @@ class ProjectTask(models.Model):
_name = "project.task"
_inherit = ["project.task", 'pad.common']
description_pad = fields.Char('Pad URL', pad_content_field='description')
description_pad = fields.Char('Pad URL', pad_content_field='description', copy=False)
use_pad = fields.Boolean(related="project_id.use_pads", string="Use collaborative pad")
@api.model
@@ -19,13 +19,6 @@ class ProjectTask(models.Model):
self = self.with_context(pad_no_create=True)
return super(ProjectTask, self).create(vals)
@api.multi
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
if not self.use_pad:
self = self.with_context(pad_no_create=True)
return super(ProjectTask, self).copy(default)
class ProjectProject(models.Model):
_inherit = "project.project"
+4 -2
View File
@@ -73,7 +73,8 @@ class WebsitePayment(http.Controller):
return tx
@http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',], type='json', auth='public')
@http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',
'/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>',], type='json', auth='public')
def transaction(self, acquirer_id, reference, amount, currency_id, **kwargs):
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
@@ -100,7 +101,8 @@ class WebsitePayment(http.Controller):
return acquirer.sudo().render(reference, float(amount), int(currency_id), values=render_values)
@http.route(['/website_payment/token/<string:reference>/<float:amount>/<int:currency_id>'], type='http', auth='public', website=True)
@http.route(['/website_payment/token/<string:reference>/<string:amount>/<string:currency_id>',
'/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>'], type='http', auth='public', website=True)
def payment_token(self, pm_id, reference, amount, currency_id, return_url=None, **kwargs):
token = request.env['payment.token'].browse(int(pm_id))
+2 -11
View File
@@ -78,8 +78,8 @@ class PaymentAcquirer(models.Model):
help="Capture the amount from Odoo, when the delivery is completed.")
# Formerly associated to `generate_and_pay_invoice` option from auto_confirm
journal_id = fields.Many2one(
'account.journal', 'Payment Journal', domain=[('type', '=', 'bank')],
default=lambda self: self.env['account.journal'].search([('type', '=', 'bank')], limit=1),
'account.journal', 'Payment Journal', domain=[('type', 'in', ['bank', 'cash'])],
default=lambda self: self.env['account.journal'].search([('type', 'in', ['bank', 'cash'])], limit=1),
help="""Payments will be registered into this journal. If you get paid straight on your bank account,
select your bank account. If you get paid in batch for several transactions, create a specific
payment journal for this payment acquirer to easily manage the bank reconciliation. You hold
@@ -200,22 +200,13 @@ class PaymentAcquirer(models.Model):
@api.model
def create(self, vals):
image_resize_images(vals)
vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).create(vals)
@api.multi
def write(self, vals):
image_resize_images(vals)
vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).write(vals)
def _check_journal_id(self, vals):
if not vals.get('journal_id', False):
default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
if default_journal:
vals.update({'journal_id': default_journal.id})
return vals
@api.multi
def toggle_website_published(self):
self.write({'website_published': not self.website_published})
@@ -1,6 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
payment_acquirer_all,payment.acquirer.all,model_payment_acquirer,,1,0,0,0
payment_acquirer_user,payment.acquirer.user,model_payment_acquirer,base.group_user,1,1,1,0
payment_acquirer_system,payment.acquirer.system,model_payment_acquirer,base.group_system,1,1,1,1
payment_transaction_all,payment.transaction.all,model_payment_transaction,,1,0,0,0
payment_transaction_user,payment.transaction.user,model_payment_transaction,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 payment_acquirer_all payment.acquirer.all model_payment_acquirer 1 0 0 0
payment_acquirer_user payment.acquirer.user model_payment_acquirer base.group_user 1 1 1 0
3 payment_acquirer_system payment.acquirer.system model_payment_acquirer base.group_system 1 1 1 1
4 payment_transaction_all payment.transaction.all model_payment_transaction 1 0 0 0
5 payment_transaction_user payment.transaction.user model_payment_transaction base.group_user 1 1 1 0
@@ -55,8 +55,8 @@
<p><b>Amount:</b> <t t-esc="amount" t-options="{'widget': 'monetary', 'display_currency': currency}"/></p>
<t t-call="payment.payment_tokens_list" t-if="reference and amount and currency">
<t t-set="mode" t-value="'payment'"/>
<t t-set="prepare_tx_url" t-value="'/website_payment/transaction/' + reference + '/' + str(amount) + '/' + str(currency.id)"/>
<t t-set="form_action" t-value="'/website_payment/token/' + reference + '/' + str(amount) + '/' + str(currency.id)"/>
<t t-set="prepare_tx_url" t-value="'/website_payment/transaction/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
<t t-set="form_action" t-value="'/website_payment/token/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
</t>
<div t-if="not (s2s_acquirers or form_acquirers)" class="alert alert-danger">
<p>No payment acquirer found.</p>
+1 -1
View File
@@ -81,7 +81,7 @@
</page>
<page string="Configuration">
<group name="acquirer_config">
<field name="journal_id" context="{'default_type': 'bank'}"/>
<field name="journal_id" context="{'default_type': 'bank'}" required="1"/>
<field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/>
<field name="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/>
<field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/>
+2 -1
View File
@@ -2,6 +2,7 @@
import logging
import pprint
import werkzeug
from werkzeug.urls import url_unquote_plus
from odoo import http
from odoo.http import request
@@ -26,7 +27,7 @@ class OgoneController(http.Controller):
""" Ogone contacts using GET, at least for accept """
_logger.info('Ogone: entering form_feedback with post data %s', pprint.pformat(post)) # debug
request.env['payment.transaction'].sudo().form_feedback(post, 'ogone')
return werkzeug.utils.redirect(post.pop('return_url', '/'))
return werkzeug.utils.redirect(url_unquote_plus(post.pop('return_url', '/')))
@http.route(['/payment/ogone/s2s/create_json'], type='json', auth='public', csrf=False)
def ogone_s2s_create_json(self, **kwargs):
+14 -13
View File
@@ -15,9 +15,8 @@ from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_ogone.controllers.main import OgoneController
from odoo.addons.payment_ogone.data import ogone
from odoo.tools import float_round, DEFAULT_SERVER_DATE_FORMAT
from odoo.tools.float_utils import float_compare, float_repr
from odoo.tools.safe_eval import safe_eval
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, ustr
from odoo.tools.float_utils import float_compare, float_repr, float_round
_logger = logging.getLogger(__name__)
@@ -151,6 +150,9 @@ class PaymentAcquirerOgone(models.Model):
def ogone_form_generate_values(self, values):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
ogone_tx_values = dict(values)
param_plus = {
'return_url': ogone_tx_values.pop('return_url', False)
}
temp_ogone_tx_values = {
'PSPID': self.ogone_pspid,
'ORDERID': values['reference'],
@@ -168,7 +170,7 @@ class PaymentAcquirerOgone(models.Model):
'DECLINEURL': urls.url_join(base_url, OgoneController._decline_url),
'EXCEPTIONURL': urls.url_join(base_url, OgoneController._exception_url),
'CANCELURL': urls.url_join(base_url, OgoneController._cancel_url),
'PARAMPLUS': 'return_url=%s' % ogone_tx_values.pop('return_url') if ogone_tx_values.get('return_url') else False,
'PARAMPLUS': url_encode(param_plus),
}
if self.save_token in ['ask', 'always']:
temp_ogone_tx_values.update({
@@ -211,8 +213,8 @@ class PaymentAcquirerOgone(models.Model):
class PaymentTxOgone(models.Model):
_inherit = 'payment.transaction'
# ogone status
_ogone_valid_tx_status = [5, 9]
_ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99]
_ogone_valid_tx_status = [5, 9, 8]
_ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99, 81, 82]
_ogone_pending_tx_status = [46] # 3DS HTML response
_ogone_cancel_tx_status = [1]
@@ -411,13 +413,12 @@ class PaymentTxOgone(models.Model):
'ORDERID': reference,
'AMOUNT': int(self.amount * 100),
'CURRENCY': self.currency_id.name,
'OPERATION': 'RFD',
'ECI': 2, # Recurring (from MOTO)
'ALIAS': self.payment_token_id.acquirer_ref,
'RTIMEOUT': 30,
'OPERATION': 'RFS',
'PAYID': self.acquirer_reference,
}
data['SHASIGN'] = self.acquirer_id._ogone_generate_shasign('in', data)
direct_order_url = 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % (self.acquirer_id.environment)
direct_order_url = 'https://secure.ogone.com/ncol/%s/maintenancedirect.asp' % (self.acquirer_id.environment)
logged_data = data.copy()
logged_data.pop('PSWD')
@@ -476,7 +477,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 +586,4 @@ class PaymentToken(models.Model):
'acquirer_ref': alias,
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name'])
}
return {}
return {}
@@ -109,8 +109,8 @@ odoo.define('payment_stripe.stripe', function(require) {
if ($('.o_website_payment').length !== 0) {
var invoice_num = $("input[name='invoice_num']").val();
var url = _.str.sprintf("/website_payment/transaction/%s/%f/%s",
invoice_num, amount, currency_id);
var url = _.str.sprintf("/website_payment/transaction/v2/%f/%s/%s",
amount, currency_id, invoice_num);
var create_tx = ajax.jsonRpc(url, 'call', {
acquirer_id: acquirer_id
+1 -1
View File
@@ -52,7 +52,7 @@ designed for modern retailer.
### Designed for Productivity
Whether it's for a restaurant or a shop, you can activate the multiple tickets
Whether it's for a restaurant or a shop, you can activate the multiple orders
in parallel to not make your customers wait.
### Blazing fast search
@@ -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="uom.product_uom_kgm" />
+2 -1
View File
@@ -64,7 +64,8 @@ class PosConfig(models.Model):
return self.env['ir.qweb'].render('point_of_sale.customer_facing_display_html')
name = fields.Char(string='Point of Sale Name', index=True, required=True, help="An internal identification of the point of sale.")
is_installed_account_accountant = fields.Boolean(compute="_compute_is_installed_account_accountant")
is_installed_account_accountant = fields.Boolean(string="Is the Full Accounting Installed",
compute="_compute_is_installed_account_accountant")
journal_ids = fields.Many2many(
'account.journal', 'pos_config_journal_rel',
'pos_config_id', 'journal_id', string='Available Payment Methods',
+5 -4
View File
@@ -160,13 +160,14 @@ class PosOrder(models.Model):
"""
Prepare the dict of values to create the new invoice for a pos order.
"""
invoice_type = 'out_invoice' if self.amount_total >= 0 else 'out_refund'
return {
'name': self.name,
'origin': self.name,
'account_id': self.partner_id.property_account_receivable_id.id,
'journal_id': self.session_id.config_id.invoice_journal_id.id,
'company_id': self.company_id.id,
'type': 'out_invoice',
'type': invoice_type,
'reference': self.name,
'partner_id': self.partner_id.id,
'comment': self.note or '',
@@ -208,7 +209,7 @@ class PosOrder(models.Model):
inv_line = {
'invoice_id': invoice_id,
'product_id': line.product_id.id,
'quantity': line.qty,
'quantity': line.qty if self.amount_total >= 0 else -line.qty,
'account_analytic_id': self._prepare_analytic_account(line),
'name': inv_name,
}
@@ -732,7 +733,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
@@ -792,7 +793,7 @@ class PosOrder(models.Model):
'location_dest_id': move.location_dest_id.id,
'lot_id': lot_id,
})
if not pack_lots:
if not pack_lots and not float_is_zero(qty_done, precision_rounding=move.product_uom.rounding):
move.quantity_done = qty_done
return has_wrong_lots
+11 -5
View File
@@ -1305,6 +1305,7 @@ exports.Orderline = Backbone.Model.extend({
this.type = 'unit';
this.selected = false;
this.id = orderline_id++;
this.price_manually_set = false;
if (options.price) {
this.set_unit_price(options.price);
@@ -1345,6 +1346,7 @@ exports.Orderline = Backbone.Model.extend({
orderline.price = this.price;
orderline.type = this.type;
orderline.selected = false;
orderline.price_manually_set = this.price_manually_set;
return orderline;
},
set_product_lot: function(product){
@@ -1395,7 +1397,7 @@ exports.Orderline = Backbone.Model.extend({
}
// just like in sale.order changing the quantity will recompute the unit price
if(! keep_price){
if(! keep_price && ! this.price_manually_set){
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
this.order.fix_tax_included_price(this);
}
@@ -2232,7 +2234,11 @@ exports.Order = Backbone.Model.extend({
set_pricelist: function (pricelist) {
var self = this;
this.pricelist = pricelist;
_.each(this.get_orderlines(), function (line) {
var lines_to_recompute = _.filter(this.get_orderlines(), function (line) {
return ! line.price_manually_set;
});
_.each(lines_to_recompute, function (line) {
line.set_unit_price(line.product.get_price(self.pricelist, line.get_quantity()));
self.fix_tax_included_price(line);
});
@@ -2355,7 +2361,7 @@ exports.Order = Backbone.Model.extend({
this.assert_editable();
var newPaymentline = new exports.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos});
if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){
newPaymentline.set_amount( Math.max(this.get_due(),0) );
newPaymentline.set_amount( this.get_due() );
}
this.paymentlines.add(newPaymentline);
this.select_paymentline(newPaymentline);
@@ -2519,10 +2525,10 @@ exports.Order = Backbone.Model.extend({
}
}
}
return round_pr(Math.max(0,due), this.pos.currency.rounding);
return round_pr(due, this.pos.currency.rounding);
},
is_paid: function(){
return this.get_due() === 0;
return this.get_due() <= 0;
},
is_paid_with_cash: function(){
return !!this.paymentlines.find( function(pl){
+3 -12
View File
@@ -502,7 +502,9 @@ var OrderWidget = PosBaseWidget.extend({
}else if( mode === 'discount'){
order.get_selected_orderline().set_discount(val);
}else if( mode === 'price'){
order.get_selected_orderline().set_unit_price(val);
var selected_orderline = order.get_selected_orderline();
selected_orderline.price_manually_set = true;
selected_orderline.set_unit_price(val);
}
}
},
@@ -1973,17 +1975,6 @@ var PaymentScreenWidget = ScreenWidget.extend({
return false;
}
var plines = order.get_paymentlines();
for (var i = 0; i < plines.length; i++) {
if (plines[i].get_type() === 'bank' && plines[i].get_amount() < 0) {
this.gui.show_popup('error',{
'message': _t('Negative Bank Payment'),
'comment': _t('You cannot have a negative amount in a Bank payment. Use a cash payment method to return money to the customer.'),
});
return false;
}
}
if (!order.is_paid() || this.invoicing) {
return false;
}
+15 -2
View File
@@ -166,6 +166,18 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
}, {
content: "verify that unit price of oranges changed to $1",
trigger: ".total > .value:contains('$ 2.00')",
}, {
content: "order different oranges",
trigger: ".product:contains('Orange Butterfly')",
}, {
content: "change to price mode",
trigger: ".numpad button:contains('Price')",
}, {
content: "manually override the unit price of these oranges to $5",
trigger: ".numpad button.input-button:visible:contains('5')",
}, {
content: "change back to qty mode",
trigger: ".numpad button:contains('Qty')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
@@ -173,8 +185,9 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
content: "select public pricelist",
trigger: ".selection-item:contains('Public Pricelist')",
}, {
content: "verify that order has been recomputed",
trigger: ".total > .value:contains('$ 3.96')",
content: "verify that the boni oranges have been recomputed and the\
butterfly oranges have not (their price was manually overriden)",
trigger: ".total > .value:contains('$ 8.96')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
@@ -45,4 +45,13 @@
<field name="context">{'search_default_available_in_pos': 1, 'default_type': 'cash'}</field>
</record>
<!-- View Taxes -->
<menuitem id="menu_action_tax_form_open"
action="account.action_tax_form"
parent="point_of_sale.menu_point_config_product"
groups="base.group_no_one"
sequence="40"/>
</odoo>
+4
View File
@@ -5,6 +5,7 @@ from werkzeug.exceptions import NotFound, Forbidden
from odoo import http
from odoo.http import request
from odoo.osv import expression
from odoo.tools import consteq
@@ -95,6 +96,9 @@ class PortalChatter(http.Controller):
access_as_sudo = _has_token_access(res_model, res_id, token=kw.get('token'))
if not access_as_sudo: # if token is not correct, raise Forbidden
raise Forbidden()
# Non-employee see only messages with not internal subtype (aka, no internal logs)
if not request.env['res.users'].has_group('base.group_user'):
domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)], domain])
Message = request.env['mail.message'].sudo()
return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
@@ -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;
},

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