[MERGE] Forward port of saas-12.4 to master up to 1f5a4649a6
closes odoo/odoo#34850 Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
This commit is contained in:
@@ -1358,7 +1358,7 @@ class AccountTax(models.Model):
|
||||
base = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount)
|
||||
incl_fixed_amount = incl_percent_amount = incl_division_amount = 0
|
||||
store_included_tax_total = True
|
||||
if tax.price_include:
|
||||
if tax.price_include or self._context.get('force_price_include'):
|
||||
if tax.amount_type == 'percent':
|
||||
incl_percent_amount += tax.amount
|
||||
elif tax.amount_type == 'division':
|
||||
@@ -1387,7 +1387,7 @@ class AccountTax(models.Model):
|
||||
cumulated_tax_included_amount = 0
|
||||
for tax in taxes:
|
||||
#compute the tax_amount
|
||||
if tax.price_include and total_included_checkpoints.get(i):
|
||||
if (self._context.get('force_price_include') or tax.price_include) and total_included_checkpoints.get(i):
|
||||
# We know the total to reach for that tax, so we make a substraction to avoid any rounding issues
|
||||
tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount)
|
||||
cumulated_tax_included_amount = 0
|
||||
@@ -1426,7 +1426,7 @@ class AccountTax(models.Model):
|
||||
'sequence': tax.sequence,
|
||||
'account_id': tax.cash_basis_transition_account_id.id if tax.tax_exigibility == 'on_payment' else repartition_line.account_id.id,
|
||||
'analytic': tax.analytic,
|
||||
'price_include': tax.price_include,
|
||||
'price_include': tax.price_include or self._context.get('force_price_include'),
|
||||
'tax_exigibility': tax.tax_exigibility,
|
||||
'tax_repartition_line_id': repartition_line.id,
|
||||
'tag_ids': (repartition_line.tag_ids + subsequent_tags).ids,
|
||||
|
||||
@@ -1024,7 +1024,7 @@ class AccountMove(models.Model):
|
||||
vendor_display_name = _('From: ') + move.invoice_source_email
|
||||
move.invoice_vendor_icon = '@'
|
||||
else:
|
||||
vendor_display_name = ('Created by: ') + move.create_uid.name
|
||||
vendor_display_name = ('Created by: ') + move.sudo().create_uid.name
|
||||
move.invoice_vendor_icon = '#'
|
||||
move.invoice_vendor_display_name = vendor_display_name
|
||||
|
||||
@@ -1877,6 +1877,9 @@ class AccountMove(models.Model):
|
||||
for move in self:
|
||||
if move.auto_post and move.date > fields.Date.today():
|
||||
raise UserError(_("This move is configured to be auto-posted on {}".format(move.date.strftime(self.env['res.lang']._lang_get(self.env.user.lang).date_format))))
|
||||
|
||||
move.message_subscribe([p.id for p in [move.partner_id, move.commercial_partner_id] if p not in move.message_partner_ids])
|
||||
|
||||
to_write = {'state': 'posted'}
|
||||
|
||||
if move.name == '/':
|
||||
@@ -2091,6 +2094,18 @@ class AccountMove(models.Model):
|
||||
])
|
||||
records.post()
|
||||
|
||||
# offer the possibility to duplicate thanks to a button instead of a hidden menu, which is more visible
|
||||
@api.multi
|
||||
def action_duplicate(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('account.action_move_journal_line').read()[0]
|
||||
action['context'] = dict(self.env.context)
|
||||
action['context']['form_view_initial_mode'] = 'edit'
|
||||
action['context']['view_no_maturity'] = False
|
||||
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
|
||||
action['res_id'] = self.copy().id
|
||||
return action
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_name = "account.move.line"
|
||||
@@ -3536,19 +3551,6 @@ class AccountMoveLine(models.Model):
|
||||
tables, where_clause, where_clause_params = query.get_sql()
|
||||
return tables, where_clause, where_clause_params
|
||||
|
||||
# FIXME: Clarify me and change me in master
|
||||
@api.multi
|
||||
def action_duplicate(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('account.action_move_journal_line').read()[0]
|
||||
action['target'] = 'inline'
|
||||
action['context'] = dict(self.env.context)
|
||||
action['context']['form_view_initial_mode'] = 'edit'
|
||||
action['context']['view_no_maturity'] = False
|
||||
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
|
||||
action['res_id'] = self.copy().id
|
||||
return action
|
||||
|
||||
def _reconciled_lines(self):
|
||||
ids = []
|
||||
for aml in self.filtered('account_id.reconcile'):
|
||||
|
||||
@@ -174,19 +174,23 @@ class AccountReconcileModel(models.Model):
|
||||
tax = self.env['account.tax'].browse(tax_res['id'])
|
||||
|
||||
new_aml_dicts.append({
|
||||
'account_id': tax.account_id and tax.account_id.id or base_line_dict['account_id'],
|
||||
'name': tax.name,
|
||||
'account_id': tax_res['account_id'] or base_line_dict['account_id'],
|
||||
'name': tax_res['name'],
|
||||
'partner_id': base_line_dict.get('partner_id'),
|
||||
'debit': tax_res['amount'] > 0 and tax_res['amount'] or 0,
|
||||
'credit': tax_res['amount'] < 0 and -tax_res['amount'] or 0,
|
||||
'analytic_account_id': tax.analytic and base_line_dict['analytic_account_id'],
|
||||
'analytic_tag_ids': tax.analytic and base_line_dict['analytic_tag_ids'],
|
||||
'tax_exigible': tax.tax_exigibility == 'on_payment',
|
||||
'tax_exigible': tax_res['tax_exigibility'],
|
||||
'tax_repartition_line_id': tax_res['tax_repartition_line_id'],
|
||||
'tax_ids': tax_res['tax_ids'],
|
||||
'tag_ids': tax_res['tag_ids']
|
||||
})
|
||||
|
||||
# Handle price included taxes.
|
||||
base_line_dict['debit'] = tax_res['base'] > 0 and tax_res['base'] or base_line_dict['debit']
|
||||
base_line_dict['credit'] = tax_res['base'] < 0 and -tax_res['base'] or base_line_dict['credit']
|
||||
base_line_dict['tag_ids'] = [(6, 0, res['base_tags'])]
|
||||
return new_aml_dicts
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -262,9 +262,9 @@ class ResPartner(models.Model):
|
||||
LEFT JOIN account_move_line aml ON aml.partner_id = partner.id
|
||||
RIGHT JOIN account_account acc ON aml.account_id = acc.id
|
||||
WHERE acc.internal_type = %s
|
||||
AND NOT acc.deprecated
|
||||
AND NOT acc.deprecated AND acc.company_id = %s
|
||||
GROUP BY partner.id
|
||||
HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, sign, operand))
|
||||
HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.user.company_id.id, sign, operand))
|
||||
res = self._cr.fetchall()
|
||||
if not res:
|
||||
return [('id', '=', '0')]
|
||||
@@ -282,7 +282,6 @@ class ResPartner(models.Model):
|
||||
def _invoice_total(self):
|
||||
account_invoice_report = self.env['account.invoice.report']
|
||||
if not self.ids:
|
||||
self.total_invoiced = 0.0
|
||||
return True
|
||||
|
||||
user_currency_id = self.env.company.currency_id.id
|
||||
|
||||
@@ -215,7 +215,8 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
total[(i + 1)] += values['total']
|
||||
values['partner_id'] = partner['partner_id']
|
||||
if partner['partner_id']:
|
||||
browsed_partner = self.env['res.partner'].browse(partner['partner_id'])
|
||||
#browse the partner name and trust field in sudo, as we may not have full access to the record (but we still have to see it in the report)
|
||||
browsed_partner = self.env['res.partner'].sudo().browse(partner['partner_id'])
|
||||
values['name'] = browsed_partner.name and len(browsed_partner.name) >= 45 and browsed_partner.name[0:40] + '...' or browsed_partner.name
|
||||
values['trust'] = browsed_partner.trust
|
||||
else:
|
||||
|
||||
@@ -94,6 +94,12 @@ class TestReconciliationMatchingRules(AccountingTestCase):
|
||||
'sequence': 1,
|
||||
})
|
||||
|
||||
self.tax21 = self.env['account.tax'].create({
|
||||
'name': '21%',
|
||||
'type_tax_use': 'purchase',
|
||||
'amount': 21,
|
||||
})
|
||||
|
||||
def test_matching_fields(self):
|
||||
''' Test all fields used to restrict the rules's applicability.'''
|
||||
|
||||
@@ -300,6 +306,34 @@ class TestReconciliationMatchingRules(AccountingTestCase):
|
||||
{'partner_id': self.partner_2.id, 'debit': 0.0, 'credit': 1000.0},
|
||||
])
|
||||
|
||||
def test_auto_reconcile_with_tax(self):
|
||||
''' Test auto reconciliation with a tax amount included in the bank statement line'''
|
||||
|
||||
self.rule_1.write({
|
||||
'auto_reconcile': True,
|
||||
'force_tax_included': True,
|
||||
'tax_ids': [(6, 0, self.tax21.ids)],
|
||||
'rule_type': 'writeoff_suggestion',
|
||||
})
|
||||
|
||||
self.bank_line_2.unlink()
|
||||
self.bank_line_1.amount = -121
|
||||
|
||||
self._check_statement_matching(
|
||||
self.rule_1,
|
||||
{
|
||||
self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled'},
|
||||
},
|
||||
self.bank_st
|
||||
)
|
||||
|
||||
# Check first line has been well reconciled.
|
||||
self.assertRecordValues(self.bank_line_1.journal_entry_ids, [
|
||||
{'partner_id': self.partner_1.id, 'debit': 100.0, 'credit': 0.0, 'tax_ids': [self.tax21.id]},
|
||||
{'partner_id': self.partner_1.id, 'debit': 21.0, 'credit': 0.0, 'tax_line_id': self.tax21.id},
|
||||
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0},
|
||||
])
|
||||
|
||||
def test_reverted_move_matching(self):
|
||||
AccountMove = self.env['account.move']
|
||||
move = AccountMove.create({
|
||||
|
||||
@@ -761,6 +761,26 @@
|
||||
<field name="exclude_from_invoice_tab" invisible="1"/>
|
||||
<field name="account_internal_type" invisible="1"/>
|
||||
</tree>
|
||||
<!-- Form view to cover mobile use -->
|
||||
<form>
|
||||
<group>
|
||||
<field name="tax_line_id" invisible="1"/>
|
||||
<field name="account_id" domain="[('company_id', '=', parent.company_id), ('deprecated', '=', False)]"/>
|
||||
<field name="partner_id" domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]"/>
|
||||
<field name="name"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
|
||||
<field name="amount_currency" groups="base.group_multi_currency"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="currency_id" options="{'no_create': True}" groups="base.group_multi_currency"/>
|
||||
<field name="debit" sum="Total Debit"/>
|
||||
<field name="credit" sum="Total Credit"/>
|
||||
<field name="tax_ids" string="Taxes Applied" widget="many2many_tags" options="{'no_create': True}"/>
|
||||
<field name="date_maturity" required="0" invisible="context.get('view_no_maturity', False)"/>
|
||||
<field name="recompute_tax_line" invisible="1" readonly="1"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page id="other_tab" string="Other Info" name="other_info"
|
||||
@@ -929,7 +949,8 @@
|
||||
view_id="account.view_move_line_tax_audit_tree"/>
|
||||
|
||||
<record id="action_account_moves_all_a" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_sales':1, 'name_groupby':1}</field> <field name="name">Journal Items</field>
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'search_default_posted':1, 'search_default_sales':1, 'name_groupby':1}</field>
|
||||
<field name="name">Journal Items</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
<field name="view_id" ref="view_move_line_tree_grouped"/>
|
||||
@@ -937,7 +958,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_sales" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'sales', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_sales':1, 'name_groupby':1}</field>
|
||||
<field name="context">{'journal_type':'sales', 'search_default_group_by_move': 1, 'search_default_posted':1, 'search_default_sales':1, 'name_groupby':1}</field>
|
||||
<field name="name">Sales</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -946,7 +967,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_purchase" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'purchase', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_purchases':1, 'name_groupby':1}</field>
|
||||
<field name="context">{'journal_type':'purchase', 'search_default_group_by_move': 1, 'search_default_posted':1, 'search_default_purchases':1, 'name_groupby':1}</field>
|
||||
<field name="name">Purchase</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -955,7 +976,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_bank_cash" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'bank', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_bank':1, 'search_default_cash':1, 'name_groupby':1}</field>
|
||||
<field name="context">{'journal_type':'bank', 'search_default_group_by_move': 1, 'search_default_posted':1, 'search_default_bank':1, 'search_default_cash':1, 'name_groupby':1}</field>
|
||||
<field name="name">Bank and Cash</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -964,7 +985,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_misc" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_misc_filter':1, 'name_groupby':1}</field>
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'search_default_posted':1, 'search_default_misc_filter':1, 'name_groupby':1}</field>
|
||||
<field name="name">Miscellaneous</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -973,7 +994,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_ledger_general" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_account': 1, 'group_by':'account_id', 'search_default_posted':1}</field>
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_account': 1, 'search_default_posted':1}</field>
|
||||
<field name="name">General Ledger</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -982,7 +1003,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_ledger_partner" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_partner': 1, 'group_by':'partner_id', 'search_default_posted':1, 'search_default_payable':1, 'search_default_receivable':1, 'search_default_unreconciled':1}</field>
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_partner': 1, 'search_default_posted':1, 'search_default_payable':1, 'search_default_receivable':1, 'search_default_unreconciled':1}</field>
|
||||
<field name="name">Partner Ledger</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -991,7 +1012,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_bank_cash" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'bank', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_bank':1, 'search_default_cash':1, 'name_groupby':1}</field>
|
||||
<field name="context">{'journal_type':'bank', 'search_default_group_by_move': 1, 'search_default_posted':1, 'search_default_bank':1, 'search_default_cash':1, 'name_groupby':1}</field>
|
||||
<field name="name">Bank and Cash</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -1000,7 +1021,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_misc" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_misc_filter':1, 'name_groupby':1}</field>
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'search_default_posted':1, 'search_default_misc_filter':1, 'name_groupby':1}</field>
|
||||
<field name="name">Miscellaneous</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -1009,7 +1030,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_ledger_general" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_account': 1, 'group_by':'account_id', 'search_default_posted':1}</field>
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_account': 1, 'search_default_posted':1}</field>
|
||||
<field name="name">General Ledger</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
@@ -1018,7 +1039,7 @@
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_ledger_partner" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_partner': 1, 'group_by':'partner_id', 'search_default_posted':1, 'search_default_payable':1, 'search_default_receivable':1, 'search_default_unreconciled':1}</field>
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_partner': 1, 'search_default_posted':1, 'search_default_payable':1, 'search_default_receivable':1, 'search_default_unreconciled':1}</field>
|
||||
<field name="name">Partner Ledger</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<field name="composer_id" invisible="1"/>
|
||||
<field name="invoice_ids" invisible="1"/>
|
||||
<field name="email_from" invisible="1" />
|
||||
<field name="mail_server_id" invisible="1"/>
|
||||
<div name="option_print">
|
||||
<field name="is_print" />
|
||||
<b><label for="is_print"/></b>
|
||||
|
||||
@@ -20,7 +20,7 @@ class PaymentTransaction(models.Model):
|
||||
values.update(render_values)
|
||||
return self.acquirer_id.with_context(submit_class='btn btn-primary', submit_txt=submit_txt or _('Pay Now')).sudo().render(
|
||||
self.reference,
|
||||
invoice.residual_signed,
|
||||
invoice.amount_residual_signed,
|
||||
invoice.currency_id.id,
|
||||
values=values,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<td class="text-center">
|
||||
<t t-set="tx_ids" t-value="invoice.transaction_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
|
||||
<t t-set="pending_manual_txs" t-value="tx_ids.filtered(lambda tx: tx.state == 'pending' and tx.acquirer_id.provider in ('transfer', 'manual'))"/>
|
||||
<a t-if="invoice.state == 'open' and invoice.amount_total and invoice.type == 'out_invoice' and (pending_manual_txs or not tx_ids)"
|
||||
<a t-if="invoice.state == 'posted' and invoice.invoice_payment_state == 'not_paid' and invoice.amount_total and invoice.type == 'out_invoice' and (pending_manual_txs or not tx_ids)"
|
||||
t-att-href="invoice.get_portal_url(anchor='portal_pay')" title="Pay now" aria-label="Pay now" class="btn btn-sm btn-primary" role="button">
|
||||
<i class="fa fa-arrow-circle-right"/><span class='d-none d-md-inline'> Pay Now</span>
|
||||
</a>
|
||||
@@ -16,16 +16,16 @@
|
||||
<xpath expr="//t[@t-foreach='invoices']/tr/td[hasclass('tx_status')]" position="replace">
|
||||
<t t-set="last_tx" t-value="invoice.get_portal_last_transaction()"/>
|
||||
<td class="tx_status text-center">
|
||||
<t t-if="invoice.state == 'open' and (last_tx.state not in ['pending', 'authorized', 'done', 'cancel'] or (last_tx.state == 'pending' and last_tx.acquirer_id.provider in ('transfer', 'manual')))">
|
||||
<t t-if="invoice.state == 'posted' and invoice.invoice_payment_state == 'not_paid' and (last_tx.state not in ['pending', 'authorized', 'done', 'cancel'] or (last_tx.state == 'pending' and last_tx.acquirer_id.provider in ('transfer', 'manual')))">
|
||||
<span class="badge badge-pill badge-info"><i class="fa fa-fw fa-clock-o"></i><span class="d-none d-md-inline"> Waiting for Payment</span></span>
|
||||
</t>
|
||||
<t t-if="invoice.state == 'open' and last_tx.state == 'authorized'">
|
||||
<t t-if="invoice.state == 'posted' and last_tx.state == 'authorized'">
|
||||
<span class="badge badge-pill badge-primary"><i class="fa fa-fw fa-check"/><span class="d-none d-md-inline"> Authorized</span></span>
|
||||
</t>
|
||||
<t t-if="invoice.state == 'open' and last_tx.state == 'pending' and last_tx.acquirer_id.provider not in ('transfer', 'manual')">
|
||||
<t t-if="invoice.state == 'posted' and last_tx.state == 'pending' and last_tx.acquirer_id.provider not in ('transfer', 'manual')">
|
||||
<span class="badge badge-pill badge-warning"><span class="d-none d-md-inline"> Pending</span></span>
|
||||
</t>
|
||||
<t t-if="invoice.state == 'paid' or last_tx.state == 'done'">
|
||||
<t t-if="invoice.state == 'posted' and invoice.invoice_payment_state == 'paid' or last_tx.state == 'done'">
|
||||
<span class="badge badge-pill badge-success"><i class="fa fa-fw fa-check"></i><span class="d-none d-md-inline"> Paid</span></span>
|
||||
</t>
|
||||
<t t-if="invoice.state == 'cancel'">
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<template id="portal_invoice_payment" name="Invoice Payment">
|
||||
<div class="row" t-if="not tx_ids and invoice.state == 'open' and invoice.amount_total" id="portal_pay">
|
||||
<div class="row" t-if="not tx_ids and invoice.state == 'posted' and invoice.invoice_payment_state == 'not_paid' and invoice.amount_total" id="portal_pay">
|
||||
<div class="modal fade" id="pay_with" role="dialog">
|
||||
<div class="modal-dialog modal-content">
|
||||
<div class="modal-content">
|
||||
@@ -71,14 +71,14 @@
|
||||
<t t-set="tx_ids" t-value="invoice.transaction_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
|
||||
<t t-set="pending_manual_txs" t-value="tx_ids.filtered(lambda tx: tx.state == 'pending' and tx.acquirer_id.provider in ('transfer', 'manual'))"/>
|
||||
<div>
|
||||
<a href="#" t-if="invoice.state == 'open' and invoice.amount_total and invoice.type == 'out_invoice' and (pending_manual_txs or not tx_ids)"
|
||||
<a href="#" t-if="invoice.state == 'posted' and invoice.invoice_payment_state == 'not_paid' and invoice.amount_total and invoice.type == 'out_invoice' and (pending_manual_txs or not tx_ids)"
|
||||
class="btn btn-primary btn-block mb-2" data-toggle="modal" data-target="#pay_with">
|
||||
<i class="fa fa-fw fa-arrow-circle-right"/> Pay Now
|
||||
</a>
|
||||
<div t-if="tx_ids and not pending_manual_txs and invoice.state != 'paid'" class="alert alert-info py-1 mb-2" >
|
||||
<div t-if="tx_ids and not pending_manual_txs and invoice.invoice_payment_state != 'paid'" class="alert alert-info py-1 mb-2" >
|
||||
<i class="fa fa-fw fa-check-circle"/> Pending
|
||||
</div>
|
||||
<div t-if="invoice.state == 'paid'" class="alert alert-success py-1 mb-2" >
|
||||
<div t-if="invoice.invoice_payment_state == 'paid'" class="alert alert-success py-1 mb-2" >
|
||||
<i class="fa fa-fw fa-check-circle"/> Paid
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,11 +87,11 @@
|
||||
<div t-if="invoice.transaction_ids and invoice.amount_total and not success and not error" class="o_account_payment_tx_status" t-att-data-invoice-id="invoice.id">
|
||||
<t t-call="payment.payment_confirmation_status">
|
||||
<t t-set="payment_tx_id" t-value="invoice.get_portal_last_transaction()"/>
|
||||
<t t-set="reference" t-value="invoice.reference"/>
|
||||
<t t-set="reference" t-value="invoice.invoice_payment_ref"/>
|
||||
</t>
|
||||
</div>
|
||||
<t t-set="tx_ids" t-value="invoice.transaction_ids.filtered(lambda tx: tx.state in ('authorized', 'done'))"/>
|
||||
<div t-if="not tx_ids and invoice.state == 'open' and invoice.amount_total" id="portal_pay">
|
||||
<div t-if="not tx_ids and invoice.state == 'posted' and invoice.invoice_payment_state == 'not_paid' and invoice.amount_total" id="portal_pay">
|
||||
<div t-if="pms or acquirers" id="payment_method">
|
||||
<t t-call="account_payment.portal_invoice_payment"/>
|
||||
</div>
|
||||
@@ -133,11 +133,11 @@
|
||||
<t t-set="payment_tx_id" t-value="invoice.get_portal_last_transaction()"/>
|
||||
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
|
||||
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
|
||||
<div t-if="payment_tx_id.acquirer_id.post_msg and payment_tx_id.acquirer_id.provider == 'transfer' and invoice.reference">
|
||||
<b>Communication: </b><span t-esc='invoice.reference'/>
|
||||
<div t-if="payment_tx_id.acquirer_id.post_msg and payment_tx_id.acquirer_id.provider == 'transfer' and invoice.ref">
|
||||
<b>Communication: </b><span t-esc='invoice.ref'/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="success == 'pay_invoice' and invoice.state == 'paid'">
|
||||
<t t-if="success == 'pay_invoice' and invoice.invoice_payment_state == 'paid'">
|
||||
Done, your online payment has been successfully processed. Thank you for your order.
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
@@ -55,7 +55,7 @@ class GeoCoder(models.AbstractModel):
|
||||
return self._geo_query_address_default(street=street, zip=zip, city=city, state=state, country=country)
|
||||
|
||||
@api.model
|
||||
def geo_find(self, addr):
|
||||
def geo_find(self, addr, **kw):
|
||||
"""Use a location provider API to convert an address string into a latitude, longitude tuple.
|
||||
Here we use Openstreetmap Nominatim by default.
|
||||
:param addr: Address string passed to API
|
||||
@@ -64,7 +64,7 @@ class GeoCoder(models.AbstractModel):
|
||||
provider = self._get_provider().tech_name
|
||||
try:
|
||||
service = getattr(self, '_call_' + provider)
|
||||
result = service(addr)
|
||||
result = service(addr, **kw)
|
||||
except AttributeError:
|
||||
raise UserError(_(
|
||||
'Provider %s is not implemented for geolocation service.'
|
||||
@@ -77,7 +77,7 @@ class GeoCoder(models.AbstractModel):
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _call_openstreetmap(self, addr):
|
||||
def _call_openstreetmap(self, addr, **kw):
|
||||
"""
|
||||
Use Openstreemap Nominatim service to retrieve location
|
||||
:return: (latitude, longitude) or None if not found
|
||||
@@ -95,7 +95,7 @@ class GeoCoder(models.AbstractModel):
|
||||
return float(geo['lat']), float(geo['lon'])
|
||||
|
||||
@api.model
|
||||
def _call_googlemap(self, addr):
|
||||
def _call_googlemap(self, addr, **kw):
|
||||
""" Use google maps API. It won't work without a valid API key.
|
||||
:return: (latitude, longitude) or None if not found
|
||||
"""
|
||||
@@ -106,8 +106,11 @@ class GeoCoder(models.AbstractModel):
|
||||
"Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information."
|
||||
))
|
||||
url = "https://maps.googleapis.com/maps/api/geocode/json"
|
||||
params = {'sensor': 'false', 'address': addr, 'key': apikey}
|
||||
if kw.get('force_country'):
|
||||
params['components'] = 'country:%s' % kw['force_country']
|
||||
try:
|
||||
result = requests.get(url, params={'sensor': 'false', 'address': addr, 'key': apikey}).json()
|
||||
result = requests.get(url, params).json()
|
||||
except Exception as e:
|
||||
self._raise_query_error(e)
|
||||
|
||||
@@ -151,4 +154,3 @@ class GeoCoder(models.AbstractModel):
|
||||
|
||||
def _raise_query_error(self, error):
|
||||
raise UserError(_('Error with geolocation server:') + ' %s' % error)
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ class ResPartner(models.Model):
|
||||
def _geo_localize(self, street='', zip='', city='', state='', country=''):
|
||||
geo_obj = self.env['base.geocoder']
|
||||
search = geo_obj.geo_query_address(street=street, zip=zip, city=city, state=state, country=country)
|
||||
result = geo_obj.geo_find(search)
|
||||
result = geo_obj.geo_find(search, force_country=country)
|
||||
if result is None:
|
||||
search = geo_obj.geo_query_address(city=city, state=state, country=country)
|
||||
result = geo_obj.geo_find(search)
|
||||
result = geo_obj.geo_find(search, force_country=country)
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
'name': 'CRM Livechat',
|
||||
'category': 'crm',
|
||||
'summary': 'Create lead from livechat conversation',
|
||||
'data': [
|
||||
'data/utm_data.xml',
|
||||
],
|
||||
'depends': [
|
||||
'crm',
|
||||
'im_livechat'
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="utm.source" id="utm_source_livechat">
|
||||
<field name="name">Livechat</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -4,6 +4,7 @@
|
||||
from odoo import models, _
|
||||
from odoo.tools import html2plaintext
|
||||
|
||||
|
||||
class MailChannel(models.Model):
|
||||
_inherit = 'mail.channel'
|
||||
|
||||
@@ -24,13 +25,15 @@ class MailChannel(models.Model):
|
||||
'%s: %s\n' % (message.author_id.name or self.anonymous_name, message.body)
|
||||
for message in self.channel_message_ids.sorted('id')
|
||||
)
|
||||
utm_source = self.env.ref('crm_livechat.utm_source_livechat', raise_if_not_found=False)
|
||||
lead = self.env['crm.lead'].create({
|
||||
'name': html2plaintext(key[5:]),
|
||||
'partner_id': channel_partners.partner_id.id,
|
||||
'user_id': None,
|
||||
'team_id': None,
|
||||
'description': html2plaintext(description),
|
||||
'referred': partner.name
|
||||
'referred': partner.name,
|
||||
'source_id': utm_source and utm_source.id,
|
||||
})
|
||||
lead._onchange_partner_id()
|
||||
msg = _('Created a new lead: <a href="#" data-oe-id="%s" data-oe-model="crm.lead">%s</a>') % (lead.id, lead.name)
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
<field name="description" type="html"><p>You just began the adventure! Welcome!</p></field>
|
||||
<field name="description_motivational" type="html">
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">Reach the next rank to show the rest of the world you exist!</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_student_badge.svg"/>
|
||||
<div class="media-body">Earn your first points and join the adventure !</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_newbie_badge.svg"/>
|
||||
</div>
|
||||
</field>
|
||||
<field name="karma_min">1</field>
|
||||
@@ -27,8 +27,8 @@
|
||||
<field name="description" type="html"><p>You're a young padawan now. May the force be with you!</p></field>
|
||||
<field name="description_motivational" type="html">
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">Reach the next rank to improve your status.</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_bachelor_badge.svg"/>
|
||||
<div class="media-body">Reach the next rank to show the rest of the world you exist.</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_student_badge.svg"/>
|
||||
</div>
|
||||
</field>
|
||||
<field name="karma_min">100</field>
|
||||
@@ -40,8 +40,8 @@
|
||||
<field name="description" type="html"><p>You love learning things. Curiosity is a good way to progress.</p></field>
|
||||
<field name="description_motivational" type="html">
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">Reach the next rank and become a Master!</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_master_badge.svg"/>
|
||||
<div class="media-body">Reach the next rank to improve your status!</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_bachelor_badge.svg"/>
|
||||
</div>
|
||||
</field>
|
||||
<field name="karma_min">500</field>
|
||||
@@ -53,8 +53,8 @@
|
||||
<field name="description" type="html"><p>You know what you are talking about. People learn from you.</p></field>
|
||||
<field name="description_motivational" type="html">
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">Reach the next rank and become a powerful user!</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_doctor_badge.svg"/>
|
||||
<div class="media-body">Reach the next rank and become a Master!</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_master_badge.svg"/>
|
||||
</div>
|
||||
</field>
|
||||
<field name="karma_min">2000</field>
|
||||
@@ -66,7 +66,8 @@
|
||||
<field name="description" type="html"><p>You have reached the last rank. Congratulations!</p></field>
|
||||
<field name="description_motivational" type="html">
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">You have reached the last rank. Congratulations!</div>
|
||||
<div class="media-body">Reach the next rank and become a powerful user!</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_doctor_badge.svg"/>
|
||||
</div>
|
||||
</field>
|
||||
<field name="karma_min">10000</field>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="0">
|
||||
<odoo><data noupdate="1">
|
||||
<!-- Set demo karma for test tour as demo user-->
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="karma">20</field>
|
||||
@@ -39,7 +39,7 @@
|
||||
<record id="rank_doctor" model="gamification.karma.rank">
|
||||
<field name="description_motivational" type="html">
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">You have reached the last rank. Congratulations!</div>
|
||||
<div class="media-body">Reach the next rank and gain a very nice unicorn !</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_unicorn.png"/>
|
||||
</div>
|
||||
</field>
|
||||
|
||||
@@ -96,6 +96,14 @@ class Users(models.Model):
|
||||
if old_rank != user.rank_id:
|
||||
user._rank_changed()
|
||||
|
||||
def _get_next_rank(self):
|
||||
""" For fresh users with 0 karma that don't have a rank_id and next_rank_id yet
|
||||
this method returns the first karma rank (by karma ascending). This acts as a
|
||||
default value in related views.
|
||||
|
||||
TDE FIXME in post-12.4: make next_rank_id a non-stored computed field correctly computed """
|
||||
return self.next_rank_id or (not self.rank_id and self.env['gamification.karma.rank'].search([], order="karma_min ASC", limit=1))
|
||||
|
||||
def get_gamification_redirection_data(self):
|
||||
"""
|
||||
Hook for other modules to add redirect button(s) in new rank reached mail
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from pprint import pformat
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -184,29 +183,14 @@ class GoogleService(models.TransientModel):
|
||||
except:
|
||||
pass
|
||||
except requests.HTTPError as error:
|
||||
# https://developers.google.com/calendar/v3/errors
|
||||
if error.response.status_code in (204, 404):
|
||||
status = error.response.status_code
|
||||
response = ""
|
||||
else:
|
||||
try:
|
||||
exception_req = pformat(json.loads(error.request.body))
|
||||
except:
|
||||
exception_req = error.request.body
|
||||
|
||||
try:
|
||||
exception_res = pformat(error.response.json())
|
||||
error_msg = _("Error while requesting Google Services: %s") % res['error']['message']
|
||||
warning_msg = _("Something went wrong with your request to google: %s") % res['error']['message']
|
||||
except:
|
||||
exception_res = error.response.text
|
||||
error_msg = _("Error while requesting Google Services")
|
||||
warning_msg = _("Something went wrong with your request to google")
|
||||
|
||||
_logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", exception_req, exception_res)
|
||||
_logger.exception("Bad google request : %s !", error.response.content)
|
||||
if error.response.status_code in (400, 401, 410):
|
||||
raise UserError(error_msg)
|
||||
raise self.env['res.config.settings'].get_config_warning(warning_msg)
|
||||
raise error
|
||||
raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google"))
|
||||
return (status, response, ask_time)
|
||||
|
||||
# TODO : remove me, it is only used in google calendar. Make google_calendar use the constants
|
||||
|
||||
@@ -143,6 +143,12 @@ class HrEmployeePrivate(models.Model):
|
||||
raise AccessError(_('The fields "%s" you try to read is not available on the public employee profile.') % (','.join(private_fields)))
|
||||
return self.env['hr.employee.public'].browse(self.ids).read(fields, load=load)
|
||||
|
||||
@api.model
|
||||
def load_views(self, views, options=None):
|
||||
if self.check_access_rights('read', raise_exception=False):
|
||||
return super(HrEmployeePrivate, self).load_views(views, options=options)
|
||||
return self.env['hr.employee.public'].load_views(views, options=options)
|
||||
|
||||
@api.model
|
||||
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
||||
"""
|
||||
|
||||
@@ -146,6 +146,11 @@
|
||||
<field name="permit_no"/>
|
||||
<field name="visa_expire"/>
|
||||
</group>
|
||||
<group string="Education">
|
||||
<field name="certificate"/>
|
||||
<field name="study_field"/>
|
||||
<field name="study_school"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="google_drive_link" widget="url"/>
|
||||
|
||||
@@ -100,5 +100,6 @@
|
||||
parent="menu_config_plan"
|
||||
groups="group_hr_manager"
|
||||
sequence="100"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -16,7 +16,7 @@ class Contract(models.Model):
|
||||
|
||||
name = fields.Char('Contract Reference', required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee')
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', tracking=True)
|
||||
department_id = fields.Many2one('hr.department', string="Department")
|
||||
job_id = fields.Many2one('hr.job', string='Job Position')
|
||||
date_start = fields.Date('Start Date', required=True, default=fields.Date.today,
|
||||
|
||||
@@ -600,7 +600,7 @@ class HolidaysRequest(models.Model):
|
||||
return
|
||||
current_employee = self.env['hr.employee'].sudo().search([('user_id', '=', self.env.uid)], limit=1)
|
||||
for record in self:
|
||||
emp_id = record._cache.get('employee_id', [False])[0]
|
||||
emp_id = record._cache.get('employee_id', False) and record._cache.get('employee_id')[0]
|
||||
if emp_id != current_employee.id:
|
||||
try:
|
||||
record._cache['name']
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<report
|
||||
id="action_report_holidayssummary"
|
||||
string="Time Off Summary"
|
||||
model="hr.leave"
|
||||
model="hr.holidays.summary.dept"
|
||||
report_type="qweb-pdf"
|
||||
name="hr_holidays.report_holidayssummary"
|
||||
file="hr_holidays.report_holidayssummary"
|
||||
|
||||
@@ -643,6 +643,18 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.server" id="hr_applicant_resumes_server">
|
||||
<field name="name">hr.applicant.resumes.server</field>
|
||||
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
act = env.ref('hr_recruitment.hr_applicant_resumes').read()[0]
|
||||
act['domain'] = [('res_model', '=', 'hr.applicant'), '|', ('company_id', '=', False), ('company_id', '=', env.user.company_id.id)]
|
||||
action = act
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Stage Tree View -->
|
||||
<record model="ir.ui.view" id="hr_recruitment_stage_tree">
|
||||
<field name="name">hr.recruitment.stage.tree</field>
|
||||
|
||||
@@ -2296,7 +2296,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'minus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2309,7 +2309,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'plus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2333,7 +2333,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'minus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2346,7 +2346,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'plus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2405,7 +2405,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'minus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2418,7 +2418,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'plus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2442,7 +2442,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'minus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2455,7 +2455,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'plus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2479,7 +2479,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'minus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2492,7 +2492,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'plus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2516,7 +2516,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'minus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
@@ -2529,7 +2529,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': ref('account_template_101050505'),
|
||||
'account_id': ref('account_template_201010302'),
|
||||
'plus_report_line_ids': [ref('tax_report_icms_1')],
|
||||
}),
|
||||
]"/>
|
||||
|
||||
@@ -307,7 +307,7 @@ class AccountFrFec(models.TransientModel):
|
||||
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
|
||||
CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/'
|
||||
WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/'
|
||||
ELSE replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', '') END AS EcritureLib,
|
||||
ELSE replace(replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', ''), '\r', '') END AS EcritureLib,
|
||||
replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
|
||||
CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
|
||||
|
||||
@@ -502,6 +502,7 @@ class MailTemplate(models.Model):
|
||||
'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=record.display_name)),
|
||||
'model_description': self.env['ir.model']._get(record._name).display_name,
|
||||
'company': 'company_id' in record and record['company_id'] or self.env.company,
|
||||
'record': record,
|
||||
}
|
||||
body = template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
|
||||
values['body_html'] = self.env['mail.thread']._replace_local_links(body)
|
||||
|
||||
@@ -438,10 +438,27 @@ var BasicComposer = Widget.extend({
|
||||
* @param {boolean} params.submitForm [optional]
|
||||
*/
|
||||
_processAttachmentChange: function (params) {
|
||||
var self = this,
|
||||
attachments = this.get('attachment_ids'),
|
||||
files = params.files,
|
||||
submitForm = params.submitForm;
|
||||
var self = this;
|
||||
var attachments = this.get('attachment_ids');
|
||||
var files = params.files;
|
||||
var submitForm = params.submitForm;
|
||||
var $form = this.$('form.o_form_binary_form');
|
||||
|
||||
/**
|
||||
* makes a new formData as formData.delete() is not supported by IE or Safari Mobile.
|
||||
*
|
||||
* @return {FormData}
|
||||
*/
|
||||
function makeFormDataWithoutUfile() {
|
||||
var newFormData = new window.FormData();
|
||||
$form.find('input').each(function (index, input) {
|
||||
if (input.name !== 'ufile') {
|
||||
newFormData.append(input.name, input.value);
|
||||
}
|
||||
});
|
||||
return newFormData;
|
||||
}
|
||||
|
||||
_.each(files, function (file) {
|
||||
var attachment = _.findWhere(attachments, {
|
||||
name: file.name,
|
||||
@@ -453,24 +470,20 @@ var BasicComposer = Widget.extend({
|
||||
attachments = _.without(attachments, attachment);
|
||||
}
|
||||
});
|
||||
var $form = this.$('form.o_form_binary_form');
|
||||
if (submitForm) {
|
||||
$form.submit();
|
||||
this._$attachmentButton.prop('disabled', true);
|
||||
} else {
|
||||
var data = new FormData($form[0]);
|
||||
_.each(files, function (file) {
|
||||
// removing existing key with blank data and appending again with file info
|
||||
// In safari, existing key will not be updated when append with new file.
|
||||
data.delete("ufile");
|
||||
data.append("ufile", file, file.name);
|
||||
var formData = makeFormDataWithoutUfile();
|
||||
formData.append("ufile", file, file.name);
|
||||
$.ajax({
|
||||
url: $form.attr("action"),
|
||||
type: "POST",
|
||||
enctype: 'multipart/form-data',
|
||||
processData: false,
|
||||
contentType: false,
|
||||
data: data,
|
||||
data: formData,
|
||||
success: function (result) {
|
||||
var $el = $(result);
|
||||
$.globalEval($el.contents().text());
|
||||
|
||||
@@ -167,9 +167,9 @@ var RenameConversationDialog = Dialog.extend({
|
||||
kwargs: {
|
||||
name: name,
|
||||
}
|
||||
}).then(function (updatedName) {
|
||||
}).then(function () {
|
||||
var channel = self.call('mail_service', 'getThread', self._channelID);
|
||||
channel.setName(updatedName);
|
||||
channel.setName(name);
|
||||
self._callback();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -801,6 +801,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
|
||||
_warnMessageModerated: function () {
|
||||
var mailBus = this.call('mail_service', 'getMailBus');
|
||||
if (this.needsModerationByUser()) {
|
||||
this._setModeratedByUser(false);
|
||||
var moderationBox = this.call('mail_service', 'getMailbox', 'moderation');
|
||||
moderationBox.decrementMailboxCounter();
|
||||
moderationBox.removeMessage(this.getID());
|
||||
|
||||
@@ -94,9 +94,7 @@ var MailManager = AbstractService.extend({
|
||||
additionalThreadIDs: data.channel_ids
|
||||
});
|
||||
}
|
||||
if (options.domain && options.domain !== []) {
|
||||
this._addMessageToThreads(message, options);
|
||||
}
|
||||
this._addMessageToThreads(message, options);
|
||||
}
|
||||
return prom;
|
||||
},
|
||||
|
||||
@@ -1425,7 +1425,7 @@ QUnit.test('rename DM conversation', async function (assert) {
|
||||
assert.step(args.method);
|
||||
assert.strictEqual(args.args[0], 1);
|
||||
assert.strictEqual(args.kwargs.name, "Demo");
|
||||
return Promise.resolve("Demo");
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -2320,9 +2320,9 @@ msgstr ""
|
||||
|
||||
#. module: mass_mailing
|
||||
#. openerp-web
|
||||
#: code:addons/mass_mailing/static/src/js/unsubscribe.js:49
|
||||
#: code:addons/mass_mailing/static/src/js/unsubscribe.js:50
|
||||
#, python-format
|
||||
msgid "You have been <strong>successfully unsubscribed from </strong>."
|
||||
msgid "You have been <strong>successfully unsubscribed from %s</strong>."
|
||||
msgstr ""
|
||||
|
||||
#. module: mass_mailing
|
||||
|
||||
@@ -95,7 +95,7 @@ class MailMail(models.Model):
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _postprocess_sent_message(self, failure_type=None, **kwargs):
|
||||
def _postprocess_sent_message(self, success_pids, failure_reason=False, failure_type=None):
|
||||
mail_sent = not failure_type # we consider that a recipient error is a failure with mass mailling and show them as failed
|
||||
for mail in self:
|
||||
if mail.mailing_id:
|
||||
@@ -103,5 +103,4 @@ class MailMail(models.Model):
|
||||
mail.statistics_ids.write({'sent': fields.Datetime.now(), 'exception': False})
|
||||
elif mail_sent is False and mail.statistics_ids:
|
||||
mail.statistics_ids.write({'exception': fields.Datetime.now()})
|
||||
return super(MailMail, self)._postprocess_sent_message(failure_type=failure_type, **kwargs)
|
||||
|
||||
return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type)
|
||||
|
||||
@@ -46,7 +46,10 @@ odoo.define('mass_mailing.unsubscribe', function (require) {
|
||||
|
||||
var unsubscribed_list = $("input[name='unsubscribed_list']").val();
|
||||
if (unsubscribed_list){
|
||||
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed from ' + unsubscribed_list + "</strong>."));
|
||||
$('#subscription_info').html(_.str.sprintf(
|
||||
_t("You have been <strong>successfully unsubscribed from %s</strong>."),
|
||||
unsubscribed_list
|
||||
));
|
||||
}
|
||||
else{
|
||||
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed</strong>.'));
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
<header>
|
||||
<div><title>Odoo</title></div>
|
||||
<div class="text-center">
|
||||
<img t-attf-src="/web/binary/company_logo"/>
|
||||
<img t-attf-src="/web/binary/company_logo?company={{ res_company.id }}"/>
|
||||
</div>
|
||||
</header>
|
||||
<div id="wrap" class="oe_structure oe_empty"/>
|
||||
|
||||
@@ -828,8 +828,6 @@ class MrpProduction(models.Model):
|
||||
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
for move in moves_to_do.filtered(lambda m: m.product_qty == 0.0 and m.quantity_done > 0):
|
||||
move.product_uom_qty = move.quantity_done
|
||||
for move in moves_to_do.filtered(lambda m: float_is_zero(m.quantity_done, precision_rounding=m.product_uom.rounding)):
|
||||
move._action_cancel()
|
||||
# MRP do not merge move, catch the result of _action_done in order
|
||||
# to get extra moves.
|
||||
moves_to_do = moves_to_do._action_done()
|
||||
@@ -860,6 +858,11 @@ class MrpProduction(models.Model):
|
||||
if wo.time_ids.filtered(lambda x: (not x.date_end) and (x.loss_type in ('productive', 'performance'))):
|
||||
raise UserError(_('Work order %s is still running') % wo.name)
|
||||
self._check_lots()
|
||||
|
||||
# Cancel unfinished move
|
||||
move_to_cancel = self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel') and float_is_zero(m.quantity_done, precision_rounding=m.product_uom.rounding))
|
||||
move_to_cancel._action_cancel()
|
||||
|
||||
self.post_inventory()
|
||||
# Moves without quantity done are not posted => set them as done instead of canceling. In
|
||||
# case the user edits the MO later on and sets some consumed quantity on those, we do not
|
||||
|
||||
@@ -825,8 +825,8 @@ class PaymentTransaction(models.Model):
|
||||
@api.model
|
||||
def _compute_reference_prefix(self, values):
|
||||
if values and values.get('invoice_ids'):
|
||||
many_list = self.resolve_2many_commands('invoice_ids', values['invoice_ids'], fields=['number'])
|
||||
return ','.join(dic['number'] for dic in many_list)
|
||||
many_list = self.resolve_2many_commands('invoice_ids', values['invoice_ids'], fields=['name'])
|
||||
return ','.join(dic['name'] for dic in many_list)
|
||||
return None
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -62,7 +62,8 @@ class PaymentAcquirerAuthorize(models.Model):
|
||||
# The signature key is now '128-character hexadecimal format', while the
|
||||
# transaction key was only 16-character.
|
||||
if len(values['x_trans_key']) == 128:
|
||||
return hmac.new(values['x_trans_key'].decode("hex").encode('utf-8'), data, hashlib.sha512).hexdigest().upper()
|
||||
key = bytes.fromhex(values['x_trans_key'])
|
||||
return hmac.new(key, data, hashlib.sha512).hexdigest().upper()
|
||||
else:
|
||||
return hmac.new(values['x_trans_key'].encode('utf-8'), data, hashlib.md5).hexdigest()
|
||||
|
||||
@@ -315,13 +316,14 @@ class TxAuthorize(models.Model):
|
||||
'acquirer_reference': tree.get('x_trans_id'),
|
||||
'date': fields.Datetime.now(),
|
||||
})
|
||||
if init_state != 'authorized':
|
||||
self.execute_callback()
|
||||
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
|
||||
self._set_transaction_done()
|
||||
|
||||
if init_state != 'authorized':
|
||||
self.execute_callback()
|
||||
if tree.get('x_type').lower() == 'auth_only':
|
||||
self.write({'acquirer_reference': tree.get('x_trans_id')})
|
||||
self._set_transaction_authorized()
|
||||
|
||||
@@ -1738,8 +1738,11 @@ exports.Orderline = Backbone.Model.extend({
|
||||
else
|
||||
var price_include = !price_exclude;
|
||||
if (tax.amount_type === 'fixed') {
|
||||
var sign_base_amount = base_amount >= 0 ? 1 : -1;
|
||||
return (Math.abs(tax.amount) * sign_base_amount) * quantity;
|
||||
var sign_base_amount = Math.sign(base_amount) || 1;
|
||||
// Since base amount has been computed with quantity
|
||||
// we take the abs of quantity
|
||||
// Same logic as bb72dea98de4dae8f59e397f232a0636411d37ce
|
||||
return tax.amount * sign_base_amount * Math.abs(quantity);
|
||||
}
|
||||
if (tax.amount_type === 'percent' && !price_include){
|
||||
return base_amount * tax.amount / 100;
|
||||
|
||||
@@ -33,7 +33,7 @@ class PosMakePayment(models.TransientModel):
|
||||
journal_id = fields.Many2one('account.journal', string='Payment Mode', required=True, default=_default_journal)
|
||||
amount = fields.Float(digits=0, required=True, default=_default_amount)
|
||||
payment_name = fields.Char(string='Payment Reference')
|
||||
payment_date = fields.Date(string='Payment Date', required=True, default=lambda *a: fields.Date.today())
|
||||
payment_date = fields.Date(string='Payment Date', required=True, default=lambda self: fields.Date.context_today(self))
|
||||
|
||||
@api.onchange('session_id')
|
||||
def _on_change_session(self):
|
||||
|
||||
@@ -15,22 +15,21 @@ models.PosModel = models.PosModel.extend({
|
||||
return model.model === "product.product";
|
||||
});
|
||||
|
||||
// Give both the fields and domain to pos_cache in the
|
||||
// backend. This way we don't have to hardcode these
|
||||
// values in the backend and they automatically stay in
|
||||
// sync with whatever is defined (and maybe extended by
|
||||
// other modules) in js.
|
||||
var product_model = this.models[product_index];
|
||||
var product_fields = product_model.fields;
|
||||
var product_domain = product_model.domain;
|
||||
var product_model = self.models[product_index];
|
||||
|
||||
// We don't want to load product.product the normal
|
||||
// uncached way, so get rid of it.
|
||||
if (product_index !== -1) {
|
||||
this.models.splice(product_index, 1);
|
||||
}
|
||||
|
||||
return posmodel_super.load_server_data.apply(this, arguments).then(function () {
|
||||
// Give both the fields and domain to pos_cache in the
|
||||
// backend. This way we don't have to hardcode these
|
||||
// values in the backend and they automatically stay in
|
||||
// sync with whatever is defined (and maybe extended by
|
||||
// other modules) in js.
|
||||
var product_fields = typeof product_model.fields === 'function' ? product_model.fields(self) : product_model.fields;
|
||||
var product_domain = typeof product_model.domain === 'function' ? product_model.domain(self) : product_model.domain;
|
||||
var records = rpc.query({
|
||||
model: 'pos.config',
|
||||
method: 'get_products_from_cache',
|
||||
|
||||
@@ -16,10 +16,11 @@ ScreenWidget.include({
|
||||
barcode_cashier_action: function(code){
|
||||
var self = this;
|
||||
var employees = this.pos.employees;
|
||||
var prom;
|
||||
for(var i = 0, len = employees.length; i < len; i++){
|
||||
if(employees[i].barcode === Sha1.hash(code.code)){
|
||||
if (employees[i].id !== this.pos.get_cashier().id && employees[i].pin) {
|
||||
return this.gui.ask_password(employees[i].pin).then(function(){
|
||||
prom = this.gui.ask_password(employees[i].pin).then(function(){
|
||||
self.pos.set_cashier(employees[i]);
|
||||
self.chrome.widget.username.renderElement();
|
||||
return true;
|
||||
@@ -27,12 +28,18 @@ ScreenWidget.include({
|
||||
} else {
|
||||
this.pos.set_cashier(employees[i]);
|
||||
this.chrome.widget.username.renderElement();
|
||||
return true;
|
||||
prom = Promise.resolve(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.barcode_error_action(code);
|
||||
return false;
|
||||
if (!prom){
|
||||
this.barcode_error_action(code);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
else {
|
||||
return prom
|
||||
}
|
||||
},
|
||||
show: function() {
|
||||
this._super();
|
||||
@@ -73,8 +80,10 @@ var LoginScreenWidget = ScreenWidget.extend({
|
||||
* @override
|
||||
*/
|
||||
barcode_cashier_action: function(code) {
|
||||
this._super(code);
|
||||
this.unlock_screen();
|
||||
var self = this;
|
||||
return this._super(code).then(function () {
|
||||
self.unlock_screen();
|
||||
});
|
||||
},
|
||||
|
||||
unlock_screen: function() {
|
||||
|
||||
@@ -29,6 +29,7 @@ following:
|
||||
'views/pos_mercury_views.xml',
|
||||
'views/pos_mercury_transaction_templates.xml',
|
||||
'views/pos_config_setting_views.xml',
|
||||
'views/pos_config_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/pos_mercury_demo.xml',
|
||||
|
||||
@@ -509,7 +509,7 @@ PaymentScreenWidget.include({
|
||||
self.credit_code_transaction(parsed_result);
|
||||
} else { // this is for supporting another payment system like mercury
|
||||
this.gui.show_popup('selection',{
|
||||
title: 'Pay ' + this.pos.get_order().get_due().toFixed(2) + ' with : ',
|
||||
title: _t('Pay with: '),
|
||||
list: online_payment_journals,
|
||||
confirm: function (item) {
|
||||
parsed_result.journal_id = item;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="pos_config_view_form_inherit_pos_mercury" model="ir.ui.view">
|
||||
<field name="name">pos.config.form.view.inherit.mercury</field>
|
||||
<field name="model">pos.config</field>
|
||||
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<label for="iface_scan_via_proxy" position="attributes">
|
||||
<attribute name="string">Barcode Scanner/Card Reader</attribute>
|
||||
</label>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -606,12 +606,7 @@ class PurchaseOrderLine(models.Model):
|
||||
if product_lang.description_purchase:
|
||||
self.name += '\n' + product_lang.description_purchase
|
||||
|
||||
fpos = self.order_id.fiscal_position_id
|
||||
if self.env.uid == SUPERUSER_ID:
|
||||
company_id = self.env.company.id
|
||||
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id.filtered(lambda r: r.company_id.id == company_id))
|
||||
else:
|
||||
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id)
|
||||
self._compute_tax_id()
|
||||
|
||||
self._suggest_quantity()
|
||||
self._onchange_quantity()
|
||||
@@ -681,9 +676,8 @@ class PurchaseOrderLine(models.Model):
|
||||
'''
|
||||
if not self.product_id:
|
||||
return
|
||||
|
||||
seller_min_qty = self.product_id.seller_ids\
|
||||
.filtered(lambda r: r.name == self.order_id.partner_id)\
|
||||
.filtered(lambda r: r.name == self.order_id.partner_id and (not r.product_id or r.product_id == self.product_id))\
|
||||
.sorted(key=lambda r: r.min_qty)
|
||||
if seller_min_qty:
|
||||
self.product_qty = seller_min_qty[0].min_qty or 1.0
|
||||
|
||||
@@ -45,8 +45,12 @@ class StockMove(models.Model):
|
||||
if line.product_uom.id != line.product_id.uom_id.id:
|
||||
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
|
||||
if order.currency_id != order.company_id.currency_id:
|
||||
# The date must be today, and not the date of the move since the move move is still
|
||||
# in assigned state. However, the move date is the scheduled date until move is
|
||||
# done, then date of actual move processing. See:
|
||||
# https://github.com/odoo/odoo/blob/2f789b6863407e63f90b3a2d4cc3be09815f7002/addons/stock/models/stock_move.py#L36
|
||||
price_unit = order.currency_id._convert(
|
||||
price_unit, order.company_id.currency_id, order.company_id, self.date, round=False)
|
||||
price_unit, order.company_id.currency_id, order.company_id, fields.Date.context_today(self), round=False)
|
||||
return price_unit
|
||||
return super(StockMove, self)._get_price_unit()
|
||||
|
||||
|
||||
@@ -822,12 +822,12 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
'company_id': company.id,
|
||||
})
|
||||
|
||||
# To allow testing validation of PO
|
||||
# To allow testing validation of PO and Delivery
|
||||
today = date_po
|
||||
def _today(*args, **kwargs):
|
||||
return datetime.strptime(date_po, "%Y-%m-%d").date()
|
||||
# To allow testing validation of Delivery
|
||||
return datetime.strptime(today, "%Y-%m-%d").date()
|
||||
def _now(*args, **kwargs):
|
||||
return datetime.strptime(date_delivery + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
|
||||
return datetime.strptime(today + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
patchers = [
|
||||
patch('odoo.fields.Date.context_today', _today),
|
||||
@@ -856,13 +856,17 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
|
||||
line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
|
||||
|
||||
today = date_delivery
|
||||
picking = po.picking_ids
|
||||
(picking.move_lines
|
||||
.filtered(lambda l: l.purchase_line_id == line_product_avg)
|
||||
.write({'quantity_done': 1.0}))
|
||||
|
||||
picking.button_validate()
|
||||
# 5 Units received at rate 0.7 = 42.86
|
||||
self.assertAlmostEqual(product_avg.standard_price, 42.86)
|
||||
|
||||
today = date_invoice
|
||||
inv = self.env['account.move'].with_context(default_type='in_invoice').create({
|
||||
'type': 'in_invoice',
|
||||
'invoice_date': date_invoice,
|
||||
@@ -944,7 +948,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
product_avg = self.product1.copy({
|
||||
'purchase_method': 'purchase',
|
||||
'name': 'AVG',
|
||||
'standard_price': 60,
|
||||
'standard_price': 0,
|
||||
'property_account_creditor_price_difference': self.price_diff_account.id
|
||||
})
|
||||
product_avg.invoice_policy = 'order'
|
||||
@@ -991,13 +995,12 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
'company_id': company.id,
|
||||
})
|
||||
|
||||
# To allow testing validation of PO
|
||||
# To allow testing validation of PO and Delivery
|
||||
today = date_po
|
||||
def _today(*args, **kwargs):
|
||||
return datetime.strptime(date_po, "%Y-%m-%d").date()
|
||||
# To allow testing validation of Delivery
|
||||
delivery_now = date_delivery
|
||||
return datetime.strptime(today, "%Y-%m-%d").date()
|
||||
def _now(*args, **kwargs):
|
||||
return datetime.strptime(delivery_now + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
|
||||
return datetime.strptime(today + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
patchers = [
|
||||
patch('odoo.fields.Date.context_today', _today),
|
||||
@@ -1027,6 +1030,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
|
||||
line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
|
||||
|
||||
today = date_delivery
|
||||
picking = po.picking_ids
|
||||
(picking.move_lines
|
||||
.filtered(lambda l: l.purchase_line_id == line_product_avg)
|
||||
@@ -1034,7 +1038,10 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
|
||||
picking.button_validate()
|
||||
picking.action_done() # Create Backorder
|
||||
# 5 Units received at rate 0.7 = 42.86
|
||||
self.assertAlmostEqual(product_avg.standard_price, 42.86)
|
||||
|
||||
today = date_invoice
|
||||
inv = self.env['account.move'].with_context(default_type='in_invoice').create({
|
||||
'type': 'in_invoice',
|
||||
'invoice_date': date_invoice,
|
||||
@@ -1055,13 +1062,16 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
|
||||
inv.post()
|
||||
|
||||
today = date_delivery1
|
||||
backorder_picking = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
|
||||
delivery_now = date_delivery1
|
||||
(backorder_picking.move_lines
|
||||
.filtered(lambda l: l.purchase_line_id == line_product_avg)
|
||||
.write({'quantity_done': 5.0}))
|
||||
backorder_picking.button_validate()
|
||||
# 5 Units received at rate 0.7 (42.86) + 5 Units received at rate 0.8 (37.50) = 40.18
|
||||
self.assertAlmostEqual(product_avg.standard_price, 40.18)
|
||||
|
||||
today = date_invoice1
|
||||
inv1 = self.env['account.move'].with_context(default_type='in_invoice').create({
|
||||
'type': 'in_invoice',
|
||||
'invoice_date': date_invoice1,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sale_order_employee,sale.order.employee.expense,sale.model_sale_order,base.group_user,1,0,0,0
|
||||
access_sale_order_employee,sale.order.employee.expense,sale.model_sale_order,base.group_user,0,0,0,0
|
||||
|
@@ -13,6 +13,7 @@
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="active" eval="False"/> <!-- opw-2027005: this rules breaks sale "see own document" -->
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='employee_id']" position="after">
|
||||
<field name="sale_order_id" context="{'sale_show_partner_name': True}"/>
|
||||
<field name="sale_order_id" context="{'sale_show_partner_name': True}" groups="sales_team.group_sale_salesman"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -17,7 +17,7 @@
|
||||
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='expense_line_ids']/tree/field[@name='name']" position="after">
|
||||
<field name="sale_order_id" context="{'sale_show_partner_name': True}"/>
|
||||
<field name="sale_order_id" context="{'sale_show_partner_name': True}" groups="sales_team.group_sale_salesman"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -183,7 +183,7 @@ class ProjectTask(models.Model):
|
||||
@api.depends('project_id.sale_line_employee_ids')
|
||||
def _compute_is_project_map_empty(self):
|
||||
for task in self:
|
||||
task.is_project_map_empty = not bool(task.project_id.sale_line_employee_ids)
|
||||
task.is_project_map_empty = not bool(task.sudo().project_id.sale_line_employee_ids)
|
||||
|
||||
@api.onchange('project_id')
|
||||
def _onchange_project(self):
|
||||
|
||||
@@ -354,7 +354,7 @@
|
||||
<a t-attf-class="progress-bar o_progress_{{key}}"
|
||||
t-attf-style="width: {{amount / total * 100}}%"
|
||||
type="action" data-model="account.analytic.line"
|
||||
t-att-data-domain="json.dumps(employee['__domain_' + key])"
|
||||
t-att-data-domain="employee['__domain_' + key]"
|
||||
>
|
||||
<span t-att-title="title" style="font-size: 0px; width: 100%; height: 100%;">
|
||||
<t t-esc="label" />
|
||||
|
||||
@@ -22,8 +22,8 @@ class Project(models.Model):
|
||||
context={'create': False, 'edit': False, 'delete': False}
|
||||
)
|
||||
})
|
||||
account_invoice_lines = self.env['account.invoice.line'].search([('account_analytic_id', 'in', accounts)])
|
||||
account_invoices = account_invoice_lines.mapped('invoice_id')
|
||||
account_invoice_lines = self.env['account.move.line'].search([('analytic_account_id', 'in', accounts)])
|
||||
account_invoices = account_invoice_lines.mapped('move_id')
|
||||
if account_invoices:
|
||||
stat_buttons.append({
|
||||
'name': _('Vendor Bills'),
|
||||
|
||||
@@ -760,7 +760,7 @@ function jSignatureClass(parent, options, instanceExtensions) {
|
||||
// these, when enabled, will hover above the sig area. Hence we append them to DOM before canvas.
|
||||
this.$controlbarUpper = (function(){
|
||||
var controlbarstyle = 'padding:0 !important; margin:0 !important;'+
|
||||
'width: 100% !important; height: 0 !important; -ms-touch-action: none;'+
|
||||
'width: 100% !important; height: 0 !important; -ms-touch-action: none; touch-action: none;'+
|
||||
'margin-top:-1em !important; margin-bottom:1em !important;';
|
||||
return $('<div style="'+controlbarstyle+'"></div>').appendTo($parent);
|
||||
})();
|
||||
@@ -771,7 +771,7 @@ function jSignatureClass(parent, options, instanceExtensions) {
|
||||
|
||||
this.$controlbarLower = (function(){
|
||||
var controlbarstyle = 'padding:0 !important; margin:0 !important;'+
|
||||
'width: 100% !important; height: 0 !important; -ms-touch-action: none;'+
|
||||
'width: 100% !important; height: 0 !important; -ms-touch-action: none; touch-action: none;'+
|
||||
'margin-top:-1.5em !important; margin-bottom:1.5em !important; position: relative;';
|
||||
return $('<div style="'+controlbarstyle+'"></div>').appendTo($parent);
|
||||
})();
|
||||
@@ -876,7 +876,7 @@ function jSignatureClass(parent, options, instanceExtensions) {
|
||||
$canvas.bind('mouseup.'+apinamespace, drawEndHandler);
|
||||
$canvas.bind('mousedown.'+apinamespace, drawStartHandler);
|
||||
} else {
|
||||
canvas.ontouchstart = function(e) {
|
||||
canvas.addEventListener('touchstart', function(e) {
|
||||
canvas.onmousedown = canvas.onmouseup = canvas.onmousemove = undef;
|
||||
|
||||
this.fatFingerCompensation = (
|
||||
@@ -886,19 +886,19 @@ function jSignatureClass(parent, options, instanceExtensions) {
|
||||
|
||||
drawStartHandler(e);
|
||||
|
||||
canvas.ontouchend = drawEndHandler;
|
||||
canvas.ontouchstart = drawStartHandler;
|
||||
canvas.ontouchmove = drawMoveHandler;
|
||||
};
|
||||
canvas.onmousedown = function(e) {
|
||||
canvas.addEventListener('touchend', drawEndHandler);
|
||||
canvas.addEventListener('touchstart', drawStartHandler);
|
||||
canvas.addEventListener('touchmove', drawMoveHandler);
|
||||
});
|
||||
canvas.addEventListener('mousedown', function(e) {
|
||||
canvas.ontouchstart = canvas.ontouchend = canvas.ontouchmove = undef;
|
||||
|
||||
drawStartHandler(e);
|
||||
|
||||
canvas.onmousedown = drawStartHandler;
|
||||
canvas.onmouseup = drawEndHandler;
|
||||
canvas.onmousemove = drawMoveHandler;
|
||||
}
|
||||
canvas.addEventListener('mousedown', drawStartHandler);
|
||||
canvas.addEventListener('mouseup', drawEndHandler);
|
||||
canvas.addEventListener('mousemove', drawMoveHandler);
|
||||
});
|
||||
if (window.navigator.msPointerEnabled) {
|
||||
canvas.onmspointerdown = drawStartHandler;
|
||||
canvas.onmspointerup = drawEndHandler;
|
||||
@@ -1084,28 +1084,16 @@ jSignatureClass.prototype.initializeCanvas = function(settings) {
|
||||
settings.width = '100%';
|
||||
}
|
||||
|
||||
$canvas.css(
|
||||
'margin'
|
||||
, 0
|
||||
).css(
|
||||
'padding'
|
||||
, 0
|
||||
).css(
|
||||
'border'
|
||||
, 'none'
|
||||
).css(
|
||||
'height'
|
||||
, settings.height === 'ratio' || !settings.height ? 1 : settings.height.toString(10)
|
||||
).css(
|
||||
'width'
|
||||
, settings.width === 'ratio' || !settings.width ? 1 : settings.width.toString(10)
|
||||
).css(
|
||||
'-ms-touch-action'
|
||||
, 'none'
|
||||
).css(
|
||||
'background-color',
|
||||
settings['background-color']
|
||||
);
|
||||
$canvas.css({
|
||||
'margin': 0,
|
||||
'padding': 0,
|
||||
'border': 'none',
|
||||
'height': settings.height === 'ratio' || !settings.height ? 1 : settings.height.toString(10),
|
||||
'width': settings.width === 'ratio' || !settings.width ? 1 : settings.width.toString(10),
|
||||
'-ms-touch-action': 'none',
|
||||
'touch-action': 'none',
|
||||
'background-color': settings['background-color'],
|
||||
});
|
||||
|
||||
$canvas.appendTo(this.$parent);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('web.ajax', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
var time = require('web.time');
|
||||
var download = require('web.download');
|
||||
@@ -9,6 +10,10 @@ var contentdisposition = require('web.contentdisposition');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
// Create the final object containing all the functions first to allow monkey
|
||||
// patching them correctly if ever needed.
|
||||
var ajax = {};
|
||||
|
||||
function _genericJsonRpc (fct_name, params, settings, fct) {
|
||||
var shadow = settings.shadow || false;
|
||||
delete settings.shadow;
|
||||
@@ -469,44 +474,79 @@ var loadAsset = (function () {
|
||||
})();
|
||||
|
||||
/**
|
||||
* Loads the given js and css libraries. Note that the ajax loadJS and loadCSS methods
|
||||
* don't do anything if the given file is already loaded.
|
||||
* Loads the given js/css libraries and asset bundles. Note that no library or
|
||||
* asset will be loaded if it was already done before.
|
||||
*
|
||||
* @param {Object} libs
|
||||
* @Param {Array | Array<Array>} [libs.jsLibs=[]] The list of JS files that we want to
|
||||
* load. The list may contain strings (the files to load), or lists of strings. The
|
||||
* first level is loaded sequentially, and files listed in inner lists are loaded in
|
||||
* parallel.
|
||||
* @param {Array<string>} [libs.cssLibs=[]] A list of css files, to be loaded in
|
||||
* parallel
|
||||
* @param {Array<string>} [libs.assetLibs=[]] A list of xmlId. The loaded template
|
||||
* contains the script and link to be loaded
|
||||
* @param {Array<string|string[]>} [libs.assetLibs=[]]
|
||||
* The list of assets to load. Each list item may be a string (the xmlID
|
||||
* of the asset to load) or a list of strings. The first level is loaded
|
||||
* sequentially (so use this if the order matters) while the assets in
|
||||
* inner lists are loaded in parallel (use this for efficiency but only
|
||||
* if the order does not matter, should rarely be the case for assets).
|
||||
* @param {string[]} [libs.cssLibs=[]]
|
||||
* The list of CSS files to load. They will all be loaded in parallel but
|
||||
* put in the DOM in the given order (only the order in the DOM is used
|
||||
* to determine priority of CSS rules, not loaded time).
|
||||
* @param {Array<string|string[]>} [libs.jsLibs=[]]
|
||||
* The list of JS files to load. Each list item may be a string (the URL
|
||||
* of the file to load) or a list of strings. The first level is loaded
|
||||
* sequentially (so use this if the order matters) while the files in inner
|
||||
* lists are loaded in parallel (use this for efficiency but only
|
||||
* if the order does not matter).
|
||||
* @param {string[]} [libs.cssContents=[]]
|
||||
* List of inline styles to add after loading the CSS files.
|
||||
* @param {string[]} [libs.jsContents=[]]
|
||||
* List of inline scripts to add after loading the JS files.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function loadLibs (libs) {
|
||||
var defs = [];
|
||||
(libs.jsLibs || []).forEach(function (urls) {
|
||||
defs.push(Promise.all(defs).then(function () {
|
||||
if (typeof(urls) === 'string') {
|
||||
return ajax.loadJS(urls);
|
||||
} else {
|
||||
return Promise.all(urls.map(ajax.loadJS));
|
||||
function loadLibs(libs) {
|
||||
var mutex = new concurrency.Mutex();
|
||||
mutex.exec(function () {
|
||||
var defs = [];
|
||||
var cssLibs = [libs.cssLibs || []]; // Force loading in parallel
|
||||
defs.push(_loadArray(cssLibs, ajax.loadCSS).then(function () {
|
||||
if (libs.cssContents && libs.cssContents.length) {
|
||||
$('head').append($('<style/>', {
|
||||
html: libs.cssContents.join('\n'),
|
||||
}));
|
||||
}
|
||||
}));
|
||||
});
|
||||
(libs.cssLibs || []).forEach(function (url) {
|
||||
defs.push(ajax.loadCSS(url));
|
||||
});
|
||||
_.each(libs.assetLibs || [], function (xmlId) {
|
||||
defs.push(loadAsset(xmlId).then(function (asset) {
|
||||
return loadLibs(asset);
|
||||
defs.push(_loadArray(libs.jsLibs || [], ajax.loadJS).then(function () {
|
||||
if (libs.jsContents && libs.jsContents.length) {
|
||||
$('head').append($('<script/>', {
|
||||
html: libs.jsContents.join('\n'),
|
||||
}));
|
||||
}
|
||||
}));
|
||||
return Promise.all(defs);
|
||||
});
|
||||
return Promise.all(defs);
|
||||
mutex.exec(function () {
|
||||
return _loadArray(libs.assetLibs || [], function (xmlID) {
|
||||
return ajax.loadAsset(xmlID).then(function (asset) {
|
||||
return ajax.loadLibs(asset);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function _loadArray(array, loadCallback) {
|
||||
var _mutex = new concurrency.Mutex();
|
||||
array.forEach(function (urlData) {
|
||||
_mutex.exec(function () {
|
||||
if (typeof urlData === 'string') {
|
||||
return loadCallback(urlData);
|
||||
}
|
||||
return Promise.all(urlData.map(loadCallback));
|
||||
});
|
||||
});
|
||||
return _mutex.getUnlockedDef();
|
||||
}
|
||||
|
||||
return mutex.getUnlockedDef();
|
||||
}
|
||||
|
||||
var ajax = {
|
||||
_.extend(ajax, {
|
||||
jsonRpc: jsonRpc,
|
||||
rpc: rpc,
|
||||
loadCSS: loadCSS,
|
||||
@@ -516,7 +556,7 @@ var ajax = {
|
||||
loadLibs: loadLibs,
|
||||
get_file: get_file,
|
||||
post: post,
|
||||
};
|
||||
});
|
||||
|
||||
return ajax;
|
||||
|
||||
|
||||
@@ -155,6 +155,10 @@ var BasicModel = AbstractModel.extend({
|
||||
// save is performed.
|
||||
this.mutex = new concurrency.Mutex();
|
||||
|
||||
// this array is used to accumulate RPC requests done in the same call
|
||||
// stack, so that they can be batched in the minimum number of RPCs
|
||||
this.batchedRPCsRequests = [];
|
||||
|
||||
this.localData = Object.create(null);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -4070,6 +4074,86 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* This function accumulates RPC requests done in the same call stack, and
|
||||
* performs them in the next micro task tick so that similar requests can be
|
||||
* batched in a single RPC.
|
||||
*
|
||||
* For now, only 'read' calls are supported.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} params
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_performRPC: function (params) {
|
||||
var self = this;
|
||||
|
||||
// save the RPC request
|
||||
var request = _.extend({}, params);
|
||||
var prom = new Promise(function (resolve, reject) {
|
||||
request.resolve = resolve;
|
||||
request.reject = reject;
|
||||
});
|
||||
this.batchedRPCsRequests.push(request);
|
||||
|
||||
// empty the pool of RPC requests in the next micro tick
|
||||
Promise.resolve().then(function () {
|
||||
if (!self.batchedRPCsRequests.length) {
|
||||
// pool has already been processed
|
||||
return;
|
||||
}
|
||||
|
||||
// reset pool of RPC requests
|
||||
var batchedRPCsRequests = self.batchedRPCsRequests;
|
||||
self.batchedRPCsRequests = [];
|
||||
|
||||
// batch similar requests
|
||||
var batches = {};
|
||||
var key;
|
||||
for (var i = 0; i < batchedRPCsRequests.length; i++) {
|
||||
var request = batchedRPCsRequests[i];
|
||||
key = request.model + ',' + JSON.stringify(request.context);
|
||||
if (!batches[key]) {
|
||||
batches[key] = _.extend({}, request, {requests: [request]});
|
||||
} else {
|
||||
batches[key].ids = _.uniq(batches[key].ids.concat(request.ids));
|
||||
batches[key].fieldNames = _.uniq(batches[key].fieldNames.concat(request.fieldNames));
|
||||
batches[key].requests.push(request);
|
||||
}
|
||||
}
|
||||
|
||||
// perform batched RPCs
|
||||
function onSuccess(batch, results) {
|
||||
for (var i = 0; i < batch.requests.length; i++) {
|
||||
var request = batch.requests[i];
|
||||
var fieldNames = request.fieldNames.concat(['id']);
|
||||
var filteredResults = results.filter(function (record) {
|
||||
return request.ids.indexOf(record.id) >= 0;
|
||||
}).map(function (record) {
|
||||
return _.pick(record, fieldNames);
|
||||
});
|
||||
request.resolve(filteredResults);
|
||||
}
|
||||
}
|
||||
function onFailure(batch, error) {
|
||||
for (var i = 0; i < batch.requests.length; i++) {
|
||||
var request = batch.requests[i];
|
||||
request.reject(error);
|
||||
}
|
||||
}
|
||||
for (key in batches) {
|
||||
var batch = batches[key];
|
||||
self._rpc({
|
||||
model: batch.model,
|
||||
method: 'read',
|
||||
args: [batch.ids, batch.fieldNames],
|
||||
context: batch.context,
|
||||
}).then(onSuccess.bind(null, batch)).guardedCatch(onFailure.bind(null, batch));
|
||||
}
|
||||
});
|
||||
|
||||
return prom;
|
||||
},
|
||||
/**
|
||||
* Once a record is created and some data has been fetched, we need to do
|
||||
* quite a lot of computations to determine what needs to be fetched. This
|
||||
@@ -4277,11 +4361,12 @@ var BasicModel = AbstractModel.extend({
|
||||
|
||||
var def;
|
||||
if (missingIDs.length && fieldNames.length) {
|
||||
def = self._rpc({
|
||||
model: list.model,
|
||||
method: 'read',
|
||||
args: [missingIDs, fieldNames],
|
||||
def = self._performRPC({
|
||||
context: list.getContext(),
|
||||
fieldNames: fieldNames,
|
||||
ids: missingIDs,
|
||||
method: 'read',
|
||||
model: list.model,
|
||||
});
|
||||
} else {
|
||||
def = Promise.resolve(_.map(missingIDs, function (id) {
|
||||
|
||||
@@ -651,8 +651,8 @@ var BasicRenderer = AbstractRenderer.extend({
|
||||
// this.renderFieldWidget(...).addClass(...), the class is added on
|
||||
// the temporary div and not on the actual element that will be
|
||||
// rendered. As we do not return a promise and some callers cannot
|
||||
// wait for this.defs, we copy those attributes to the final element.
|
||||
widget.$el.attr($el.getAttributes());
|
||||
// wait for this.defs, we copy those classnames to the final element.
|
||||
widget.$el.addClass($el.attr('class'));
|
||||
|
||||
$el.replaceWith(widget.$el);
|
||||
self._registerModifiers(node, record, widget, {
|
||||
|
||||
@@ -405,7 +405,8 @@ var ListRenderer = BasicRenderer.extend({
|
||||
isPassword: 'password' in node.attrs,
|
||||
});
|
||||
this._handleAttributes($td, node);
|
||||
return $td.html(formattedValue).attr('title', formattedValue);
|
||||
var title = field.type !== 'boolean' ? formattedValue : '';
|
||||
return $td.html(formattedValue).attr('title', title);
|
||||
},
|
||||
/**
|
||||
* Renders the button element associated to the given node and record.
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
&.o_field_many2manytags {
|
||||
flex-flow: row wrap;
|
||||
align-items: baseline;
|
||||
max-width: 100%;
|
||||
|
||||
> .o_field_widget {
|
||||
flex: 1 0 100px;
|
||||
@@ -101,6 +102,8 @@
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
@@ -115,6 +118,7 @@
|
||||
.o_delete {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8422,6 +8422,57 @@ QUnit.module('fields', {}, function () {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2manys inside a one2many are fetched in batch after onchange', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
turtles: function (obj) {
|
||||
obj.turtles = [
|
||||
[5],
|
||||
[1, 1, {
|
||||
turtle_foo: "leonardo",
|
||||
partner_ids: [[4, 2]],
|
||||
}],
|
||||
[1, 2, {
|
||||
turtle_foo: "donatello",
|
||||
partner_ids: [[4, 2], [4, 4]],
|
||||
}],
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'<field name="partner_ids" widget="many2many_tags"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
enableBasicModelBachedRPCs: true,
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method || route);
|
||||
if (args.method === 'read') {
|
||||
assert.deepEqual(args.args[0], [2, 4],
|
||||
'should read the partner_ids once, batched');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(form, '.o_data_row', 2);
|
||||
assert.strictEqual(form.$('.o_field_widget[name="partner_ids"]').text().replace(/\s/g, ''),
|
||||
"secondrecordsecondrecordaaa");
|
||||
|
||||
assert.verifySteps(['default_get', 'onchange', 'read']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('two one2many fields with same relation and onchanges', async function (assert) {
|
||||
// this test simulates the presence of two one2many fields with onchanges, such that
|
||||
// changes to the first o2m are repercuted on the second one
|
||||
|
||||
@@ -414,6 +414,39 @@ QUnit.module('Views', {
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('group containing both a field and a group', async function (assert) {
|
||||
// The purpose of this test is to check that classnames defined in a
|
||||
// field widget and those added by the form renderer are correctly
|
||||
// combined. For instance, the renderer adds className 'o_group_col_x'
|
||||
// on outer group's children (an outer group being a group that contains
|
||||
// at least a group).
|
||||
assert.expect(4);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<group>' +
|
||||
'<field name="foo"/>' +
|
||||
'<group>' +
|
||||
'<field name="int_field"/>' +
|
||||
'</group>' +
|
||||
'</group>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(form, '.o_group .o_field_widget[name=foo]');
|
||||
assert.containsOnce(form, '.o_group .o_inner_group .o_field_widget[name=int_field]');
|
||||
|
||||
assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_field_char');
|
||||
assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_group_col_6');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Form and subview with _view_ref contexts', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
@@ -485,6 +518,7 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('invisible fields are properly hidden', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
@@ -6862,7 +6896,6 @@ QUnit.module('Views', {
|
||||
delete widgetRegistry.map.test;
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('bounce edit button in readonly mode', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
|
||||
@@ -271,6 +271,19 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('boolean field has no title', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree><field name="bar"/></tree>',
|
||||
});
|
||||
assert.equal(list.$('tbody tr:first td:eq(1)').attr('title'), "");
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('record-depending invisible lines are correctly aligned', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
});
|
||||
// close modal on submit
|
||||
$('.modal').on('submit', 'form', function (ev) {
|
||||
var form = $(this).closest('form')[0];
|
||||
if (form && form.checkValidity && !form.checkValidity()) {
|
||||
return;
|
||||
}
|
||||
var modal = $(this).parentsUntil('body', '.modal');
|
||||
if (modal.hasClass('o_database_backup')) {
|
||||
$(modal).modal('hide');
|
||||
|
||||
@@ -1108,7 +1108,7 @@ var VideoWidget = MediaWidget.extend({
|
||||
return {errorCode: 0};
|
||||
}
|
||||
|
||||
var autoplay = options.autoplay ? '?autoplay=1' : '?autoplay=0';
|
||||
var autoplay = options.autoplay ? '?autoplay=1&mute=1' : '?autoplay=0';
|
||||
|
||||
if (ytMatch && ytMatch[2].length === 11) {
|
||||
$video.attr('src', '//www.youtube' + (ytMatch[1] || '') + '.com/embed/' + ytMatch[2] + autoplay);
|
||||
@@ -1137,7 +1137,8 @@ var VideoWidget = MediaWidget.extend({
|
||||
$video.attr('src', $video.attr('src') + '&rel=0');
|
||||
}
|
||||
if (options.loop && (ytMatch || vimMatch)) {
|
||||
$video.attr('src', $video.attr('src') + '&loop=1');
|
||||
var videoSrc = _.str.sprintf('%s&loop=1', $video.attr('src'));
|
||||
$video.attr('src', ytMatch ? _.str.sprintf('%s&playlist=%s', videoSrc, ytMatch[2]) : videoSrc);
|
||||
}
|
||||
if (options.hide_controls && (ytMatch || dmMatch)) {
|
||||
$video.attr('src', $video.attr('src') + '&controls=0');
|
||||
|
||||
@@ -318,7 +318,11 @@
|
||||
</div>
|
||||
<div class="o_video_dialog_options d-none mt-4">
|
||||
<div class="o_yt_option o_vim_option o_dm_option">
|
||||
<label class="o_switch mb0"><input id="o_video_autoplay" type="checkbox"/><span/>Autoplay</label>
|
||||
<label class="o_switch mb0">
|
||||
<input id="o_video_autoplay" type="checkbox"/><span/>
|
||||
<span style="margin-right: 8px;">Autoplay</span>
|
||||
<span class="small text-muted" style="margin-left: auto;">Videos are muted when autoplay is enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="o_yt_option o_vim_option">
|
||||
<label class="o_switch mb0"><input id="o_video_loop" type="checkbox"/><span/>Loop</label>
|
||||
|
||||
@@ -53,7 +53,7 @@ class Website(models.Model):
|
||||
domain = fields.Char('Website Domain')
|
||||
country_group_ids = fields.Many2many('res.country.group', 'website_country_group_rel', 'website_id', 'country_group_id',
|
||||
string='Country Groups', help='Used when multiple websites have the same domain.')
|
||||
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.ref('base.main_company').id, required=True)
|
||||
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company, required=True)
|
||||
language_ids = fields.Many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages', default=_active_languages)
|
||||
default_lang_id = fields.Many2one('res.lang', string="Default Language", default=_default_language, required=True)
|
||||
default_lang_code = fields.Char("Default language code", related='default_lang_id.code', store=True, readonly=False)
|
||||
|
||||
@@ -76,7 +76,7 @@ publicWidget.Widget.include({
|
||||
this.editableMode = this.options.editableMode || false;
|
||||
var extraEvents = this.editableMode ? this.edit_events : this.read_events;
|
||||
if (extraEvents) {
|
||||
this.events = _.extend(this.events || {}, extraEvents);
|
||||
this.events = _.extend({}, this.events || {}, extraEvents);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ tour.register('rte_translator', {
|
||||
}, {
|
||||
content: "check bis: content is translated",
|
||||
trigger: '#wrap p font:first:contains(translated french text)',
|
||||
extra_trigger: 'body:not(:has(button[data-action=save]))',
|
||||
extra_trigger: 'html[lang*="fr"] body:not(:has(button[data-action=save]))',
|
||||
}, {
|
||||
content: "check bis: placeholder translation",
|
||||
trigger: 'input[placeholder="test french placeholder"]',
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
import werkzeug
|
||||
|
||||
import odoo
|
||||
from odoo import tools
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
from odoo.modules.module import get_module_resource
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
@@ -68,45 +67,6 @@ class TestQweb(TransactionCase):
|
||||
"alt": "Marc Demo",
|
||||
}).encode('utf8'))
|
||||
|
||||
class MockObject(object):
|
||||
_log_call = []
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__ = kwargs
|
||||
def __call__(self, *args, **kwargs):
|
||||
self._log_call.append((args, kwargs))
|
||||
return self
|
||||
def __getitem__(self, index):
|
||||
return self
|
||||
|
||||
def werkzeugRaiseNotFound(*args, **kwargs):
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
class MockRequest(object):
|
||||
""" Class with context manager mocking odoo.http.request for tests """
|
||||
def __init__(self, env, website=None, context=None, multilang=True, routing=True):
|
||||
app = MockObject(routing={
|
||||
'type': 'http',
|
||||
'website': True,
|
||||
'multilang': multilang,
|
||||
})
|
||||
app.get_db_router = app.bind = app.match = app
|
||||
if not routing:
|
||||
app.match = werkzeugRaiseNotFound
|
||||
self.request = MockObject(
|
||||
env=env, context=context or {}, db=None,
|
||||
session=MockObject(
|
||||
debug=False
|
||||
),
|
||||
website=website, httprequest=MockObject(
|
||||
path='/hello/',
|
||||
app=app
|
||||
)
|
||||
)
|
||||
odoo.http._request_stack.push(self.request)
|
||||
def __enter__(self):
|
||||
return self.request
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
odoo.http._request_stack.pop()
|
||||
|
||||
class TestQwebProcessAtt(TransactionCase):
|
||||
def setUp(self):
|
||||
@@ -134,7 +94,7 @@ class TestQwebProcessAtt(TransactionCase):
|
||||
self._test_att('/a', {'href': '/a'})
|
||||
|
||||
def test_process_att_no_website(self):
|
||||
with MockRequest(self.env) as request:
|
||||
with MockRequest(self.env):
|
||||
# no website so URL rewriting
|
||||
self._test_att('/', {'href': '/'})
|
||||
self._test_att('/en_US/', {'href': '/en_US/'})
|
||||
@@ -143,7 +103,7 @@ class TestQwebProcessAtt(TransactionCase):
|
||||
self._test_att('/a', {'href': '/a'})
|
||||
|
||||
def test_process_att_monolang_route(self):
|
||||
with MockRequest(self.env, website=self.website, multilang=False) as request:
|
||||
with MockRequest(self.env, website=self.website, multilang=False):
|
||||
# lang not changed in URL but CDN enabled
|
||||
self._test_att('/a', {'href': 'http://test.cdn/a'})
|
||||
self._test_att('/en_US/a', {'href': 'http://test.cdn/en_US/a'})
|
||||
@@ -151,19 +111,19 @@ class TestQwebProcessAtt(TransactionCase):
|
||||
self._test_att('/en_US/b', {'href': '/en_US/b'})
|
||||
|
||||
def test_process_att_no_request_lang(self):
|
||||
with MockRequest(self.env, self.website) as request:
|
||||
with MockRequest(self.env, website=self.website):
|
||||
self._test_att('/', {'href': '/'})
|
||||
self._test_att('/en_US/', {'href': '/'})
|
||||
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
|
||||
|
||||
def test_process_att_with_request_lang(self):
|
||||
with MockRequest(self.env, self.website, context={'lang': 'fr_FR'}) as request:
|
||||
with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}):
|
||||
self._test_att('/', {'href': '/fr_FR/'})
|
||||
self._test_att('/en_US/', {'href': '/'})
|
||||
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
|
||||
|
||||
def test_process_att_matching_cdn_and_lang(self):
|
||||
with MockRequest(self.env, self.website) as request:
|
||||
with MockRequest(self.env, website=self.website):
|
||||
# lang prefix is added before CDN
|
||||
self._test_att('/a', {'href': 'http://test.cdn/a'})
|
||||
self._test_att('/en_US/a', {'href': 'http://test.cdn/a'})
|
||||
@@ -173,13 +133,13 @@ class TestQwebProcessAtt(TransactionCase):
|
||||
self._test_att('/fr_FR/b', {'href': '/fr_FR/b'})
|
||||
|
||||
def test_process_att_no_route(self):
|
||||
with MockRequest(self.env, self.website, context={'lang': 'fr_FR'}, routing=False) as request:
|
||||
with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}, routing=False):
|
||||
# default on multilang=True if route is not /{module}/static/
|
||||
self._test_att('/web/static/hi', {'href': '/web/static/hi'})
|
||||
self._test_att('/my-page', {'href': '/fr_FR/my-page'})
|
||||
|
||||
def test_process_att_url_crap(self):
|
||||
with MockRequest(self.env, self.website) as request:
|
||||
with MockRequest(self.env, website=self.website) as request:
|
||||
# #{fragment} is stripped from URL when testing route
|
||||
self._test_att('/x#y?z', {'href': '/x#y?z'})
|
||||
self.assertEqual(
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo
|
||||
import re
|
||||
import werkzeug
|
||||
|
||||
from odoo.tools import DotDict
|
||||
|
||||
|
||||
def get_video_embed_code(video_url):
|
||||
@@ -49,3 +53,59 @@ def get_video_embed_code(video_url):
|
||||
# We directly use the provided URL as it is
|
||||
embedUrl = video_url
|
||||
return '<iframe class="embed-responsive-item" src="%s" allowFullScreen="true" frameborder="0"></iframe>' % embedUrl
|
||||
|
||||
|
||||
class MockObject(object):
|
||||
_log_call = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__ = kwargs
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self._log_call.append((args, kwargs))
|
||||
return self
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self
|
||||
|
||||
|
||||
def werkzeugRaiseNotFound(*args, **kwargs):
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
|
||||
class MockRequest(object):
|
||||
""" Class with context manager mocking odoo.http.request for tests """
|
||||
def __init__(self, env, **kw):
|
||||
app = MockObject(routing={
|
||||
'type': 'http',
|
||||
'website': True,
|
||||
'multilang': kw.get('multilang', True),
|
||||
})
|
||||
app.get_db_router = app.bind = app.match = app
|
||||
if not kw.get('routing', True):
|
||||
app.match = werkzeugRaiseNotFound
|
||||
self.request = DotDict({
|
||||
'context': kw.get('context', {}),
|
||||
'db': None,
|
||||
'env': env,
|
||||
'httprequest': {
|
||||
'path': '/hello/',
|
||||
'app': app,
|
||||
},
|
||||
'redirect': werkzeug.utils.redirect,
|
||||
'session': {
|
||||
'geoip': {
|
||||
'country_code': kw.get('country_code'),
|
||||
},
|
||||
'debug': False,
|
||||
'sale_order_id': kw.get('sale_order_id'),
|
||||
},
|
||||
'website': kw.get('website'),
|
||||
})
|
||||
odoo.http._request_stack.push(self.request)
|
||||
|
||||
def __enter__(self):
|
||||
return self.request
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
odoo.http._request_stack.pop()
|
||||
|
||||
@@ -26,8 +26,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2>Website <button name="action_website_create_new" type="object" string="New" class="ml-2 btn btn-link" icon="fa-plus" groups="!website.group_multi_website"/></h2>
|
||||
<div class="row mt16 o_settings_container" id="website_settings">
|
||||
<div class="col-12 o_setting_box" id="website_action_setting">
|
||||
<div class="row mt16 o_settings_container" id="website_settings_placeholder" attrs="{'invisible': [('website_id', '!=', False)]}">
|
||||
<div class="col-12 o_setting_box">
|
||||
<div class="text-muted" groups="website.group_multi_website">
|
||||
Select a website to load its settings.
|
||||
</div>
|
||||
<div class="text-muted" groups="!website.group_multi_website">
|
||||
There is no website available for this company. You could create a new one.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !! Every fields inside this container should be website specific (related to website record) !! -->
|
||||
<div class="row mt16 o_settings_container" id="website_settings" attrs="{'invisible': [('website_id', '=', False)]}">
|
||||
<div class="col-12 o_setting_box" id="website_action_setting">
|
||||
<button name="website_go_to" type="object" string="Go to Website" class="btn btn-primary" icon="fa-globe"/>
|
||||
<button name="%(website.action_website_add_features)d" type="action" string="Add features" class="ml-2 btn btn-secondary" icon="fa-plus"/>
|
||||
</div>
|
||||
@@ -41,7 +52,7 @@
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label class="col-lg-3 o_light_label" string="Name" for="website_name"/>
|
||||
<field name="website_name" attrs="{'required': [('website_id', '!=', False)] }"/>
|
||||
<field name="website_name" attrs="{'required': [('website_id', '!=', False)]}"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-lg-3 o_light_label" for="favicon" />
|
||||
|
||||
@@ -192,7 +192,8 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage):
|
||||
if not qs or qs.lower() in loc:
|
||||
yield {'loc': loc}
|
||||
|
||||
partners_dom = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True), ('grade_id.website_published', '=', True)]
|
||||
partners_dom = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True),
|
||||
('grade_id.website_published', '=', True), ('country_id', '!=', False)]
|
||||
dom += sitemap_qs2dom(qs=qs, route='/partners/country/')
|
||||
countries = env['res.partner'].sudo().read_group(partners_dom, fields=['id', 'country_id'], groupby='country_id')
|
||||
for country in countries:
|
||||
|
||||
@@ -29,7 +29,7 @@ class TestPartnerAssign(TransactionCase):
|
||||
'partner_id': self.customer_uk.id
|
||||
})
|
||||
|
||||
def geo_find(addr):
|
||||
def geo_find(addr, **kw):
|
||||
return {
|
||||
'Wavre, Belgium': (50.7158956, 4.6128075),
|
||||
'Cannon Hill Park, B46 3AG Birmingham, United Kingdom': (52.45216, -1.898578),
|
||||
|
||||
@@ -128,6 +128,8 @@ class WebsiteForm(http.Controller):
|
||||
# If it's not, we'll use attachments instead
|
||||
if field_name in authorized_fields and authorized_fields[field_name]['type'] == 'binary':
|
||||
data['record'][field_name] = base64.b64encode(field_value.read())
|
||||
if authorized_fields[field_name]['manual']:
|
||||
data['record'][field_name + "_filename"] = field_value.filename
|
||||
else:
|
||||
field_value.field_name = field_name
|
||||
data['attachments'].append(field_value)
|
||||
|
||||
@@ -18,9 +18,6 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
'click .karma_required': '_onKarmaRequiredClick',
|
||||
'mouseenter .o_js_forum_tag_follow': '_onTagFollowBoxMouseEnter',
|
||||
'mouseleave .o_js_forum_tag_follow': '_onTagFollowBoxMouseLeave',
|
||||
'click .o_forum_profile_pic_edit': '_onEditProfilePicClick',
|
||||
'change .o_forum_file_upload': '_onFileUploadChange',
|
||||
'click .o_forum_profile_pic_clear': '_onProfilePicClearClick',
|
||||
'mouseenter .o_forum_user_info': '_onUserInfoMouseEnter',
|
||||
'mouseleave .o_forum_user_info': '_onUserInfoMouseLeave',
|
||||
'mouseleave .o_forum_user_bio_expand': '_onUserBioExpandMouseLeave',
|
||||
@@ -214,41 +211,6 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
_onTagFollowBoxMouseLeave: function (ev) {
|
||||
$(ev.currentTarget).find('.o_forum_tag_follow_box').stop().fadeOut().css('display', 'none');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onEditProfilePicClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
$(ev.currentTarget).closest('form').find('.o_forum_file_upload').trigger('click');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onFileUploadChange: function (ev) {
|
||||
if (!ev.currentTarget.files.length) {
|
||||
return;
|
||||
}
|
||||
var $form = $(ev.currentTarget).closest('form');
|
||||
utils.getDataURLFromFile(ev.currentTarget.files[0]).then(function (result) {
|
||||
$form.find('.o_forum_avatar_img').attr('src', result);
|
||||
});
|
||||
$form.find('#forum_clear_image').remove();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onProfilePicClearClick: function (ev) {
|
||||
var $form = $(ev.currentTarget).closest('form');
|
||||
$form.find('.o_forum_avatar_img').attr('src', '/web/static/src/img/placeholder.png');
|
||||
$form.append($('<input/>', {
|
||||
name: 'clear_image',
|
||||
id: 'forum_clear_image',
|
||||
type: 'hidden',
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
|
||||
@@ -44,6 +44,9 @@ publicWidget.registry.websiteProfile = publicWidget.Widget.extend({
|
||||
publicWidget.registry.websiteProfileEditor = publicWidget.Widget.extend({
|
||||
selector: '.o_wprofile_editor_form',
|
||||
read_events: {
|
||||
'click .o_forum_profile_pic_edit': '_onEditProfilePicClick',
|
||||
'change .o_forum_file_upload': '_onFileUploadChange',
|
||||
'click .o_forum_profile_pic_clear': '_onProfilePicClearClick',
|
||||
'click .o_wprofile_submit_btn': '_onSubmitClick',
|
||||
},
|
||||
|
||||
@@ -74,6 +77,43 @@ publicWidget.registry.websiteProfileEditor = publicWidget.Widget.extend({
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onEditProfilePicClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
$(ev.currentTarget).closest('form').find('.o_forum_file_upload').trigger('click');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onFileUploadChange: function (ev) {
|
||||
if (!ev.currentTarget.files.length) {
|
||||
return;
|
||||
}
|
||||
var $form = $(ev.currentTarget).closest('form');
|
||||
var reader = new window.FileReader();
|
||||
reader.readAsDataURL(ev.currentTarget.files[0]);
|
||||
reader.onload = function (ev) {
|
||||
$form.find('.o_forum_avatar_img').attr('src', ev.target.result);
|
||||
};
|
||||
$form.find('#forum_clear_image').remove();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onProfilePicClearClick: function (ev) {
|
||||
var $form = $(ev.currentTarget).closest('form');
|
||||
$form.find('.o_forum_avatar_img').attr('src', '/web/static/src/img/placeholder.png');
|
||||
$form.append($('<input/>', {
|
||||
name: 'clear_image',
|
||||
id: 'forum_clear_image',
|
||||
type: 'hidden',
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
@@ -97,20 +97,20 @@
|
||||
<template id="user_profile_edit_content" name="Edit Profile">
|
||||
<h1 class="o_page_header">Edit Profile</h1>
|
||||
<div>
|
||||
<form t-attf-action="/profile/user/save" method="post" role="form" class="js_website_submit_form row" enctype="multipart/form-data">
|
||||
<form t-attf-action="/profile/user/save" method="post" role="form" class="o_wprofile_editor_form js_website_submit_form row" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="file" class="d-none o_forum_file_upload" name="ufile" accept="image/*"/>
|
||||
<input type="hidden" name="url_param" t-att-value="request.params.get('url_param')"/>
|
||||
<div class="col-3">
|
||||
<div class="card o_card_people">
|
||||
<div class="card-body">
|
||||
<img class="w-100 mb-3" t-attf-src="/web/image/res.users/#{user.id}/image_medium"/>
|
||||
<img class="o_forum_avatar_img w-100 mb-3" t-attf-src="/web/image/res.users/#{user.id}/image_medium"/>
|
||||
<div class="text-center">
|
||||
<a href="#" class="o_forum_profile_pic_edit btn btn-primary" aria-label="Edit">
|
||||
<i class="fa fa-pencil fa-1g float-sm-none float-md-left" title="Edit"></i>
|
||||
</a>
|
||||
<a href="#" title="Clear" aria-label="Clear" class="btn border-primary">
|
||||
<i class="fa fa-trash-o float-sm-none float-md-right o_forum_profile_pic_clear"></i>
|
||||
<a href="#" title="Clear" aria-label="Clear" class="btn border-primary o_forum_profile_pic_clear">
|
||||
<i class="fa fa-trash-o float-sm-none float-md-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="form-group mt-3 mb-0 pt-2 border-top">
|
||||
@@ -125,7 +125,7 @@
|
||||
</div>
|
||||
<div class="col-9 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body o_wprofile_editor_form">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<input name="user_id" t-att-value="user.id" type="hidden"/>
|
||||
<div class="form-group col-12">
|
||||
@@ -258,12 +258,11 @@
|
||||
<button class="btn btn-sm d-md-none bg-white border" type="button" data-toggle="collapse" data-target="#o_wprofile_sidebar_collapse" aria-expanded="false" aria-controls="o_wprofile_sidebar_collapse">More info</button>
|
||||
</div>
|
||||
<div class="collapse d-md-block" id="o_wprofile_sidebar_collapse">
|
||||
<div t-if="user.rank_id" class="mb-3">
|
||||
<small t-if="user.next_rank_id" class="font-weight-bold mt-1">Next rank:</small>
|
||||
<t t-call="website_profile.profile_next_rank_card">
|
||||
<t t-set="img_max_width">40%</t>
|
||||
</t>
|
||||
</div>
|
||||
<t t-set="next_rank_id" t-value="user._get_next_rank()"/>
|
||||
<small t-if="next_rank_id" class="font-weight-bold mt-1">Next rank:</small>
|
||||
<t t-if="next_rank_id or user.rank_id" t-call="website_profile.profile_next_rank_card">
|
||||
<t t-set="img_max_width">40%</t>
|
||||
</t>
|
||||
|
||||
<table class="table table-sm w-100" id="o_wprofile_sidebar_table">
|
||||
<tbody>
|
||||
@@ -315,17 +314,21 @@
|
||||
<template id="profile_next_rank_card" name="Profile Next Rank Card">
|
||||
<div class="o_wprofile_progress_circle">
|
||||
<svg viewBox="0 0 36 36" class="o_pc_circular_chart">
|
||||
<t t-if="user.next_rank_id">
|
||||
<t t-if="(user.next_rank_id.karma_min - user.rank_id.karma_min) > 0">
|
||||
<t t-set="user_points" t-value="int(100*(user.karma - user.rank_id.karma_min)/(user.next_rank_id.karma_min - user.rank_id.karma_min))"/>
|
||||
<t t-set="next_rank_id" t-value="next_rank_id or user._get_next_rank()"/>
|
||||
<t t-if="next_rank_id and user.rank_id">
|
||||
<t t-if="(next_rank_id.karma_min - user.rank_id.karma_min) > 0">
|
||||
<t t-set="user_points" t-value="int(100*(user.karma - user.rank_id.karma_min)/(next_rank_id.karma_min - user.rank_id.karma_min))"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="user_points" t-value="0"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-elif="user.rank_id">
|
||||
<t t-set="user_points" t-value="100"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="user_points" t-value="0"/>
|
||||
</t>
|
||||
<path class="o_pc_circle_bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||
<path class="o_pc_circle" t-attf-stroke-dasharray="#{user_points}, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" stroke="url(#gradient)" mask="url(#mask)"/>
|
||||
<mask id="mask">
|
||||
@@ -338,18 +341,18 @@
|
||||
</svg>
|
||||
<div class="o_pc_overlay d-flex flex-column align-items-center justify-content-center">
|
||||
<img class="img-fluid"
|
||||
t-attf-src="/web/image/gamification.karma.rank/#{user.next_rank_id.id if user.next_rank_id else user.rank_id.id}/image_medium"
|
||||
t-att-alt="(user.next_rank_id.name if user.next_rank_id else user.rank_id.name) + ' badge'"
|
||||
t-attf-src="/web/image/gamification.karma.rank/#{next_rank_id.id if next_rank_id else user.rank_id.id}/image_medium"
|
||||
t-att-alt="(next_rank_id.name if next_rank_id else user.rank_id.name) + ' badge'"
|
||||
t-att-style="'max-width: ' + (img_max_width if img_max_width else '50%;')"/>
|
||||
<h4 class=" mb-0">
|
||||
"
|
||||
<span t-if="user.next_rank_id" t-field="user.next_rank_id.name"/>
|
||||
<span t-if="next_rank_id" t-field="next_rank_id.name"/>
|
||||
<span t-else="" t-field="user.rank_id.name"/>
|
||||
"
|
||||
</h4>
|
||||
<small>
|
||||
<span class="font-weight-bold text-primary" t-field="user.karma"/>/
|
||||
<span t-if="user.next_rank_id" class="font-weight-bold" t-field="user.next_rank_id.karma_min"/>
|
||||
<span t-if="next_rank_id" class="font-weight-bold" t-field="next_rank_id.karma_min"/>
|
||||
<span t-else="" class="font-weight-bold" t-field="user.rank_id.karma_min"/>
|
||||
xp
|
||||
</small>
|
||||
@@ -510,7 +513,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-inline-block position-relative">
|
||||
<img class="rounded-circle img-fluid" t-att-src="'/profile/avatar/%s?field=image_medium%s' % (user['id'], '&res_model=%s&res_id=%s' % (record._name, record.id) if record else '')"/>
|
||||
<img class="position-absolute" t-attf-src="/website_profile/static/src/img/rank_#{user['position']}.svg" alt="User rank" style="bottom: 0; right: -10px"/>
|
||||
<img class="position-absolute" t-attf-src="/website_profile/static/src/img/rank_#{user_index + 1}.svg" alt="User rank" style="bottom: 0; right: -10px"/>
|
||||
</div>
|
||||
<h3 class="mt-2 mb-0" t-esc="user['name']"></h3>
|
||||
<span class="badge badge-danger font-weight-normal px-2" t-if="not user['website_published']">Unpublished</span>
|
||||
|
||||
@@ -640,10 +640,11 @@ class WebsiteSale(http.Controller):
|
||||
new_values['customer'] = True
|
||||
new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id
|
||||
new_values['user_id'] = request.website.salesperson_id and request.website.salesperson_id.id
|
||||
new_values['website_id'] = request.website.id
|
||||
|
||||
if order.partner_id.id == request.website.user_id.sudo().partner_id.id:
|
||||
# If the partner is public, we assign the company of the website
|
||||
if request.website.specific_user_account:
|
||||
new_values['website_id'] = request.website.id
|
||||
|
||||
if mode[0] == 'new':
|
||||
new_values['company_id'] = request.website.company_id.id
|
||||
|
||||
lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
|
||||
from odoo import api
|
||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestUi(odoo.tests.HttpCase):
|
||||
@@ -33,4 +38,114 @@ class TestUi(odoo.tests.HttpCase):
|
||||
|
||||
self.start_tour("/", 'website_sale_tour')
|
||||
|
||||
# TO DO - add public test with new address when convert to web.tour format.
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestWebsiteSaleCheckoutAddress(odoo.tests.TransactionCase):
|
||||
''' The goal of this method class is to test the address management on
|
||||
the checkout (new/edit billing/shipping, company_id, website_id..).
|
||||
'''
|
||||
def setUp(self):
|
||||
super(TestWebsiteSaleCheckoutAddress, self).setUp()
|
||||
self.website = self.env['website'].browse(1)
|
||||
self.country_id = self.env['res.country'].search([], limit=1).id
|
||||
self.WebsiteSaleController = WebsiteSale()
|
||||
self.default_address_values = {
|
||||
'name': 'a res.partner address', 'email': 'email@email.email', 'street': 'ooo',
|
||||
'city': 'ooo', 'country_id': self.country_id, 'submitted': 1,
|
||||
}
|
||||
|
||||
def _create_so(self, partner_id=None):
|
||||
return self.env['sale.order'].create({
|
||||
'partner_id': partner_id,
|
||||
'website_id': self.website.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.env['product.product'].create({'name': 'Product A', 'list_price': 100}).id,
|
||||
'name': 'Product A',
|
||||
})]
|
||||
})
|
||||
|
||||
def _get_last_address(self, partner):
|
||||
''' Useful to retrieve the last created shipping address '''
|
||||
return partner.child_ids.sorted('id', reverse=True)[0]
|
||||
|
||||
# TEST WEBSITE
|
||||
def test_01_create_shipping_address_specific_user_account(self):
|
||||
''' Ensure `website_id` is correctly set (specific_user_account) '''
|
||||
p = self.env.user.partner_id
|
||||
so = self._create_so(p.id)
|
||||
|
||||
with MockRequest(self.env, website=self.website, sale_order_id=so.id):
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
self.assertFalse(self._get_last_address(p).website_id, "New shipping address should not have a website set on it (no specific_user_account).")
|
||||
|
||||
self.website.specific_user_account = True
|
||||
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
self.assertEqual(self._get_last_address(p).website_id, self.website, "New shipping address should have a website set on it (specific_user_account).")
|
||||
|
||||
# TEST COMPANY
|
||||
def _setUp_multicompany_env(self):
|
||||
''' Have 2 companies A & B.
|
||||
Have 1 website 1 which company is B
|
||||
Have admin on company A
|
||||
'''
|
||||
self.company_a = self.env['res.company'].create({
|
||||
'name': 'Company A',
|
||||
})
|
||||
self.company_b = self.env['res.company'].create({
|
||||
'name': 'Company B',
|
||||
})
|
||||
self.company_c = self.env['res.company'].create({
|
||||
'name': 'Company C',
|
||||
})
|
||||
self.website.company_id = self.company_b
|
||||
self.env.user.company_id = self.company_a
|
||||
|
||||
self.demo_user = self.env.ref('base.user_demo')
|
||||
self.demo_user.company_ids += self.company_c
|
||||
self.demo_user.company_id = self.company_c
|
||||
self.demo_partner = self.demo_user.partner_id
|
||||
|
||||
def test_02_demo_address_and_company(self):
|
||||
''' This test ensure that the company_id of the address (partner) is
|
||||
correctly set and also, is not wrongly changed.
|
||||
eg: new shipping should use the company of the website and not the
|
||||
one from the admin, and editing a billing should not change its
|
||||
company.
|
||||
'''
|
||||
self._setUp_multicompany_env()
|
||||
so = self._create_so(self.demo_partner.id)
|
||||
|
||||
env = api.Environment(self.env.cr, self.demo_user.id, {})
|
||||
# change also website env for `sale_get_order` to not change order partner_id
|
||||
with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id):
|
||||
# 1. Logged in user, new shipping
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
new_shipping = self._get_last_address(self.demo_partner)
|
||||
self.assertTrue(new_shipping.company_id != self.env.user.company_id, "Logged in user new shipping should not get the company of the sudo() neither the one from it's partner..")
|
||||
self.assertEqual(new_shipping.company_id, self.website.company_id, ".. but the one from the website.")
|
||||
|
||||
# 2. Logged in user, edit billing
|
||||
self.default_address_values['partner_id'] = self.demo_partner.id
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
self.assertEqual(self.demo_partner.company_id, self.company_c, "Logged in user edited billing (the partner itself) should not get its company modified.")
|
||||
|
||||
def test_03_public_user_address_and_company(self):
|
||||
''' Same as test_02 but with public user '''
|
||||
self._setUp_multicompany_env()
|
||||
so = self._create_so(self.website.user_id.partner_id.id)
|
||||
|
||||
env = api.Environment(self.env.cr, self.website.user_id.id, {})
|
||||
# change also website env for `sale_get_order` to not change order partner_id
|
||||
with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id):
|
||||
# 1. Public user, new billing
|
||||
self.default_address_values['partner_id'] = -1
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
new_partner = so.partner_id
|
||||
self.assertNotEqual(new_partner, self.website.user_id.partner_id, "New billing should have created a new partner and assign it on the SO")
|
||||
self.assertEqual(new_partner.company_id, self.website.company_id, "The new partner should get the company of the website")
|
||||
|
||||
# 2. Public user, edit billing
|
||||
self.default_address_values['partner_id'] = new_partner.id
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
self.assertEqual(new_partner.company_id, self.website.company_id, "Public user edited billing (the partner itself) should not get its company modified.")
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<field name="name">Orders</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">tree,form,kanban,activity</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{'show_sale': True, 'search_default_order_confirmed': 1, 'search_default_from_website': 1}</field>
|
||||
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
|
||||
<field name="help" type="html">
|
||||
|
||||
@@ -12,4 +12,4 @@ class TestUi(odoo.tests.HttpCase):
|
||||
'free_over': True,
|
||||
'amount': 10,
|
||||
})
|
||||
self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('check_free_delivery')", "odoo.__DEBUG__.services['web_tour.tour'].tours.check_free_delivery.ready", login="admin")
|
||||
self.start_tour("/", 'check_free_delivery', login="admin")
|
||||
|
||||
@@ -19,10 +19,15 @@
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/website_sale_delivery/static/src/js/website_sale_delivery.js"></script>
|
||||
<link rel="stylesheet" type="text/scss" href="/website_sale_delivery/static/src/scss/website_sale_delivery.scss"/>
|
||||
<script type="text/javascript" src="/website_sale_delivery/static/src/js/website_free_delivery_tour.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_tests" name="Website Sale Delivery Assets Tests" inherit_id="web.assets_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/website_sale_delivery/static/tests/tours/website_free_delivery.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="payment_delivery_methods">
|
||||
<input t-att-value="delivery.id" t-att-id="'delivery_%i' % delivery.id" type="radio" name="delivery_type" t-att-checked="order.carrier_id and order.carrier_id.id == delivery.id and 'checked' or False" t-att-class="'d-none' if delivery_nb == 1 else ''"/>
|
||||
<label class="label-optional" t-field="delivery.name"/>
|
||||
|
||||
@@ -257,7 +257,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
if request.env.user._is_public():
|
||||
users = request.env['res.users'].sudo().search([
|
||||
('karma', '>', 0),
|
||||
('website_published', '=', True)], limit=5, order='create_date desc')
|
||||
('website_published', '=', True)], limit=5, order='karma desc')
|
||||
else:
|
||||
users = None
|
||||
|
||||
@@ -323,6 +323,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
def _get_top3_users(self):
|
||||
return request.env['res.users'].sudo().search_read([
|
||||
('karma', '>', 0),
|
||||
('website_published', '=', True),
|
||||
('image', '!=', False)], ['id'], limit=3, order='karma desc')
|
||||
|
||||
@http.route([
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
<record id="slide_template_shared" model="mail.template">
|
||||
<field name="name">Slide Shared</field>
|
||||
<field name="model_id" ref="model_slide_slide"/>
|
||||
<field name="subject">${user.name} shared a ${object.slide_type} with you!</field>
|
||||
<field name="subject">${ctx['user'].name} shared a ${object.slide_type} with you!</field>
|
||||
<field name="email_from">${user.email_formatted | safe}</field>
|
||||
<field name="email_to">${ctx['email']}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Hello<br/><br/>
|
||||
${user.name} shared the ${object.slide_type} <strong>${object.name}</strong> with you!
|
||||
${ctx['user'].name} shared the ${object.slide_type} <strong>${object.name}</strong> with you!
|
||||
<div style="margin: 16px 8px 16px 8px; text-align: center;">
|
||||
<a href="${object.website_url}">
|
||||
<img alt="${object.name}" src="${ctx['base_url']}/web/image/slide.slide/${object.id}/image" style="height:auto; width:150px; margin: 16px;"/>
|
||||
|
||||
@@ -269,12 +269,18 @@ class Channel(models.Model):
|
||||
return _("Publishing is restricted to the responsible of training courses or members of the publisher group for documentation courses")
|
||||
|
||||
@api.multi
|
||||
@api.depends('name')
|
||||
def get_base_url(self):
|
||||
self.ensure_one()
|
||||
icp = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
return self.website_id and self.website_id._get_http_domain() or icp
|
||||
|
||||
@api.multi
|
||||
@api.depends('name', 'website_id.domain')
|
||||
def _compute_website_url(self):
|
||||
super(Channel, self)._compute_website_url()
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for channel in self:
|
||||
if channel.id: # avoid to perform a slug on a not yet saved record in case of an onchange.
|
||||
base_url = channel.get_base_url()
|
||||
channel.website_url = '%s/slides/%s' % (base_url, slug(channel))
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -242,13 +242,13 @@ class Slide(models.Model):
|
||||
self[key] = value
|
||||
|
||||
@api.multi
|
||||
@api.depends('name')
|
||||
@api.depends('name', 'channel_id.website_id.domain')
|
||||
def _compute_website_url(self):
|
||||
# TDE FIXME: clena this link.tracker strange stuff
|
||||
super(Slide, self)._compute_website_url()
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for slide in self:
|
||||
if slide.id: # avoid to perform a slug on a not yet saved record in case of an onchange.
|
||||
base_url = slide.channel_id.get_base_url()
|
||||
# link_tracker is not in dependencies, so use it to shorten url only if installed.
|
||||
if self.env.registry.get('link.tracker'):
|
||||
url = self.env['link.tracker'].sudo().create({
|
||||
@@ -379,7 +379,10 @@ class Slide(models.Model):
|
||||
mail_ids = []
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for record in self:
|
||||
mail_ids.append(self.channel_id.share_template_id.with_context(email=email, base_url=base_url).send_mail(record.id, notif_layout='mail.mail_notification_light'))
|
||||
if self.env.user.has_group('base.group_portal'):
|
||||
mail_ids.append(self.channel_id.share_template_id.with_context(user=self.env.user, email=email, base_url=base_url).sudo().send_mail(record.id, notif_layout='mail.mail_notification_light', email_values={'email_from': self.env['res.company'].catchall or self.env['res.company'].email}))
|
||||
else:
|
||||
mail_ids.append(self.channel_id.share_template_id.with_context(user=self.env.user, email=email, base_url=base_url).send_mail(record.id, notif_layout='mail.mail_notification_light'))
|
||||
return mail_ids
|
||||
|
||||
def action_like(self):
|
||||
|
||||
@@ -57,13 +57,11 @@ odoo.define('website_slides.quiz', function (require) {
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
var def = new Promise(function () {});
|
||||
if (this.quiz) {
|
||||
def.resolve();
|
||||
} else {
|
||||
def = this._fetchQuiz();
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
if (!this.quiz) {
|
||||
defs.push(this._fetchQuiz());
|
||||
}
|
||||
return Promise.all([this._super.apply(this, arguments), def]);
|
||||
return Promise.all(defs);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -329,7 +327,7 @@ odoo.define('website_slides.quiz', function (require) {
|
||||
_extractChannelData: function (slideData){
|
||||
return {
|
||||
id: slideData.channelId,
|
||||
channelEnroll: slideData.enroll,
|
||||
channelEnroll: slideData.channelEnroll,
|
||||
signupAllowed: slideData.signupAllowed
|
||||
};
|
||||
},
|
||||
|
||||
@@ -355,7 +355,7 @@
|
||||
<div class="row o_wslides_home_aside_title">
|
||||
<div class="col">
|
||||
<a href="/profile/users" class="float-right">View all</a>
|
||||
<h5 class="m-0">Leaderboard of the week</h5>
|
||||
<h5 class="m-0">Leaderboard</h5>
|
||||
<hr class="mt-2 pt-2"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,14 +396,14 @@
|
||||
<img t-attf-src="/web/image/gamification.karma.rank/#{user.rank_id.id}/image_small" height="16" alt="" class="mr-1"/>
|
||||
<a href="/profile/ranks_badges" t-field="user.rank_id"/>
|
||||
</div>
|
||||
<t t-if="user.rank_id">
|
||||
<div t-if="user.next_rank_id" class="font-weight-bold text-muted mt-1">Next rank:</div>
|
||||
<t t-call="website_profile.profile_next_rank_card">
|
||||
<t t-set="bg_class">bg-200</t>
|
||||
<t t-set="img_max_width">50%</t>
|
||||
</t>
|
||||
<div t-field="user.rank_id.description_motivational"/>
|
||||
<t t-set="next_rank_id" t-value="user._get_next_rank()"/>
|
||||
<div t-if="next_rank_id" class="font-weight-bold text-muted mt-1">Next rank:</div>
|
||||
<t t-if="next_rank_id or user.rank_id" t-call="website_profile.profile_next_rank_card">
|
||||
<t t-set="bg_class">bg-200</t>
|
||||
<t t-set="img_max_width">50%</t>
|
||||
</t>
|
||||
<div t-if="next_rank_id" t-field="next_rank_id.description_motivational"/>
|
||||
<div t-else="">Congratulations, you have reached the last rank!</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -46,3 +46,4 @@ Erick Birbe erick@vauxoo.com https://github.com/ebirbe
|
||||
Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro
|
||||
Carmen Liliana Miranda González carmen@vauxoo.com https://github.com/CarmenMiranda
|
||||
Arturo Flores arturo@vauxoo.com https://github.com/umiphos
|
||||
Deivis Laya deivis@vauxoo.com https://github.com/deivislaya
|
||||
|
||||
@@ -7,6 +7,7 @@ from odoo.tools.misc import find_in_path
|
||||
from odoo.tools import config
|
||||
from odoo.sql_db import TestCursor
|
||||
from odoo.http import request
|
||||
from odoo.osv.expression import NEGATIVE_TERM_OPERATORS, FALSE_DOMAIN
|
||||
|
||||
import time
|
||||
import base64
|
||||
@@ -24,6 +25,7 @@ from distutils.version import LooseVersion
|
||||
from reportlab.graphics.barcode import createBarcodeDrawing
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Iterable
|
||||
from PIL import Image, ImageFile
|
||||
# Allow truncated images
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
@@ -88,7 +90,8 @@ class IrActionsReport(models.Model):
|
||||
name = fields.Char(translate=True)
|
||||
type = fields.Char(default='ir.actions.report')
|
||||
binding_type = fields.Selection(default='report')
|
||||
model = fields.Char(required=True)
|
||||
model = fields.Char(required=True, string='Model Name')
|
||||
model_id = fields.Many2one('ir.model', string='Model', compute='_compute_model_id', search='_search_model_id')
|
||||
|
||||
report_type = fields.Selection([
|
||||
('qweb-html', 'HTML'),
|
||||
@@ -114,6 +117,32 @@ class IrActionsReport(models.Model):
|
||||
attachment = fields.Char(string='Save as Attachment Prefix',
|
||||
help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.')
|
||||
|
||||
@api.depends('model')
|
||||
def _compute_model_id(self):
|
||||
for action in self:
|
||||
action.model_id = self.env['ir.model']._get(action.model).id
|
||||
|
||||
def _search_model_id(self, operator, value):
|
||||
ir_model_ids = None
|
||||
if isinstance(value, str):
|
||||
names = self.env['ir.model'].name_search(value, operator=operator)
|
||||
ir_model_ids = [n[0] for n in names]
|
||||
|
||||
elif isinstance(value, Iterable):
|
||||
ir_model_ids = value
|
||||
|
||||
elif isinstance(value, int) and not isinstance(value, bool):
|
||||
ir_model_ids = [value]
|
||||
|
||||
if ir_model_ids:
|
||||
operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
|
||||
ir_model = self.env['ir.model'].browse(ir_model_ids)
|
||||
return [('model', operator, ir_model.mapped('model'))]
|
||||
elif isinstance(value, bool) or value is None:
|
||||
return [('model', operator, value)]
|
||||
else:
|
||||
return FALSE_DOMAIN
|
||||
|
||||
@api.multi
|
||||
def associated_view(self):
|
||||
"""Used in the ir.actions.report form view in order to search naively after the view(s)
|
||||
|
||||
@@ -449,6 +449,16 @@ class IrAttachment(models.Model):
|
||||
|
||||
# sort result according to the original sort ordering
|
||||
result = [id for id in orig_ids if id in ids]
|
||||
|
||||
# If the original search reached the limit, it is important the
|
||||
# filtered record set does so too. When a JS view recieve a
|
||||
# record set whose length is bellow the limit, it thinks it
|
||||
# reached the last page.
|
||||
if len(orig_ids) == limit and len(result) < len(orig_ids):
|
||||
result.extend(self._search(args, offset=offset + len(orig_ids),
|
||||
limit=limit, order=order, count=count,
|
||||
access_rights_uid=access_rights_uid)[:limit - len(result)])
|
||||
|
||||
return len(result) if count else list(result)
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -279,6 +279,11 @@ class IrMailServer(models.Model):
|
||||
smtp_user = pycompat.to_text(ustr(smtp_user))
|
||||
smtp_password = pycompat.to_text(ustr(smtp_password))
|
||||
connection.login(smtp_user, smtp_password)
|
||||
|
||||
# Some methods of SMTP don't check whether EHLO/HELO was sent.
|
||||
# Anyway, as it may have been sent by login(), all subsequent usages should consider this command as sent.
|
||||
connection.ehlo_or_helo_if_needed()
|
||||
|
||||
return connection
|
||||
|
||||
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
|
||||
|
||||
@@ -851,8 +851,13 @@ class IrModelFields(models.Model):
|
||||
keys = [key for key in new_vals if old_vals[key] != new_vals[key]]
|
||||
self.pool.post_init(record.modified, keys)
|
||||
old_vals.update(new_vals)
|
||||
if module and not field.manual and (module == model._original_module or module in field._modules):
|
||||
to_xmlids.append(name)
|
||||
if module and (module == model._original_module or module in field._modules):
|
||||
# remove this and only keep the else clause if version >= saas-12.4
|
||||
if field.manual:
|
||||
self.pool.loaded_xmlids.add(
|
||||
'%s.field_%s__%s' % (module, model._name.replace('.', '_'), name))
|
||||
else:
|
||||
to_xmlids.append(name)
|
||||
|
||||
if to_insert:
|
||||
# insert missing fields
|
||||
|
||||
@@ -440,8 +440,6 @@ class Partner(models.Model):
|
||||
field = self._fields[fname]
|
||||
if field.type == 'many2one':
|
||||
values[fname] = self[fname].id
|
||||
if fname == 'company_id' and not self[fname].id:
|
||||
values.pop('company_id')
|
||||
elif field.type == 'one2many':
|
||||
raise AssertionError(_('One2Many fields cannot be synchronized as part of `commercial_fields` or `address fields`'))
|
||||
elif field.type == 'many2many':
|
||||
@@ -473,7 +471,7 @@ class Partner(models.Model):
|
||||
partners that aren't `commercial entities` themselves, and will be
|
||||
delegated to the parent `commercial entity`. The list is meant to be
|
||||
extended by inheriting classes. """
|
||||
return ['vat', 'credit_limit', 'company_id']
|
||||
return ['vat', 'credit_limit']
|
||||
|
||||
@api.multi
|
||||
def _commercial_sync_from_company(self):
|
||||
@@ -700,7 +698,7 @@ class Partner(models.Model):
|
||||
if not name and partner.type in ['invoice', 'delivery', 'other']:
|
||||
name = dict(self.fields_get(['type'])['type']['selection'])[partner.type]
|
||||
if not partner.is_company:
|
||||
name = "%s, %s" % (partner.commercial_company_name or partner.parent_id.name, name)
|
||||
name = "%s, %s" % (partner.commercial_company_name or partner.sudo().parent_id.name, name)
|
||||
if self._context.get('show_address_only'):
|
||||
name = partner._display_address(without_company=True)
|
||||
if self._context.get('show_address'):
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user