[MERGE] forward port branch saas-11.2 up to 42659dc70c
This commit is contained in:
@@ -4,6 +4,7 @@ include README.md
|
||||
recursive-include odoo *.css
|
||||
recursive-include odoo *.csv
|
||||
recursive-include odoo *.doc
|
||||
recursive-include odoo *.docx
|
||||
recursive-include odoo *.eml
|
||||
recursive-include odoo *.eot
|
||||
recursive-include odoo *.gif
|
||||
@@ -16,6 +17,8 @@ recursive-include odoo *.less
|
||||
recursive-include odoo *.md
|
||||
recursive-include odoo *.mp3
|
||||
recursive-include odoo *.ogg
|
||||
recursive-include odoo *.ods
|
||||
recursive-include odoo *.odt
|
||||
recursive-include odoo *.otf
|
||||
recursive-include odoo *.pdf
|
||||
recursive-include odoo *.png
|
||||
@@ -35,8 +38,10 @@ recursive-include odoo *.wsdl
|
||||
recursive-include odoo *.xls
|
||||
recursive-include odoo *.xsd
|
||||
recursive-include odoo *.xsl
|
||||
recursive-include odoo *.xlsx
|
||||
recursive-include odoo *.xml
|
||||
recursive-include odoo *.yml
|
||||
recursive-include odoo *.zip
|
||||
recursive-include odoo/addons/l10n_mx_edi *.xslt *.key *.cer *.txt
|
||||
recursive-include odoo/addons/l10n_mx_reports *.xslt
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
@@ -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.')
|
||||
@@ -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},
|
||||
])
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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="['&', ('state','in',['draft','open','paid']),('type','in',('out_invoice','in_invoice'))]"/>
|
||||
<filter name="refunds" string="Credit Notes" domain="['&', ('state','in',['draft','open','paid']),('type','in',('out_refund','in_refund'))]"/>
|
||||
<separator/>
|
||||
<filter name="unpaid" string="Not Paid" domain="[('state','=','open')]"/>
|
||||
<filter name="unpaid" string="Open" domain="[('state', '=', 'open')]"/>
|
||||
<filter name="paid" string="Paid" domain="[('state', '=', 'paid')]"/>
|
||||
<filter name="late" string="Overdue" domain="['&', ('date_due', '<', time.strftime('%%Y-%%m-%%d')), ('state', '=', 'open')]" help="Overdue invoices, maturity date passed"/>
|
||||
<separator/>
|
||||
<field name="partner_id" operator="child_of"/>
|
||||
@@ -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>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
id="action_report_financial"
|
||||
model="account.financial.report"
|
||||
string="Financial report"
|
||||
menu="False"
|
||||
report_type="qweb-pdf"
|
||||
name="account.report_financial"
|
||||
file="account.report_financial"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<field name="code" placeholder="code"/>
|
||||
<field name="name"/>
|
||||
<field name="user_type_id" widget="selection"/>
|
||||
<field name="tax_ids" widget="many2many_tags"/>
|
||||
<field name="tax_ids" widget="many2many_tags" domain="[('company_id','=',company_id)]"/>
|
||||
<field name="tag_ids" widget="many2many_tags" domain="[('applicability', '!=', 'taxes')]" context="{'default_applicability': 'accounts'}" options="{'no_create_edit': True}"/>
|
||||
<field name="group_id"/>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,3 +11,12 @@ def uninstall_hook(cr, registry):
|
||||
('module', '=', 'account')
|
||||
]).mapped('res_id')
|
||||
env['ir.ui.menu'].browse(res_ids).update({'active': False})
|
||||
|
||||
|
||||
def post_init_hook(cr, registry):
|
||||
env = Environment(cr, SUPERUSER_ID, {})
|
||||
res_ids = env['ir.model.data'].search([
|
||||
('model', '=', 'ir.ui.menu'),
|
||||
('module', '=', 'account'),
|
||||
]).mapped('res_id')
|
||||
env['ir.ui.menu'].browse(res_ids).update({'active': True})
|
||||
|
||||
@@ -28,4 +28,5 @@ You could use this simplified accounting in case you work with an (external) acc
|
||||
],
|
||||
'application': True,
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
'post_init_hook': 'post_init_hook',
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ from . import res_config_settings
|
||||
from . import ir_http
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import ir_model_fields
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
# This is a nasty hack, targeted for V11 only
|
||||
class IrModelFields(models.Model):
|
||||
_inherit = 'ir.model.fields'
|
||||
|
||||
def unlink(self):
|
||||
# Prevent the deletion of some `shared` fields... -_-
|
||||
self = self.filtered(
|
||||
lambda rec: not (
|
||||
rec.model == 'res.config.settings'
|
||||
and rec.name == 'auth_signup_uninvited'
|
||||
)
|
||||
)
|
||||
return super(IrModelFields, self).unlink()
|
||||
@@ -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
|
||||
|
||||
@@ -302,7 +302,8 @@ class Import(models.TransientModel):
|
||||
try:
|
||||
thousand_separator = decimal_separator = False
|
||||
for val in preview_values:
|
||||
if val == '':
|
||||
val = val.strip()
|
||||
if not val:
|
||||
continue
|
||||
# value might have the currency symbol left or right from the value
|
||||
val = self._remove_currency_symbol(val)
|
||||
@@ -587,6 +588,7 @@ class Import(models.TransientModel):
|
||||
thousand_separator = options.get('float_thousand_separator', ' ')
|
||||
decimal_separator = options.get('float_decimal_separator', '.')
|
||||
for line in data:
|
||||
line[index] = line[index].strip()
|
||||
if not line[index]:
|
||||
continue
|
||||
line[index] = line[index].replace(thousand_separator, '').replace(decimal_separator, '.')
|
||||
|
||||
@@ -145,12 +145,13 @@ def _is_studio_custom(path):
|
||||
Returns True if any of the records contains a context with the key
|
||||
studio in it, False if none of the records do
|
||||
"""
|
||||
path = os.path.join(path, 'data')
|
||||
filenames = next(iter(os.walk(path)))[2]
|
||||
filenames = [f for f in filenames if f.lower().endswith('.xml')]
|
||||
filepaths = []
|
||||
for level in os.walk(path):
|
||||
filepaths += [os.path.join(level[0], fn) for fn in level[2]]
|
||||
filepaths = [fp for fp in filepaths if fp.lower().endswith('.xml')]
|
||||
|
||||
for filename in filenames:
|
||||
root = lxml.etree.parse(os.path.join(path, filename)).getroot()
|
||||
for fp in filepaths:
|
||||
root = lxml.etree.parse(fp).getroot()
|
||||
|
||||
for record in root:
|
||||
# there might not be a context if it's a non-studio module
|
||||
|
||||
@@ -2,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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -84,18 +84,6 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:15px 20px 0px 20px;">
|
||||
<table style="width:100%;border-top:1px solid #e1e1e1;">
|
||||
<tr>
|
||||
<td style="padding:25px 0px; text-align:center;">
|
||||
<p>
|
||||
Don't forget to <strong>add it to your calendar</strong>:
|
||||
</p>
|
||||
<a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
|
||||
<a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
|
||||
<a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="width:100%;border-top:1px solid #e1e1e1;">
|
||||
<tr>
|
||||
<td style="padding:25px 0px;vertical-align:top;">
|
||||
@@ -165,6 +153,16 @@
|
||||
</tr>
|
||||
</table>
|
||||
% endif
|
||||
<table style="width:100%;border-top:1px solid #e1e1e1;">
|
||||
<tr>
|
||||
<td style="padding:25px 0px;">
|
||||
<strong>Add this event to your calendar</strong>
|
||||
<a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
|
||||
<a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
|
||||
<a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -247,9 +247,11 @@ class FleetVehicle(models.Model):
|
||||
@return: the costs log view
|
||||
"""
|
||||
self.ensure_one()
|
||||
copy_context = dict(self.env.context)
|
||||
copy_context.pop('group_by', None)
|
||||
res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
|
||||
res.update(
|
||||
context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True),
|
||||
context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True),
|
||||
domain=[('vehicle_id', '=', self.id)]
|
||||
)
|
||||
return res
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', '<=', 0)]}"/>
|
||||
<widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '>=', 1)]}"/>
|
||||
<button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '>=', 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', '<=', 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', '<=', 0)]}"/>
|
||||
<widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '>=', 1)]}"/>
|
||||
<button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '>=', 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', '<=', 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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
<field name="name">DIOT: 16%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_diot_16_imp" model="account.account.tag">
|
||||
<field name="name">DIOT: 16% IMP</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_diot_0" model="account.account.tag">
|
||||
<field name="name">DIOT: 0%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
|
||||
@@ -5,18 +5,25 @@
|
||||
<!-- Smileys -->
|
||||
<record id="mail_smiley_normal_smile" model="mail.shortcode">
|
||||
<field name="source">:)</field>
|
||||
<field name="unicode_source">😊</field>
|
||||
<field name="substitution"><img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/></field>
|
||||
<field name="unicode_source">🙂</field>
|
||||
<field name="substitution"><img src='/mail/static/src/img/smiley/smile.png' alt='🙂'/></field>
|
||||
<field name="description">Smile</field>
|
||||
<field name="shortcode_type">image</field>
|
||||
</record>
|
||||
<record id="mail_smiley_smile" model="mail.shortcode">
|
||||
<field name="source">:D</field>
|
||||
<field name="unicode_source">😄</field>
|
||||
<field name="substitution"><img src='/mail/static/src/img/smiley/smile.png' alt='😄'/></field>
|
||||
<field name="unicode_source">😀</field>
|
||||
<field name="substitution"><img src='/mail/static/src/img/smiley/smile_open_mouth.png' alt='😀'/></field>
|
||||
<field name="description">Smile with open mouth</field>
|
||||
<field name="shortcode_type">image</field>
|
||||
</record>
|
||||
<record id="mail_smiley_smile_eyes" model="mail.shortcode">
|
||||
<field name="source">^^</field>
|
||||
<field name="unicode_source">😊</field>
|
||||
<field name="substitution"><img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/></field>
|
||||
<field name="description">Smile with with smiling eyes</field>
|
||||
<field name="shortcode_type">image</field>
|
||||
</record>
|
||||
<record id="mail_smiley_laugh" model="mail.shortcode">
|
||||
<field name="source">xD</field>
|
||||
<field name="unicode_source">😆</field>
|
||||
@@ -25,7 +32,7 @@
|
||||
<field name="shortcode_type">image</field>
|
||||
</record>
|
||||
<record id="mail_smiley_tears_of_joy" model="mail.shortcode">
|
||||
<field name="source">:-)</field>
|
||||
<field name="source">:joy</field>
|
||||
<field name="unicode_source">😂</field>
|
||||
<field name="substitution"><img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/></field>
|
||||
<field name="description">Tears of joy</field>
|
||||
|
||||
@@ -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). """
|
||||
|
||||
@@ -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 |
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1305,6 +1305,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
this.type = 'unit';
|
||||
this.selected = false;
|
||||
this.id = orderline_id++;
|
||||
this.price_manually_set = false;
|
||||
|
||||
if (options.price) {
|
||||
this.set_unit_price(options.price);
|
||||
@@ -1345,6 +1346,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
orderline.price = this.price;
|
||||
orderline.type = this.type;
|
||||
orderline.selected = false;
|
||||
orderline.price_manually_set = this.price_manually_set;
|
||||
return orderline;
|
||||
},
|
||||
set_product_lot: function(product){
|
||||
@@ -1395,7 +1397,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
}
|
||||
|
||||
// just like in sale.order changing the quantity will recompute the unit price
|
||||
if(! keep_price){
|
||||
if(! keep_price && ! this.price_manually_set){
|
||||
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
|
||||
this.order.fix_tax_included_price(this);
|
||||
}
|
||||
@@ -2232,7 +2234,11 @@ exports.Order = Backbone.Model.extend({
|
||||
set_pricelist: function (pricelist) {
|
||||
var self = this;
|
||||
this.pricelist = pricelist;
|
||||
_.each(this.get_orderlines(), function (line) {
|
||||
|
||||
var lines_to_recompute = _.filter(this.get_orderlines(), function (line) {
|
||||
return ! line.price_manually_set;
|
||||
});
|
||||
_.each(lines_to_recompute, function (line) {
|
||||
line.set_unit_price(line.product.get_price(self.pricelist, line.get_quantity()));
|
||||
self.fix_tax_included_price(line);
|
||||
});
|
||||
@@ -2355,7 +2361,7 @@ exports.Order = Backbone.Model.extend({
|
||||
this.assert_editable();
|
||||
var newPaymentline = new exports.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos});
|
||||
if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){
|
||||
newPaymentline.set_amount( Math.max(this.get_due(),0) );
|
||||
newPaymentline.set_amount( this.get_due() );
|
||||
}
|
||||
this.paymentlines.add(newPaymentline);
|
||||
this.select_paymentline(newPaymentline);
|
||||
@@ -2519,10 +2525,10 @@ exports.Order = Backbone.Model.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
return round_pr(Math.max(0,due), this.pos.currency.rounding);
|
||||
return round_pr(due, this.pos.currency.rounding);
|
||||
},
|
||||
is_paid: function(){
|
||||
return this.get_due() === 0;
|
||||
return this.get_due() <= 0;
|
||||
},
|
||||
is_paid_with_cash: function(){
|
||||
return !!this.paymentlines.find( function(pl){
|
||||
|
||||
@@ -502,7 +502,9 @@ var OrderWidget = PosBaseWidget.extend({
|
||||
}else if( mode === 'discount'){
|
||||
order.get_selected_orderline().set_discount(val);
|
||||
}else if( mode === 'price'){
|
||||
order.get_selected_orderline().set_unit_price(val);
|
||||
var selected_orderline = order.get_selected_orderline();
|
||||
selected_orderline.price_manually_set = true;
|
||||
selected_orderline.set_unit_price(val);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1973,17 +1975,6 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
||||
return false;
|
||||
}
|
||||
|
||||
var plines = order.get_paymentlines();
|
||||
for (var i = 0; i < plines.length; i++) {
|
||||
if (plines[i].get_type() === 'bank' && plines[i].get_amount() < 0) {
|
||||
this.gui.show_popup('error',{
|
||||
'message': _t('Negative Bank Payment'),
|
||||
'comment': _t('You cannot have a negative amount in a Bank payment. Use a cash payment method to return money to the customer.'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!order.is_paid() || this.invoicing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -166,6 +166,18 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
|
||||
}, {
|
||||
content: "verify that unit price of oranges changed to $1",
|
||||
trigger: ".total > .value:contains('$ 2.00')",
|
||||
}, {
|
||||
content: "order different oranges",
|
||||
trigger: ".product:contains('Orange Butterfly')",
|
||||
}, {
|
||||
content: "change to price mode",
|
||||
trigger: ".numpad button:contains('Price')",
|
||||
}, {
|
||||
content: "manually override the unit price of these oranges to $5",
|
||||
trigger: ".numpad button.input-button:visible:contains('5')",
|
||||
}, {
|
||||
content: "change back to qty mode",
|
||||
trigger: ".numpad button:contains('Qty')",
|
||||
}, {
|
||||
content: "click pricelist button",
|
||||
trigger: ".control-button.o_pricelist_button",
|
||||
@@ -173,8 +185,9 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
|
||||
content: "select public pricelist",
|
||||
trigger: ".selection-item:contains('Public Pricelist')",
|
||||
}, {
|
||||
content: "verify that order has been recomputed",
|
||||
trigger: ".total > .value:contains('$ 3.96')",
|
||||
content: "verify that the boni oranges have been recomputed and the\
|
||||
butterfly oranges have not (their price was manually overriden)",
|
||||
trigger: ".total > .value:contains('$ 8.96')",
|
||||
}, {
|
||||
content: "click pricelist button",
|
||||
trigger: ".control-button.o_pricelist_button",
|
||||
|
||||
@@ -45,4 +45,13 @@
|
||||
<field name="context">{'search_default_available_in_pos': 1, 'default_type': 'cash'}</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- View Taxes -->
|
||||
|
||||
<menuitem id="menu_action_tax_form_open"
|
||||
action="account.action_tax_form"
|
||||
parent="point_of_sale.menu_point_config_product"
|
||||
groups="base.group_no_one"
|
||||
sequence="40"/>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user