[MERGE] forward port branch saas-12.2 up to 8747d72f1e
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
include requirements.txt
|
||||
include LICENSE
|
||||
include README.md
|
||||
include odoo/addons/mail/static/scripts/odoo-mailgate.py
|
||||
recursive-include odoo *.css
|
||||
recursive-include odoo *.csv
|
||||
recursive-include odoo *.doc
|
||||
|
||||
@@ -98,7 +98,10 @@ class AccountInvoice(models.Model):
|
||||
currency_clause = [('currency_id', '=', currency_id)]
|
||||
if currency_id == company_currency_id:
|
||||
currency_clause = ['|', ('currency_id', '=', False)] + currency_clause
|
||||
return self.env['account.journal'].search(domain + currency_clause, limit=1)
|
||||
return (
|
||||
self.env['account.journal'].search(domain + currency_clause, limit=1)
|
||||
or self.env['account.journal'].search(domain, limit=1)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _default_currency(self):
|
||||
|
||||
@@ -482,8 +482,8 @@ class AccountMove(models.Model):
|
||||
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
|
||||
|
||||
@@ -467,6 +467,8 @@ class AccountReconcileModel(models.Model):
|
||||
(
|
||||
line_partner.partner_id = 0
|
||||
AND
|
||||
TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')) != ''
|
||||
AND
|
||||
(
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<field name="composition_mode" invisible="1"/>
|
||||
<field name="composer_id" invisible="1"/>
|
||||
<field name="invoice_ids" invisible="1"/>
|
||||
<field name="email_from" invisible="1" />
|
||||
<div name="option_print">
|
||||
<field name="is_print" />
|
||||
<b><label for="is_print"/></b>
|
||||
|
||||
@@ -305,9 +305,12 @@ class AccountVoucher(models.Model):
|
||||
}
|
||||
# Create one line per tax and fix debit-credit for the move line if there are tax included
|
||||
if (line.tax_ids):
|
||||
tax_group = line.tax_ids.compute_all(line.price_unit, line.currency_id, line.quantity, line.product_id, self.partner_id)
|
||||
tax_group = line.tax_ids.compute_all(self._convert(line.price_unit), self.company_id.currency_id, line.quantity, line.product_id, self.partner_id)
|
||||
if move_line['debit']: move_line['debit'] = tax_group['total_excluded']
|
||||
if move_line['credit']: move_line['credit'] = tax_group['total_excluded']
|
||||
Currency = self.env['res.currency']
|
||||
company_cur = Currency.browse(company_currency)
|
||||
current_cur = Currency.browse(current_currency)
|
||||
for tax_vals in tax_group['taxes']:
|
||||
if tax_vals['amount']:
|
||||
tax = self.env['account.tax'].browse([tax_vals['id']])
|
||||
@@ -328,8 +331,8 @@ class AccountVoucher(models.Model):
|
||||
ctx = {}
|
||||
if self.account_date:
|
||||
ctx['date'] = self.account_date
|
||||
temp['currency_id'] = current_currency.id
|
||||
temp['amount_currency'] = company_currency._convert(tax_vals['amount'], current_currency, line.company_id, self.account_date or fields.Date.today(), round=True)
|
||||
temp['currency_id'] = current_currency
|
||||
temp['amount_currency'] = company_cur._convert(tax_vals['amount'], current_cur, line.company_id, self.account_date or fields.Date.today(), round=True)
|
||||
self.env['account.move.line'].create(temp)
|
||||
|
||||
self.env['account.move.line'].create(move_line)
|
||||
|
||||
@@ -83,7 +83,9 @@ class SaleOrderLine(models.Model):
|
||||
company = self.event_id.company_id or self.env.user.company_id
|
||||
currency = company.currency_id or self.env.user.company_id.currency_id
|
||||
self.price_unit = currency._convert(
|
||||
self.event_ticket_id.price, self.order_id.currency_id, self.order_id.company_id, self.order_id.date_order or fields.Date.today())
|
||||
self.event_ticket_id.price, self.order_id.currency_id,
|
||||
self.order_id.company_id or self.env.user.company_id,
|
||||
self.order_id.date_order or fields.Date.today())
|
||||
|
||||
# we call this to force update the default name
|
||||
self.product_id_change()
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<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"/>
|
||||
<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">1</field>
|
||||
@@ -28,7 +28,7 @@
|
||||
<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"/>
|
||||
<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">100</field>
|
||||
@@ -41,7 +41,7 @@
|
||||
<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"/>
|
||||
<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">500</field>
|
||||
@@ -54,7 +54,7 @@
|
||||
<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"/>
|
||||
<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">2000</field>
|
||||
|
||||
@@ -55,10 +55,10 @@ class HrExpense(models.Model):
|
||||
tax_ids = fields.Many2many('account.tax', 'expense_tax', 'expense_id', 'tax_id', string='Taxes', states={'done': [('readonly', True)], 'post': [('readonly', True)]})
|
||||
untaxed_amount = fields.Float("Subtotal", store=True, compute='_compute_amount', digits=dp.get_precision('Account'))
|
||||
total_amount = fields.Monetary("Total", compute='_compute_amount', store=True, currency_field='currency_id', digits=dp.get_precision('Account'))
|
||||
company_currency_id = fields.Many2one('res.currency', string="Report Company Currency", related='sheet_id.currency_id', store=True, readonly=False)
|
||||
total_amount_company = fields.Monetary("Total (Company Currency)", compute='_compute_total_amount_company', store=True, currency_field='company_currency_id', digits=dp.get_precision('Account'))
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id)
|
||||
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id)
|
||||
company_currency_id = fields.Many2one('res.currency', string="Report Company Currency", related='sheet_id.currency_id', store=True, readonly=False)
|
||||
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, oldname='analytic_account')
|
||||
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
|
||||
account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=_default_account_id, help="An expense account is expected")
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<record id="holiday_status_hw" model="hr.leave.type">
|
||||
<field name="name">Home Workings</field>
|
||||
<field name="color_name">lightgreen</field>
|
||||
<field name="validity_start" eval="False"/>
|
||||
<field name="leave_notif_subtype_id" eval="ref('mt_leave_home_working')"/>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<field name="allocation_type">fixed_allocation</field>
|
||||
<field name="color_name">blue</field>
|
||||
<field name="request_unit">hour</field>
|
||||
<field name="validity_start" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_holiday_status_dv" model="hr.leave.type">
|
||||
@@ -15,6 +16,7 @@
|
||||
<field name="allocation_type">fixed</field>
|
||||
<field name="color_name">brown</field>
|
||||
<field name="validation_type">both</field>
|
||||
<field name="validity_start" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="holiday_status_cla" model="hr.leave.type">
|
||||
|
||||
@@ -168,7 +168,7 @@ class Employee(models.Model):
|
||||
employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
|
||||
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
|
||||
employee.current_leave_id = leave_data.get(employee.id, {}).get('current_leave_id')
|
||||
employee.is_absent = leave_data and leave_data.get(employee.id, {}).get('current_leave_state') not in ['cancel', 'refuse', 'draft']
|
||||
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') not in ['cancel', 'refuse', 'draft']
|
||||
|
||||
@api.onchange('parent_id')
|
||||
def _onchange_parent_id(self):
|
||||
|
||||
@@ -522,16 +522,25 @@ class HolidaysRequest(models.Model):
|
||||
@api.constrains('holiday_status_id', 'date_to', 'date_from')
|
||||
def _check_leave_type_validity(self):
|
||||
for leave in self:
|
||||
vstart = leave.holiday_status_id.validity_start
|
||||
vstop = leave.holiday_status_id.validity_stop
|
||||
dfrom = leave.date_from
|
||||
dto = leave.date_to
|
||||
if leave.holiday_status_id.validity_start and leave.holiday_status_id.validity_stop:
|
||||
vstart = leave.holiday_status_id.validity_start
|
||||
vstop = leave.holiday_status_id.validity_stop
|
||||
dfrom = leave.date_from
|
||||
dto = leave.date_to
|
||||
|
||||
if dfrom and dto and (dfrom.date() < vstart or dto.date() > vstop):
|
||||
raise UserError(
|
||||
_('You can take %s only between %s and %s') % (
|
||||
leave.holiday_status_id.display_name, leave.holiday_status_id.validity_start, leave.holiday_status_id.validity_stop))
|
||||
elif leave.holiday_status_id.validity_start:
|
||||
if dfrom and (dfrom.date() < vstart):
|
||||
raise UserError(
|
||||
_('You can take %s from %s') % (
|
||||
leave.holiday_status_id.display_name, leave.holiday_status_id.validity_start))
|
||||
elif leave.holiday_status_id.validity_stop:
|
||||
if dto and (dto.date() > vstop):
|
||||
raise UserError(
|
||||
_('You can take %s until %s') % (
|
||||
leave.holiday_status_id.display_name, leave.holiday_status_id.validity_stop))
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
|
||||
@@ -183,7 +183,7 @@ class HolidaysType(models.Model):
|
||||
if request.leave_type_request_unit == 'hour'
|
||||
else request.number_of_days)
|
||||
|
||||
for allocation in allocations:
|
||||
for allocation in allocations.sudo():
|
||||
status_dict = result[allocation.holiday_status_id.id]
|
||||
if allocation.state == 'validate':
|
||||
# note: add only validated allocation even for the virtual
|
||||
|
||||
@@ -19,6 +19,7 @@ class TestLeavesRights(TestHrHolidaysBase):
|
||||
'name': 'Unlimited',
|
||||
'validation_type': 'hr',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
})
|
||||
self.rd_dept.manager_id = False
|
||||
self.hr_dept.manager_id = False
|
||||
|
||||
@@ -20,12 +20,14 @@ class TestAccrualAllocations(TestHrHolidaysBase):
|
||||
self.accrual_type = LeaveType.create({
|
||||
'name': 'accrual',
|
||||
'allocation_type': 'fixed',
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
self.unpaid_type = LeaveType.create({
|
||||
'name': 'unpaid',
|
||||
'allocation_type': 'no',
|
||||
'unpaid': True,
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
self.set_employee_create_date(self.employee_emp_id, '2010-02-03 00:00:00')
|
||||
|
||||
@@ -18,6 +18,7 @@ class TestChangeDepartment(TestHrHolidaysBase):
|
||||
'name': 'NotLimitedHR',
|
||||
'allocation_type': 'no',
|
||||
'validation_type': 'hr',
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
def create_holiday(name, start, end):
|
||||
|
||||
@@ -29,11 +29,13 @@ class TestHolidaysFlow(TestHrHolidaysBase):
|
||||
'name': 'NotLimitedHR',
|
||||
'allocation_type': 'no',
|
||||
'validation_type': 'hr',
|
||||
'validity_start': False,
|
||||
})
|
||||
self.holidays_status_manager = HolidayStatusManagerGroup.create({
|
||||
'name': 'NotLimitedManager',
|
||||
'allocation_type': 'no',
|
||||
'validation_type': 'manager',
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
HolidaysEmployeeGroup = Requests.sudo(self.user_employee_id)
|
||||
@@ -95,13 +97,15 @@ class TestHolidaysFlow(TestHrHolidaysBase):
|
||||
HolidayStatusManagerGroup.create({
|
||||
'name': 'WithMeetingType',
|
||||
'allocation_type': 'no',
|
||||
'categ_id': self.env['calendar.event.type'].sudo(self.user_hrmanager_id).create({'name': 'NotLimitedMeetingType'}).id
|
||||
'categ_id': self.env['calendar.event.type'].sudo(self.user_hrmanager_id).create({'name': 'NotLimitedMeetingType'}).id,
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
self.holidays_status_limited = HolidayStatusManagerGroup.create({
|
||||
'name': 'Limited',
|
||||
'allocation_type': 'fixed',
|
||||
'validation_type': 'both',
|
||||
'validity_start': False,
|
||||
})
|
||||
HolidaysEmployeeGroup = Requests.sudo(self.user_employee_id)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class TestHrLeaveType(TestHrHolidaysBase):
|
||||
'name': 'Paid Time Off',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
leave_1 = self.env['hr.leave'].create({
|
||||
|
||||
@@ -32,11 +32,13 @@ class TestLeaveRequests(TestHrHolidaysBase):
|
||||
'name': 'NotLimitedHR',
|
||||
'allocation_type': 'no',
|
||||
'validation_type': 'hr',
|
||||
'validity_start': False,
|
||||
})
|
||||
self.holidays_type_2 = LeaveType.create({
|
||||
'name': 'Limited',
|
||||
'allocation_type': 'fixed',
|
||||
'validation_type': 'hr',
|
||||
'validity_start': False,
|
||||
})
|
||||
self.holidays_type_3 = LeaveType.create({
|
||||
'name': 'TimeNotLimited',
|
||||
@@ -173,3 +175,18 @@ class TestLeaveRequests(TestHrHolidaysBase):
|
||||
allocation_form = Form(self.env['hr.leave.allocation'].sudo(self.user_employee))
|
||||
allocation_form.holiday_status_id = self.holidays_type_1
|
||||
allocation = allocation_form.save()
|
||||
|
||||
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
|
||||
def test_employee_is_absent(self):
|
||||
""" Only the concerned employee should be considered absent """
|
||||
self.env['hr.leave'].sudo(self.user_employee_id).create({
|
||||
'name': 'Hol11',
|
||||
'employee_id': self.employee_emp_id,
|
||||
'holiday_status_id': self.holidays_type_1.id,
|
||||
'date_from': (fields.Datetime.now() - relativedelta(days=1)),
|
||||
'date_to': fields.Datetime.now() + relativedelta(days=1),
|
||||
'number_of_days': 2,
|
||||
})
|
||||
(self.employee_emp | self.employee_hrmanager).mapped('is_absent') # compute in batch
|
||||
self.assertTrue(self.employee_emp.is_absent, "He should be considered absent")
|
||||
self.assertFalse(self.employee_hrmanager.is_absent, "He should not be considered absent")
|
||||
|
||||
@@ -17,6 +17,7 @@ class TestOutOfOffice(TestHrHolidaysBase):
|
||||
'name': 'Legal Leaves',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
def test_leave_ooo(self):
|
||||
@@ -87,6 +88,7 @@ class TestOutOfOfficePerformance(TestHrHolidaysBase):
|
||||
'name': 'Legal Leaves',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
})
|
||||
self.leave_date_end = (datetime.today() + relativedelta(days=3))
|
||||
self.leave = self.env['hr.leave'].create({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"id","name","code","user_type_id/id","chart_template_id/id","reconcile"
|
||||
"chart1141_en","Stock In Hand","1141","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart1145_en","Stock Delivered But Not Billed","1145","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart1145_en","Stock Delivered But Not Billed","1145","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","True"
|
||||
"chart1151_en","Customers Account","1151","account.data_account_type_receivable","l10n_ca.ca_en_chart_template_en","True"
|
||||
"chart1181_en","GST receivable","1181","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart1182_en","PST/QST receivable","1182","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False"
|
||||
@@ -27,7 +27,7 @@
|
||||
"chart21424_en","Labour Health and Safety to pay","21424","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart21425_en","Labour Standards to pay","21425","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart21426_en","Provincial Income Tax","21426","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart2171_en","Stock Received But Not Billed","2171","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart2171_en","Stock Received But Not Billed","2171","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","True"
|
||||
"chart2181_en","Salaries to pay","2181","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart2183_en","Bonus to pay","2183","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False"
|
||||
"chart2184_en","Retroactive Payment to pay","2184","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False"
|
||||
|
||||
|
@@ -21,7 +21,7 @@ do_niif_11050200,11050200,Inventario de Materia Prima,account.data_account_type_
|
||||
do_niif_11050300,11050300,Inventario en Tránsito,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105
|
||||
do_niif_11050400,11050400,Inventario de Materiales y Suministros,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105
|
||||
do_niif_11050500,11050500,Inventario de Combustibles,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105
|
||||
do_niif_11050600,11050600,Bienes Enviados No Facturados,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105
|
||||
do_niif_11050600,11050600,Bienes Enviados No Facturados,account.data_account_type_current_assets,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105
|
||||
do_niif_11060100,11060100,Deterioro Acum. de Inventario en Almacen,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106",l10n_do.account_group_1106
|
||||
do_niif_11060200,11060200,Deterioro Acum. de Materiales y Suministros,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106",l10n_do.account_group_1106
|
||||
do_niif_11060300,11060300,Deterioro Acum. de Combustibles,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106",l10n_do.account_group_1106
|
||||
@@ -99,7 +99,7 @@ do_niif_21020800,21020800,Aporte a Fondo de Pensiones (AFP) por Pagar,account.da
|
||||
do_niif_21020900,21020900,Seguro de Riesgo Laboral (SRL) por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102
|
||||
do_niif_21021000,21021000,INFOTEP por Pagar,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102
|
||||
do_niif_21021100,21021100,Tesorería de la Seguridad Social (TSS) por Pagar,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102
|
||||
do_niif_21021200,21021200,Bienes Recibidos no Facturados,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102
|
||||
do_niif_21021200,21021200,Bienes Recibidos no Facturados,account.data_account_type_non_current_liabilities,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102
|
||||
do_niif_21030101,21030101,ITBIS por Venta Servicios,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301",l10n_do.account_group_210301
|
||||
do_niif_21030102,21030102,ITBIS por Venta Bienes,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301",l10n_do.account_group_210301
|
||||
do_niif_21030103,21030103,ITBIS por Venta Servicios en Nombre de Terceros,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301",l10n_do.account_group_210301
|
||||
|
||||
|
@@ -1,2 +1,2 @@
|
||||
id,property_account_receivable_id/id,property_account_payable_id/id,property_account_expense_categ_id/id,property_account_income_categ_id/id,income_currency_exchange_account_id/id,expense_currency_exchange_account_id/id
|
||||
mn_chart_1,account_template_1201_0201,account_template_3101_0201,account_template_7002_1101,account_template_5101_0201,account_template_5301_0201,account_template_5302_0201
|
||||
id,property_account_receivable_id/id,property_account_payable_id/id,property_account_expense_categ_id/id,property_account_income_categ_id/id,income_currency_exchange_account_id/id,expense_currency_exchange_account_id/id,property_stock_account_input_categ_id/id,property_stock_account_output_categ_id/id,property_stock_valuation_account_id/id
|
||||
mn_chart_1,account_template_1201_0201,account_template_3101_0201,account_template_6101_0101,account_template_5101_0101,account_template_5301_0201,account_template_5302_0201,account_template_1407_0101,account_template_1408_0101,account_template_1401_0101
|
||||
|
||||
|
@@ -10,6 +10,7 @@
|
||||
<field name="transfer_account_code_prefix">1109</field>
|
||||
<field name="code_digits">8</field>
|
||||
<field name="currency_id" ref="base.MNT"/>
|
||||
<field name="use_anglo_saxon" eval="True"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
"ua_psbp_27",27,"Продукція сільськогосподарського виробництва","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_280",280,"Транспортно-заготівельні витрати","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_281",281,"Товари на складі","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_2811",2811,"Товари відвантажені зі складу","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_2812",2812,"Товари отримані на склад","account.data_account_type_current_liabilities",FALSE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_2811",2811,"Товари відвантажені зі складу","account.data_account_type_current_assets",TRUE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_2812",2812,"Товари отримані на склад","account.data_account_type_current_liabilities",TRUE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_282",282,"Товари в торгівлі (за продажною вартістю)","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_283",283,"Товари на комісії","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template"
|
||||
"ua_psbp_284",284,"Тара під товарами","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template"
|
||||
@@ -350,7 +350,7 @@
|
||||
"ua_ias_1101",1101,"Незавершене виробництво","account.data_account_type_current_assets",FALSE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1102",1102,"Готова продукція","account.data_account_type_current_assets",FALSE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1120",1120,"Дебіторська заборгованість за товари та послуги","account.data_account_type_receivable",TRUE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1121",1121,"Відвантажено зі складу без рахунку-фактури","account.data_account_type_current_assets",FALSE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1121",1121,"Відвантажено зі складу без рахунку-фактури","account.data_account_type_current_assets",TRUE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1130",1130,"Аванси партнерам","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1131",1131,"Інші оборотні активи","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1140",1140,"Податковий кредит по ПДВ","account.data_account_type_current_assets",FALSE,"l10n_ua_ias_chart_template"
|
||||
@@ -363,7 +363,7 @@
|
||||
"ua_ias_1170",1170,"Довгострокова дебіторська заборгованість","account.data_account_type_non_current_assets",FALSE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1180",1180,"Відстрочені податкові активи","account.data_account_type_non_current_assets",FALSE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1200",1200,"Кредиторська заборгованість за товари та послуги","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1201",1201,"Отримано на склад без рахунку-фактури","account.data_account_type_current_liabilities",FALSE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1201",1201,"Отримано на склад без рахунку-фактури","account.data_account_type_current_liabilities",TRUE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1202",1202,"Податок на прибуток","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1203",1203,"ПДВ","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template"
|
||||
"ua_ias_1204",1204,"Податкові зобов’язання по ПДВ","account.data_account_type_current_liabilities",FALSE,"l10n_ua_ias_chart_template"
|
||||
|
||||
|
@@ -187,9 +187,7 @@ class Channel(models.Model):
|
||||
|
||||
@api.onchange('public')
|
||||
def _onchange_public(self):
|
||||
if self.public == 'public':
|
||||
self.alias_contact = 'everyone'
|
||||
else:
|
||||
if self.public != 'public' and self.alias_contact == 'everyone':
|
||||
self.alias_contact = 'followers'
|
||||
|
||||
@api.onchange('moderator_ids')
|
||||
|
||||
@@ -437,7 +437,11 @@ var Chatter = Widget.extend({
|
||||
*/
|
||||
_updateAttachmentCounter: function () {
|
||||
var count = this.record.data.message_attachment_count || 0;
|
||||
this.$('.o_chatter_attachment_button_count').html(count);
|
||||
var $element = this.$('.o_chatter_attachment_button_count');
|
||||
if (Number($element.html()) !== count) {
|
||||
this._areAttachmentsLoaded = false;
|
||||
$element.html(count);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -16,8 +16,9 @@ FormView.include({
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if ('message_ids' in this.fieldsInfo[this.viewType]) {
|
||||
this.fieldsInfo[this.viewType].message_attachment_count = {};
|
||||
var fieldsInfo = this.fieldsInfo[this.viewType];
|
||||
if ('message_ids' in fieldsInfo && !('message_attachment_count' in fieldsInfo)) {
|
||||
fieldsInfo.message_attachment_count = {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -242,6 +242,59 @@ QUnit.test('basic rendering', async function (assert) {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('basic rendering: message_attachment_count can be in view standalone', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="message_attachment_count" string="I\'m here"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_form_label').text(), "I'm here",
|
||||
"The field message_attachment_count must be present according to the view's specs");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('basic rendering: message_attachment_count can be in view with chatter', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="message_attachment_count" string="I\'m here"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'<div class="oe_chatter">' +
|
||||
'<field name="message_follower_ids" widget="mail_followers"/>' +
|
||||
'<field name="message_ids" widget="mail_thread"/>' +
|
||||
'<field name="activity_ids" widget="mail_activity"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_form_label').text(), "I'm here",
|
||||
"The field message_attachment_count must be present according to the view's specs");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Activity Done keep feedback on blur', async function (assert) {
|
||||
assert.expect(3);
|
||||
var done = assert.async();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<field name="padding">5</field>
|
||||
<field name="number_next">1</field>
|
||||
<field name="number_increment">1</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="sequence_mrp_unbuild" model="ir.sequence">
|
||||
|
||||
@@ -506,6 +506,9 @@ class MrpProduction(models.Model):
|
||||
factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) / production.bom_id.product_qty
|
||||
boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
|
||||
for bom_line, line_data in lines:
|
||||
if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom' or\
|
||||
bom_line.product_id.type not in ['product', 'consu']:
|
||||
continue
|
||||
moves.append(production._get_move_raw_values(bom_line, line_data))
|
||||
return moves
|
||||
|
||||
@@ -513,10 +516,6 @@ class MrpProduction(models.Model):
|
||||
quantity = line_data['qty']
|
||||
# alt_op needed for the case when you explode phantom bom and all the lines will be consumed in the operation given by the parent bom line
|
||||
alt_op = line_data['parent_line'] and line_data['parent_line'].operation_id.id or False
|
||||
if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom':
|
||||
return self.env['stock.move']
|
||||
if bom_line.product_id.type not in ['product', 'consu']:
|
||||
return self.env['stock.move']
|
||||
source_location = self.location_src_id
|
||||
data = {
|
||||
'sequence': bom_line.sequence,
|
||||
|
||||
@@ -989,3 +989,36 @@ class TestMrpOrder(TestMrpCommon):
|
||||
self.assertEqual(product_produce.raw_workorder_line_ids[0].product_uom_id, unit, 'Should be the product uom so "unit"')
|
||||
self.assertEqual(product_produce.raw_workorder_line_ids[1].qty_to_consume, 1, 'Should be 1 unit since the tracking is serial and quantity 2.')
|
||||
self.assertEqual(product_produce.raw_workorder_line_ids[1].product_uom_id, unit, 'should be the product uom so "unit"')
|
||||
|
||||
def test_product_type_service_1(self):
|
||||
# Create finished product
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Geyser',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
# Create service type product
|
||||
product_raw = self.env['product.product'].create({
|
||||
'name': 'raw Geyser',
|
||||
'type': 'service',
|
||||
})
|
||||
|
||||
# Create bom for finish product
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': finished_product.id,
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})]
|
||||
})
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = finished_product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_uom_id = self.env.ref('uom.product_uom_unit')
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
|
||||
# Check Mo is created or not
|
||||
self.assertTrue(mo, "Mo is created")
|
||||
|
||||
@@ -57,6 +57,9 @@ class ChangeProductionQty(models.TransientModel):
|
||||
boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
|
||||
documents = {}
|
||||
for line, line_data in lines:
|
||||
if line.child_bom_id and line.child_bom_id.type == 'phantom' or\
|
||||
line.product_id.type not in ['product', 'consu']:
|
||||
continue
|
||||
move, old_qty, new_qty = production._update_raw_move(line, line_data)
|
||||
iterate_key = production._get_document_iterate_key(move)
|
||||
if iterate_key:
|
||||
|
||||
@@ -881,7 +881,7 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
values.update({
|
||||
'partner_name': partner.name,
|
||||
'partner_lang': partner.lang or 'en_US',
|
||||
'partner_lang': partner.lang or self.env.user.lang,
|
||||
'partner_email': partner.email,
|
||||
'partner_zip': partner.zip,
|
||||
'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''),
|
||||
|
||||
@@ -1566,6 +1566,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
price_display : this.get_display_price(),
|
||||
price_with_tax : this.get_price_with_tax(),
|
||||
price_without_tax: this.get_price_without_tax(),
|
||||
price_with_tax_before_discount: this.get_price_with_tax_before_discount(),
|
||||
tax: this.get_tax(),
|
||||
product_description: this.get_product().description,
|
||||
product_description_sale: this.get_product().description_sale,
|
||||
@@ -1629,9 +1630,9 @@ exports.Orderline = Backbone.Model.extend({
|
||||
},
|
||||
get_display_price_one: function(){
|
||||
var rounding = this.pos.currency.rounding;
|
||||
var price_unit = this.get_unit_price() * (1.0 - (this.get_discount() / 100.0));
|
||||
var price_unit = this.get_unit_price();
|
||||
if (this.pos.config.iface_tax_included !== 'total') {
|
||||
return round_pr(price_unit, rounding);
|
||||
return round_pr(price_unit * (1.0 - (this.get_discount() / 100.0)), rounding);
|
||||
} else {
|
||||
var product = this.get_product();
|
||||
var taxes_ids = product.taxes_id;
|
||||
@@ -1662,6 +1663,9 @@ exports.Orderline = Backbone.Model.extend({
|
||||
get_price_with_tax: function(){
|
||||
return this.get_all_prices().priceWithTax;
|
||||
},
|
||||
get_price_with_tax_before_discount: function () {
|
||||
return this.get_all_prices().priceWithTaxBeforeDiscount;
|
||||
},
|
||||
get_tax: function(){
|
||||
return this.get_all_prices().tax;
|
||||
},
|
||||
@@ -1907,6 +1911,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
});
|
||||
|
||||
var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding);
|
||||
var all_taxes_before_discount = this.compute_all(product_taxes, this.get_unit_price(), this.get_quantity(), this.pos.currency.rounding);
|
||||
_(all_taxes.taxes).each(function(tax) {
|
||||
taxtotal += tax.amount;
|
||||
taxdetail[tax.id] = tax.amount;
|
||||
@@ -1916,6 +1921,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
"priceWithTax": all_taxes.total_included,
|
||||
"priceWithoutTax": all_taxes.total_excluded,
|
||||
"priceSumTaxVoid": all_taxes.total_void,
|
||||
"priceWithTaxBeforeDiscount": all_taxes_before_discount.total_included,
|
||||
"tax": taxtotal,
|
||||
"taxDetails": taxdetail,
|
||||
};
|
||||
|
||||
@@ -791,8 +791,8 @@
|
||||
<left><t t-esc='line.product_name_wrapped[0]' /></left>
|
||||
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value></right>
|
||||
</line>
|
||||
<t t-call="XmlReceiptWrappedProductNameLines"/>
|
||||
</t>
|
||||
<t t-call="XmlReceiptWrappedProductNameLines"/>
|
||||
<t t-if='!simple'>
|
||||
<line><left><t t-esc='line.product_name_wrapped[0]' /></left></line>
|
||||
<t t-call="XmlReceiptWrappedProductNameLines"/>
|
||||
@@ -812,7 +812,12 @@
|
||||
<line indent='1'>
|
||||
<left>
|
||||
<value t-att-value-decimals='pos.dp["Product Price"]'>
|
||||
<t t-esc="line.price_lst" />
|
||||
<t t-if="pos.config.iface_tax_included === 'total'">
|
||||
<t t-esc="line.price_with_tax_before_discount" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="line.price_lst" />
|
||||
</t>
|
||||
</value>
|
||||
</left>
|
||||
</line>
|
||||
|
||||
@@ -28,7 +28,8 @@ class report_product_pricelist(models.AbstractModel):
|
||||
}
|
||||
|
||||
def _get_quantity(self, data):
|
||||
return sorted([data['form'][key] for key in data['form'] if key.startswith('qty') and data['form'][key]])
|
||||
form = data and data.get('form') or {}
|
||||
return sorted([form[key] for key in form if key.startswith('qty') and form[key]])
|
||||
|
||||
def _get_categories(self, pricelist, products, quantities):
|
||||
categ_data = []
|
||||
|
||||
@@ -46,6 +46,7 @@ class TestTimesheetHolidays(TestCommonTimesheet):
|
||||
'timesheet_generate': True,
|
||||
'timesheet_project_id': self.internal_project.id,
|
||||
'timesheet_task_id': self.internal_task_leaves.id,
|
||||
'validity_start': False,
|
||||
})
|
||||
self.hr_leave_type_no_ts = self.env['hr.leave.type'].create({
|
||||
'name': 'Leave Type without timesheet generation',
|
||||
@@ -53,6 +54,7 @@ class TestTimesheetHolidays(TestCommonTimesheet):
|
||||
'timesheet_generate': False,
|
||||
'timesheet_project_id': False,
|
||||
'timesheet_task_id': False,
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
# HR Officer allocates some leaves to the employee 1
|
||||
|
||||
@@ -201,6 +201,9 @@ class SaleOrder(models.Model):
|
||||
@api.depends('pricelist_id', 'date_order', 'company_id')
|
||||
def _compute_currency_rate(self):
|
||||
for order in self:
|
||||
if not order.company_id:
|
||||
order.currency_rate = order.currency_id.with_context(date=order.date_order).rate or 1.0
|
||||
continue
|
||||
order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order)
|
||||
|
||||
def _compute_access_url(self):
|
||||
|
||||
@@ -1459,3 +1459,48 @@ class TestSaleMrpFlow(common.SavepointCase):
|
||||
move_component_kg = order.picking_ids[0].move_lines - move_component_unit
|
||||
self.assertEquals(move_component_unit.product_uom_qty, 0.5)
|
||||
self.assertEquals(move_component_kg.product_uom_qty, 0.583)
|
||||
|
||||
def test_product_type_service_1(self):
|
||||
route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
|
||||
route_mto = self.warehouse.mto_pull_id.route_id.id
|
||||
self.uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
|
||||
# Create finished product
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Geyser',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, route_mto), (4, route_manufacture)],
|
||||
})
|
||||
|
||||
# Create service type product
|
||||
product_raw = self.env['product.product'].create({
|
||||
'name': 'raw Geyser',
|
||||
'type': 'service',
|
||||
})
|
||||
|
||||
# Create bom for finish product
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': finished_product.id,
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})]
|
||||
})
|
||||
|
||||
# Create sale order
|
||||
sale_form = Form(self.env['sale.order'])
|
||||
sale_form.partner_id = self.env.ref('base.res_partner_1')
|
||||
with sale_form.order_line.new() as line:
|
||||
line.name = finished_product.name
|
||||
line.product_id = finished_product
|
||||
line.product_uom_qty = 1.0
|
||||
line.product_uom = self.uom_unit
|
||||
line.price_unit = 10.0
|
||||
sale_order = sale_form.save()
|
||||
|
||||
sale_order.action_confirm()
|
||||
|
||||
mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
|
||||
|
||||
self.assertTrue(mo, 'Manufacturing order created.')
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * survey_crm
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~11.5\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-09-18 09:49+0000\n"
|
||||
"PO-Revision-Date: 2018-08-24 09:26+0000\n"
|
||||
"Language-Team: Azerbaijani (https://www.transifex.com/odoo/teams/41243/az/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: az\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: survey_crm
|
||||
#: model:ir.model,name:survey_crm.model_survey_mail_compose_message
|
||||
msgid "Email composition wizard for Survey"
|
||||
msgstr ""
|
||||
|
||||
#. module: survey_crm
|
||||
#: model:ir.actions.act_window,name:survey_crm.action_partner_survey_mail_crm
|
||||
msgid "Partner Survey Mailing"
|
||||
msgstr ""
|
||||
@@ -1048,7 +1048,7 @@ class Binary(http.Controller):
|
||||
filename_field=filename_field, download=download, mimetype=mimetype,
|
||||
default_mimetype='image/png', access_token=access_token)
|
||||
|
||||
if status == 301 or (status != 200 and download):
|
||||
if status in [301, 304] or (status != 200 and download):
|
||||
return request.env['ir.http']._response_by_status(status, headers, content)
|
||||
if not content:
|
||||
content = base64.b64encode(self.placeholder(image=placeholder))
|
||||
|
||||
@@ -58,18 +58,18 @@ var pluralize = function (u) {
|
||||
};
|
||||
};
|
||||
var months = [
|
||||
'كانون الثاني يناير',
|
||||
'شباط فبراير',
|
||||
'آذار مارس',
|
||||
'نيسان أبريل',
|
||||
'أيار مايو',
|
||||
'حزيران يونيو',
|
||||
'تموز يوليو',
|
||||
'آب أغسطس',
|
||||
'أيلول سبتمبر',
|
||||
'تشرين الأول أكتوبر',
|
||||
'تشرين الثاني نوفمبر',
|
||||
'كانون الأول ديسمبر'
|
||||
'يناير',
|
||||
'فبراير',
|
||||
'مارس',
|
||||
'أبريل',
|
||||
'مايو',
|
||||
'يونيو',
|
||||
'يوليو',
|
||||
'أغسطس',
|
||||
'سبتمبر',
|
||||
'أكتوبر',
|
||||
'نوفمبر',
|
||||
'ديسمبر'
|
||||
];
|
||||
|
||||
var ar = moment.defineLocale('ar', {
|
||||
|
||||
@@ -12,6 +12,7 @@ var AddNewFavoriteMenu = Widget.extend({
|
||||
events: _.extend({}, Widget.prototype.events, {
|
||||
'click .o_save_favorite': '_onSaveFavoriteClick',
|
||||
'click .o_add_favorite.o_menu_header': '_onMenuHeaderClick',
|
||||
'click input[type="checkbox"]': '_onCheckboxClick',
|
||||
'keyup .o_save_name input': '_onKeyUp',
|
||||
}),
|
||||
/**
|
||||
@@ -107,6 +108,33 @@ var AddNewFavoriteMenu = Widget.extend({
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Additional behavior when a checkbox is clicked upon
|
||||
* Namely, some checkbox are mutually exclusive
|
||||
* this function allows this
|
||||
*
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onCheckboxClick: function (ev) {
|
||||
function mutuallyExclusive (elem) {
|
||||
if (!elem.id) return false;
|
||||
return ['use_by_default', 'share_all_users'].some(function (str) {
|
||||
return elem.id.indexOf(str);
|
||||
});
|
||||
}
|
||||
var $checkboxes = this.$('input[type="checkbox"]');
|
||||
var currentCheckBox = ev.target;
|
||||
|
||||
if (mutuallyExclusive(currentCheckBox)) {
|
||||
for (var i=0; i < $checkboxes.length; i++) {
|
||||
var checkbox = $checkboxes[i];
|
||||
if (checkbox !== currentCheckBox && mutuallyExclusive(checkbox)) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
|
||||
@@ -53,7 +53,6 @@ var PivotController = AbstractController.extend({
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$el.toggleClass('o_enable_linking', this.enableLinking);
|
||||
this.$fieldSelection = this.$('.o_field_selection');
|
||||
core.bus.on('click', this, function () {
|
||||
this.$fieldSelection.empty();
|
||||
|
||||
@@ -283,7 +283,7 @@ var PivotRenderer = AbstractRenderer.extend({
|
||||
$cell = $('<td>')
|
||||
.data('id', rows[i].id)
|
||||
.data('col_id', rows[i].col_ids[Math.floor(j / nbrMeasures)])
|
||||
.toggleClass('o_empty', !value)
|
||||
.toggleClass('o_empty', _.isUndefined(value))
|
||||
.addClass('o_pivot_cell_value text-right');
|
||||
if (value !== undefined) {
|
||||
$cell.append($('<div>', {class: 'o_value'}).html(formatter(value, measure)));
|
||||
|
||||
@@ -34,6 +34,11 @@ var PieChart = Widget.extend({
|
||||
title: node.attrs.title || modifiers.title || modifiers.measure,
|
||||
});
|
||||
|
||||
var pieChartContext = JSON.parse(JSON.stringify(record.context));
|
||||
delete pieChartContext.graph_mode;
|
||||
delete pieChartContext.graph_measure;
|
||||
delete pieChartContext.graph_groupbys;
|
||||
|
||||
this.subViewParams = {
|
||||
modelName: record.model,
|
||||
withControlPanel: false,
|
||||
@@ -41,7 +46,7 @@ var PieChart = Widget.extend({
|
||||
mode: 'pie',
|
||||
};
|
||||
this.subViewParams.searchQuery = {
|
||||
context: record.context,
|
||||
context: pieChartContext,
|
||||
domain: domain,
|
||||
groupBy: [],
|
||||
};
|
||||
|
||||
@@ -87,4 +87,9 @@
|
||||
right: 6px;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
.o_pivot_field_menu .dropdown-item.disabled {
|
||||
color: $text-muted;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,7 +532,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="PivotView" class="o_pivot">
|
||||
<div t-name="PivotView" t-attf-class="o_pivot #{widget.enableLinking ? 'o_enable_linking' : ''}">
|
||||
<div class="o_field_selection"/>
|
||||
</div>
|
||||
<t t-name="PivotView.buttons">
|
||||
|
||||
@@ -418,6 +418,47 @@ QUnit.module('Views', {
|
||||
controlPanel.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Favorites Use by Default and Share are exclusive', async function (assert) {
|
||||
assert.expect(11);
|
||||
var controlPanel = await createControlPanel({
|
||||
model: 'partner',
|
||||
arch: "<search></search>",
|
||||
data: this.data,
|
||||
searchMenuTypes: ['favorite'],
|
||||
});
|
||||
testUtils.dom.click(controlPanel.$('.o_favorites_menu_button'));
|
||||
testUtils.dom.click(controlPanel.$('button.o_add_favorite'));
|
||||
var $checkboxes = controlPanel.$('input[type="checkbox"]');
|
||||
|
||||
assert.strictEqual($checkboxes.length, 2,
|
||||
'2 checkboxes are present')
|
||||
|
||||
assert.notOk($checkboxes[0].checked, 'Start: None of the checkboxes are checked (1)');
|
||||
assert.notOk($checkboxes[1].checked, 'Start: None of the checkboxes are checked (2)');
|
||||
|
||||
testUtils.dom.click($checkboxes.eq(0));
|
||||
assert.ok($checkboxes[0].checked, 'The first checkbox is checked');
|
||||
assert.notOk($checkboxes[1].checked, 'The second checkbox is not checked');
|
||||
|
||||
testUtils.dom.click($checkboxes.eq(1));
|
||||
assert.notOk($checkboxes[0].checked,
|
||||
'Clicking on the second checkbox checks it, and unchecks the first (1)');
|
||||
assert.ok($checkboxes[1].checked,
|
||||
'Clicking on the second checkbox checks it, and unchecks the first (2)');
|
||||
|
||||
testUtils.dom.click($checkboxes.eq(0));
|
||||
assert.ok($checkboxes[0].checked,
|
||||
'Clicking on the first checkbox checks it, and unchecks the second (1)');
|
||||
assert.notOk($checkboxes[1].checked,
|
||||
'Clicking on the first checkbox checks it, and unchecks the second (2)');
|
||||
|
||||
testUtils.dom.click($checkboxes.eq(0));
|
||||
assert.notOk($checkboxes[0].checked, 'End: None of the checkboxes are checked (1)');
|
||||
assert.notOk($checkboxes[1].checked, 'End: None of the checkboxes are checked (2)');
|
||||
|
||||
controlPanel.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('load filter', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(pivot.$el,'o_enable_linking',
|
||||
assert.hasClass(pivot.$('.o_pivot'),'o_enable_linking',
|
||||
"root node should have classname 'o_enable_linking'");
|
||||
assert.strictEqual(pivot.$('td.o_pivot_cell_value:contains(32)').length, 1,
|
||||
"should contain a pivot cell with the sum of all records");
|
||||
@@ -269,7 +269,7 @@ QUnit.module('Views', {
|
||||
}
|
||||
});
|
||||
|
||||
assert.hasClass(pivot.$el,'o_enable_linking',
|
||||
assert.hasClass(pivot.$('.o_pivot'),'o_enable_linking',
|
||||
"root node should have classname 'o_enable_linking'");
|
||||
await testUtils.dom.click(pivot.$('.o_pivot_cell_value:contains(12)')); // should trigger a do_action
|
||||
|
||||
@@ -293,7 +293,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(pivot.$el, 'o_enable_linking',
|
||||
assert.doesNotHaveClass(pivot.$('.o_pivot'), 'o_enable_linking',
|
||||
"root node should not have classname 'o_enable_linking'");
|
||||
assert.containsOnce(pivot, '.o_pivot_cell_value',
|
||||
"should have one cell");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import io
|
||||
import base64
|
||||
|
||||
from PIL import Image
|
||||
|
||||
@@ -42,3 +43,23 @@ class TestImage(HttpCase):
|
||||
response = self.url_open('/web/image/fake/0/image_no_size')
|
||||
image = Image.open(io.BytesIO(response.content))
|
||||
self.assertEqual(image.size, (256, 256))
|
||||
|
||||
def test_02_content_image_Etag_304(self):
|
||||
"""This test makes sure that the 304 response is properly returned if the ETag is properly set"""
|
||||
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'datas': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
||||
'name': 'testEtag.gif',
|
||||
'public': True,
|
||||
'datas_fname': 'testEtag.gif',
|
||||
'mimetype': 'image/gif',
|
||||
})
|
||||
response = self.url_open('/web/image/%s' % attachment.id)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(base64.b64encode(response.content), attachment.datas)
|
||||
|
||||
etag = response.headers.get('ETag')
|
||||
|
||||
response2 = self.url_open('/web/image/%s' % attachment.id, headers={"If-None-Match": etag})
|
||||
self.assertEqual(response2.status_code, 304)
|
||||
self.assertEqual(len(response2.content), 0)
|
||||
|
||||
@@ -68,6 +68,8 @@ Wysiwyg.include({
|
||||
}
|
||||
$(document.body).off('.' + this.id);
|
||||
|
||||
this.$iframe.parents().removeClass('o_wysiwyg_no_transform');
|
||||
|
||||
this.$target.insertBefore(this.$iframe);
|
||||
|
||||
delete window.top[this._onUpdateIframeId];
|
||||
@@ -100,6 +102,10 @@ Wysiwyg.include({
|
||||
}
|
||||
self.$iframe.toggleClass('o_fullscreen');
|
||||
self.$iframe.contents().find('body').toggleClass('o_fullscreen');
|
||||
|
||||
// Hack to avoid a parent of the fullscreen iframe to have a
|
||||
// transform (otherwise the position: fixed won't work)
|
||||
self.$iframe.parents().toggleClass('o_wysiwyg_no_transform');
|
||||
},
|
||||
isFullscreen: function () {
|
||||
if (!self.$iframe) {
|
||||
|
||||
@@ -10,6 +10,10 @@ iframe.wysiwyg_iframe.o_fullscreen {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.o_wysiwyg_no_transform {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
body.o_in_iframe {
|
||||
background-color: $o-view-background-color;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -9,10 +9,12 @@ $-compat-gray-map: (
|
||||
'gray': '700',
|
||||
'gray-light': '600',
|
||||
'gray-lighter': '200',
|
||||
);
|
||||
) !default;
|
||||
@each $old, $new in $-compat-gray-map {
|
||||
@include bg-variant(".bg-#{$old}", gray($new));
|
||||
@include text-emphasis-variant(".text-#{$old}", gray($new));
|
||||
@if not map-has-key($grays, $old) {
|
||||
@include bg-variant(".bg-#{$old}", gray($new));
|
||||
@include text-emphasis-variant(".text-#{$old}", gray($new));
|
||||
}
|
||||
}
|
||||
|
||||
// Restore media ?
|
||||
@@ -210,9 +212,13 @@ $-compat-breakpoints: (
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
.show {
|
||||
display: block !important;
|
||||
}
|
||||
// The 'show' class could be supported if defined here and like that,
|
||||
// unfortunately, BS4 still defines a 'show' class for other purposes which
|
||||
// conflict with this (tab-pane, fade effects, ...). Adding more complex rules
|
||||
// won't solve the problem as they would change css rules priorities.
|
||||
// .show {
|
||||
// display: block !important;
|
||||
// }
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@
|
||||
<template id="iframe_css_assets_edit" name="CSS assets for wysiwyg iframe content for popup">
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-js="false"/>
|
||||
<t t-call-assets="web_editor.assets_wysiwyg" t-js="false"/>
|
||||
<t t-call-assets="website.assets_editor" t-js="false"/>
|
||||
<link rel="stylesheet" href="/website_mass_mailing/static/src/css/website_mass_mailing_popup.css" type="text/css"/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -608,9 +608,12 @@ 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['company_id'] = request.website.company_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
|
||||
new_values['company_id'] = request.website.company_id.id
|
||||
|
||||
lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None
|
||||
if lang:
|
||||
new_values['lang'] = lang
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
odoo.define('website_sale_comparison.comparison', function (require) {
|
||||
'use strict';
|
||||
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var utils = require('web.utils');
|
||||
@@ -30,6 +31,7 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, {
|
||||
this.product_data = {};
|
||||
this.comparelist_product_ids = JSON.parse(utils.get_cookie('comparelist_product_ids') || '[]');
|
||||
this.product_compare_limit = 4;
|
||||
this.guard = new concurrency.Mutex();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -154,6 +156,9 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, {
|
||||
* @private
|
||||
*/
|
||||
_addNewProducts: function (product_id) {
|
||||
this.guard.exec(this._addNewProductsImpl.bind(this, product_id));
|
||||
},
|
||||
_addNewProductsImpl: function (product_id) {
|
||||
var self = this;
|
||||
$('.o_product_feature_panel').addClass('d-md-block');
|
||||
if (!_.contains(self.comparelist_product_ids, product_id)) {
|
||||
@@ -161,8 +166,9 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, {
|
||||
if (_.has(self.product_data, product_id)){
|
||||
self._updateContent();
|
||||
} else {
|
||||
self._loadProducts([product_id]).then(function () {
|
||||
return self._loadProducts([product_id]).then(function () {
|
||||
self._updateContent();
|
||||
self._updateCookie();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -189,6 +195,9 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, {
|
||||
* @private
|
||||
*/
|
||||
_removeFromComparelist: function (e) {
|
||||
this.guard.exec(this._removeFromComparelistImpl.bind(this, e));
|
||||
},
|
||||
_removeFromComparelistImpl: function (e) {
|
||||
this.comparelist_product_ids = _.without(this.comparelist_product_ids, $(e.currentTarget).data('product_product_id'));
|
||||
$(e.currentTarget).parents('.o_product_row').remove();
|
||||
this._updateCookie();
|
||||
|
||||
@@ -379,6 +379,16 @@ class WebsiteSlides(WebsiteProfile):
|
||||
step=self._slides_per_page, url_args=pager_args,
|
||||
scope=page_count if page_count < self._pager_max_pages else self._pager_max_pages)
|
||||
|
||||
query_string = None
|
||||
if category:
|
||||
query_string = "?search_category=%s" % category.id
|
||||
elif tag:
|
||||
query_string = "?search_tag=%s" % tag.id
|
||||
elif slide_type:
|
||||
query_string = "?search_slide_type=%s" % slide_type
|
||||
elif uncategorized:
|
||||
query_string = "?search_uncategorized=1"
|
||||
|
||||
values = {
|
||||
'channel': channel,
|
||||
'active_tab': kw.get('active_tab', 'home'),
|
||||
@@ -387,6 +397,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
'search_tag': tag,
|
||||
'search_slide_type': slide_type,
|
||||
'search_uncategorized': uncategorized,
|
||||
'query_string': query_string,
|
||||
'slide_types': slide_types,
|
||||
'sorting': actual_sorting,
|
||||
'search': search,
|
||||
@@ -506,7 +517,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
|
||||
@http.route('''/slides/slide/<model("slide.slide", "[('website_id', 'in', (False, current_website_id))]"):slide>''', type='http', auth="public", website=True)
|
||||
def slide_view(self, slide, **kwargs):
|
||||
if not slide.channel_id.can_access_from_current_website():
|
||||
if not slide.channel_id.can_access_from_current_website() or not slide.active:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
self._set_viewed_slide(slide)
|
||||
|
||||
@@ -517,6 +528,15 @@ class WebsiteSlides(WebsiteProfile):
|
||||
# sidebar: update with user channel progress
|
||||
values['channel_progress'] = self._get_channel_progress(slide.channel_id, include_quiz=True)
|
||||
|
||||
# Allows to have breadcrumb for the previously used filter
|
||||
values.update({
|
||||
'search_category': slide.category_id.id if kwargs.get('search_category') else None,
|
||||
'search_tag': request.env['slide.tag'].browse(int(kwargs.get('search_tag'))) if kwargs.get('search_tag') else None,
|
||||
'slide_types': dict(request.env['slide.slide']._fields['slide_type']._description_selection(request.env)) if kwargs.get('search_slide_type') else None,
|
||||
'search_slide_type': kwargs.get('search_slide_type'),
|
||||
'search_uncategorized': kwargs.get('search_uncategorized')
|
||||
})
|
||||
|
||||
if kwargs.get('fullscreen') == '1':
|
||||
return request.render("website_slides.slide_fullscreen", values)
|
||||
return request.render("website_slides.slide_main", values)
|
||||
|
||||
@@ -135,7 +135,7 @@ class Slide(models.Model):
|
||||
('video', 'Video'),
|
||||
('quiz', "Quiz")],
|
||||
string='Type', required=True,
|
||||
default='document', readonly=True,
|
||||
default='document',
|
||||
help="The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document).")
|
||||
index_content = fields.Text('Transcript')
|
||||
datas = fields.Binary('Content', attachment=True)
|
||||
@@ -553,11 +553,16 @@ class Slide(models.Model):
|
||||
return result
|
||||
|
||||
def _find_document_data_from_url(self, url):
|
||||
expr = re.compile(r'^.*((youtu.be/)|(v/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*')
|
||||
arg = expr.match(url)
|
||||
document_id = arg and arg.group(7) or False
|
||||
if document_id:
|
||||
return ('youtube', document_id)
|
||||
url_obj = urls.url_parse(url)
|
||||
if url_obj.ascii_host == 'youtu.be':
|
||||
return ('youtube', url_obj.path[1:] if url_obj.path else False)
|
||||
elif url_obj.ascii_host in ('youtube.com', 'www.youtube.com', 'm.youtube.com'):
|
||||
v_query_value = url_obj.decode_query().get('v')
|
||||
if v_query_value:
|
||||
return ('youtube', v_query_value)
|
||||
split_path = url_obj.path.split('/')
|
||||
if len(split_path) >= 3 and split_path[1] in ('v', 'embed'):
|
||||
return ('youtube', split_path[2])
|
||||
|
||||
expr = re.compile(r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)')
|
||||
arg = expr.match(url)
|
||||
|
||||
@@ -5,6 +5,7 @@ odoo.define('website_slides.fullscreen', function (require) {
|
||||
|
||||
var publicWidget = require('web.public.widget');
|
||||
var core = require('web.core');
|
||||
var config = require('web.config');
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var session = require('web.session');
|
||||
@@ -321,6 +322,9 @@ odoo.define('website_slides.fullscreen', function (require) {
|
||||
start: function (){
|
||||
var self = this;
|
||||
this.on('change:slide', this, this._onChangeSlide);
|
||||
if (config.device.size_class > config.device.SIZES.MD) {
|
||||
this._toggleSidebar();
|
||||
}
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self._onChangeSlide(); // trigger manually once DOM ready, since slide content is not rendered server side
|
||||
});
|
||||
@@ -540,12 +544,20 @@ odoo.define('website_slides.fullscreen', function (require) {
|
||||
this.sidebar.goNext();
|
||||
},
|
||||
/**
|
||||
* Show or Hide the sidebar
|
||||
* Called when the sidebar toggle is clicked -> toggles the sidebar visibility.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onClickToggleSidebar: function (ev){
|
||||
ev.preventDefault();
|
||||
this._toggleSidebar();
|
||||
},
|
||||
/**
|
||||
* Toggles sidebar visibility.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toggleSidebar: function () {
|
||||
this.$('.o_wslides_fs_sidebar').toggleClass('o_wslides_fs_sidebar_hidden');
|
||||
this.$('.o_wslides_fs_toggle_sidebar').toggleClass('active');
|
||||
},
|
||||
|
||||
@@ -49,48 +49,12 @@ publicWidget.registry.websiteSlidesShare = publicWidget.Widget.extend({
|
||||
* @param {Object} parent
|
||||
*/
|
||||
start: function (parent) {
|
||||
var self = this;
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
defs.push(new ShareMail(this).attachTo($('.oe_slide_js_share_email')));
|
||||
|
||||
if ($('div#statistic').length) {
|
||||
this.slideURL = $('div#statistic').attr('slide-url');
|
||||
this.socialURLs = {
|
||||
linkedin: 'https://www.linkedin.com/countserv/count/share?url=',
|
||||
twitter: 'https://cdn.api.twitter.com/1/urls/count.json?url=',
|
||||
facebook: 'https://graph.facebook.com/?id=',
|
||||
};
|
||||
}
|
||||
|
||||
_.each(this.socialURLs, function (value, key) {
|
||||
self._updateStatistics(key, self.slideURL);
|
||||
});
|
||||
|
||||
return Promise.all(defs);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} socialSite
|
||||
* @param {string} slide_url
|
||||
*/
|
||||
_updateStatistics: function (socialSite, slideURL) {
|
||||
var self = this;
|
||||
$.ajax({
|
||||
url: self.socialURLs[socialSite] + slideURL,
|
||||
dataType: 'jsonp',
|
||||
success: function (data) {
|
||||
var shareCount = (socialSite === 'facebook' ? data.shares : data.count) || 0;
|
||||
$('#' + socialSite + '-badge').text(shareCount);
|
||||
$('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#' + socialSite + '-badge').text()));
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -100,14 +64,11 @@ publicWidget.registry.websiteSlidesShare = publicWidget.Widget.extend({
|
||||
* @param {Object} ev
|
||||
*/
|
||||
_onSlidesSocialShare: function (ev) {
|
||||
var self = this;
|
||||
ev.preventDefault();
|
||||
var key = $(ev.currentTarget).attr('social-key');
|
||||
var popUpURL = $(ev.currentTarget).attr('href');
|
||||
var popUp = window.open(popUpURL, 'Share Dialog', 'width=626,height=436');
|
||||
$(window).on('focus', function () {
|
||||
if (popUp.closed) {
|
||||
self._updateStatistics(key, self.slide_url);
|
||||
$(window).off('focus');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,3 +5,5 @@ from . import common
|
||||
from . import test_security
|
||||
from . import test_statistics
|
||||
from . import test_karma
|
||||
|
||||
from . import test_from_url
|
||||
@@ -0,0 +1,30 @@
|
||||
import odoo.tests
|
||||
|
||||
|
||||
class TestFromURL(odoo.tests.TransactionCase):
|
||||
def test_youtube_urls(self):
|
||||
urls = {
|
||||
'W0JQcpGLSFw': [
|
||||
'https://youtu.be/W0JQcpGLSFw',
|
||||
'https://www.youtube.com/watch?v=W0JQcpGLSFw',
|
||||
'https://www.youtube.com/watch?v=W0JQcpGLSFw&list=PL1-aSABtP6ACZuppkBqXFgzpNb2nVctZx',
|
||||
],
|
||||
'vmhB-pt7EfA': [ # id starts with v, it is important
|
||||
'https://youtu.be/vmhB-pt7EfA',
|
||||
'https://www.youtube.com/watch?feature=youtu.be&v=vmhB-pt7EfA',
|
||||
'https://www.youtube.com/watch?v=vmhB-pt7EfA&list=PL1-aSABtP6ACZuppkBqXFgzpNb2nVctZx&index=7',
|
||||
],
|
||||
'hlhLv0GN1hA': [
|
||||
'https://www.youtube.com/v/hlhLv0GN1hA',
|
||||
'https://www.youtube.com/embed/hlhLv0GN1hA',
|
||||
'https://m.youtube.com/watch?v=hlhLv0GN1hA'
|
||||
],
|
||||
}
|
||||
|
||||
model = self.env['slide.slide']
|
||||
for id, urls in urls.items():
|
||||
for url in urls:
|
||||
with self.subTest(url=url, id=id):
|
||||
document = model._find_document_data_from_url(url)
|
||||
self.assertEqual(document[0], 'youtube')
|
||||
self.assertEqual(document[1], id)
|
||||
@@ -71,13 +71,13 @@
|
||||
</page>
|
||||
<page string="Document">
|
||||
<group>
|
||||
<field name="slide_type" readonly="1"/>
|
||||
<field name="slide_type"/>
|
||||
<field name="url"
|
||||
attrs="{'required': [('slide_type', 'in', ('video'))], 'readonly': [('slide_type', 'in', ('document', 'video'))], 'invisible': [('slide_type', 'not in', ('document', 'presentation', 'video'))]}" />
|
||||
attrs="{'required': [('slide_type', 'in', ('video'))], 'invisible': [('slide_type', 'not in', ('document', 'presentation', 'video'))]}" />
|
||||
<field name="document_id" readonly="1" attrs="{'invisible': [('document_id', '=', False)]}"/>
|
||||
<field name="mime_type" readonly="1"
|
||||
attrs="{'invisible': [('slide_type', 'not in', ('document', 'presentation', 'infographic', 'webpage'))]}"/>
|
||||
<field name="datas" readonly="1"
|
||||
<field name="datas"
|
||||
attrs="{'invisible': [('slide_type', 'not in', ('document', 'presentation', 'infographic', 'webpage'))]}"/>
|
||||
</group>
|
||||
</page>
|
||||
@@ -93,6 +93,7 @@
|
||||
<field name="slide_views"/>
|
||||
<field name="public_views"/>
|
||||
<field name="total_views"/>
|
||||
<field name="completion_time" widget='float_time'/>
|
||||
</group>
|
||||
</group>
|
||||
<label for="index_content"/>
|
||||
|
||||
@@ -7,30 +7,34 @@
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-between">
|
||||
<!-- Desktop Mode -->
|
||||
<nav aria-label="breadcrumb" class="col d-none d-md-flex">
|
||||
<nav aria-label="breadcrumb" class="col-md-8 d-none d-md-flex">
|
||||
<ol class="breadcrumb bg-transparent mb-0 pl-0 py-0">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/slides">Home</a>
|
||||
</li>
|
||||
<li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_type else '')">
|
||||
<t t-set="breadcrumb_class" t-value="'breadcrumb-item %s' % ('active' if not slide else '')" />
|
||||
<li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_type and not slide else '')">
|
||||
<a t-att-href="'/slides/%s' % slug(channel)"><span t-esc="channel.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_category" t-if="search_category">
|
||||
<li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_category" t-if="search_category">
|
||||
<a t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))"><span t-esc="search_category.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_tag" t-if="search_tag">
|
||||
<li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_tag" t-if="search_tag">
|
||||
<a t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))"><span t-esc="search_tag.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized">
|
||||
<li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized">
|
||||
<a t-att-href="'/slides/%s?search_uncategorized=1' % (slug(channel))">Uncategorized</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type">
|
||||
<li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type">
|
||||
<a t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)"><span t-esc="slide_types[search_slide_type]"/></a>
|
||||
</li>
|
||||
<li t-if="slide" class="breadcrumb-item active">
|
||||
<a t-att-href="'/slides/slide/%s' % slug(slide)"><span t-esc="slide.name"/></a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="col d-none d-md-flex flex-row align-items-center justify-content-end">
|
||||
<div class="col-md-4 d-none d-md-flex flex-row align-items-center justify-content-end">
|
||||
<!-- search -->
|
||||
<form t-attf-action="/slides/all" role="search" method="get">
|
||||
<div class="input-group o_wslides_course_nav_search ml-1 position-relative">
|
||||
@@ -52,21 +56,25 @@
|
||||
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="/slides">Home</a>
|
||||
<t t-set="dropdown_class" t-value="'dropdown-item %s' % ('active' if not slide else '')"/>
|
||||
<a t-att-class="'dropdown-item %s' % ('active' if not search_category and not search_tag and not search_slide_type else '')" t-att-href="'/slides/%s' % slug(channel)">
|
||||
└<span class="ml-1" t-esc="channel.name"/>
|
||||
</a>
|
||||
<a class="dropdown-item active" t-att-aria-current="'page' and search_category" t-if="search_category" t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))">
|
||||
<a t-att-class="dropdown_class" t-att-aria-current="'page' and search_category" t-if="search_category" t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))">
|
||||
└<span class="ml-1" t-esc="search_category.name"/>
|
||||
</a>
|
||||
<a class="dropdown-item active" t-att-aria-current="'page' and search_tag" t-if="search_tag" t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))">
|
||||
<a t-att-class="dropdown_class" t-att-aria-current="'page' and search_tag" t-if="search_tag" t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))">
|
||||
└<span class="ml-1" t-esc="search_tag.name"/>
|
||||
</a>
|
||||
<a class="dropdown-item active" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized" t-att-href="'/slides/%s?search_uncategorized=1' % (slug(channel))">
|
||||
<a t-att-class="dropdown_class" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized" t-att-href="'/slides/%s?search_uncategorized=1' % (slug(channel))">
|
||||
└<span class="ml-1">Uncategorized</span>
|
||||
</a>
|
||||
<a class="dropdown-item active" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type" t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)">
|
||||
<a t-att-class="dropdown_class" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type" t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)">
|
||||
└<span class="ml-1" t-esc="slide_types[search_slide_type]"/>
|
||||
</a>
|
||||
<a t-if="slide" class="dropdown-item active" t-att-href="'/slides/slide/%s' % (slug(slide))">
|
||||
└<span class="ml-1" t-esc="slide.name"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -519,7 +527,7 @@
|
||||
<t t-set="image_url" t-value="'/web/image/slide.slide/%s/image' % slide_promoted.id"/>
|
||||
|
||||
<a t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher"
|
||||
t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="w-100 w-lg-50 flex-shrink-0 rounded">
|
||||
t-attf-href="/slides/slide/#{slug(slide_promoted)}#{query_string}" class="w-100 w-lg-50 flex-shrink-0 rounded">
|
||||
<div t-attf-style="background-image:url(#{image_url}); background-size: cover; background-position:center; padding-bottom: 35%;" class="h-lg-100"/>
|
||||
</a>
|
||||
<div t-else="" class="w-100 w-lg-50 flex-shrink-0 rounded">
|
||||
@@ -528,7 +536,7 @@
|
||||
|
||||
<div class="card-body">
|
||||
<a t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher"
|
||||
t-attf-href="/slides/slide/#{slug(slide_promoted)}"
|
||||
t-attf-href="/slides/slide/#{slug(slide_promoted)}#{query_string}"
|
||||
class="h4 d-block" t-att-title="slide_promoted.name" t-field="slide_promoted.name"/>
|
||||
<h4 t-else="" class="text-muted" t-field="slide_promoted.name"/>
|
||||
|
||||
@@ -597,20 +605,20 @@
|
||||
|
||||
<t t-if="slide.image">
|
||||
<t t-set="lesson_image" t-value="'/web/image/slide.slide/%s/image' % slide.id"/>
|
||||
<a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}" t-title="slide.name">
|
||||
<a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}#{query_string}" t-title="slide.name">
|
||||
<div class="card-img-top border-bottom" t-attf-style="padding-top: 50%; background-image: url(#{lesson_image}); background-size: cover; background-position:center"/>
|
||||
</a>
|
||||
<div t-else="" class="card-img-top border-bottom" t-attf-style="padding-top: 50%; background-image: url(#{lesson_image}); background-size: cover; background-position:center"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}" t-title="slide.name">
|
||||
<a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}#{query_string}" t-title="slide.name">
|
||||
<div class="card-img-top border-bottom o_wslides_gradient" t-attf-style="padding-top: 50%;"/>
|
||||
</a>
|
||||
<div t-else="" class="card-img-top border-bottom o_wslides_gradient" t-attf-style="padding-top: 50%;"/>
|
||||
</t>
|
||||
|
||||
<div class="card-body p-3">
|
||||
<a t-if="can_access" class="card-title h5 mb-2o_wslides_desc_truncate_2" t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/>
|
||||
<a t-if="can_access" class="card-title h5 mb-2o_wslides_desc_truncate_2" t-attf-href="/slides/slide/#{slug(slide)}#{query_string}" t-esc="slide.name"/>
|
||||
<span t-else="" class="card-title h5 mb-2 o_wslides_desc_truncate_2 text-muted" t-esc="slide.name"/>
|
||||
<div class="card-subtitle mb-2 text-muted" t-if="slide.is_preview or (not slide.is_published and user.has_group('website.group_website_publisher'))">
|
||||
<t t-if="slide.is_preview">
|
||||
@@ -624,7 +632,7 @@
|
||||
<div class="o_wslides_desc_truncate_3 font-weight-light oe_no_empty" t-field="slide.description"/>
|
||||
<div t-if="slide.tag_ids" class="mt-2 pt-1 o_wslides_desc_truncate_2">
|
||||
<t t-foreach="slide.tag_ids" t-as="tag">
|
||||
<a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/>
|
||||
<a t-att-href="'/slides/%s/tag/%s' % (slug(slide.channel_id), slug(tag))" class="badge badge-light" t-esc="tag.name"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
<template id='courses_all' name="Odoo All Courses">
|
||||
<t t-set="body_classname" t-value="'o_wslides_body'"/>
|
||||
<t t-call="website.layout">
|
||||
<div class="wrap">
|
||||
<div id="wrap" class="wrap o_wslides_wrap">
|
||||
<section class="s_banner bg-900" style="background-image: url("/website_slides/static/src/img/banner_default_all.svg"); background-size: cover; background-position: 80% 20%">
|
||||
<div class="container py-5">
|
||||
<h1 t-if="search_my" class="display-3 mb-0">My Courses</h1>
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
<div class="row align-items-center my-3">
|
||||
<div class="col-12 col-md order-2 order-md-1 d-flex">
|
||||
<div class="d-flex align-items-center">
|
||||
<h1 class="h5 my-0">
|
||||
<h1 class="h4 my-0">
|
||||
<t t-call="website_slides.slide_icon">
|
||||
<t t-set="icon_class">mr-1</t>
|
||||
</t>
|
||||
@@ -245,36 +245,7 @@
|
||||
<div t-field="slide.html_content"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" t-if="slide.question_ids">
|
||||
<t t-call="website_slides.lesson_content_quiz"/>
|
||||
</div>
|
||||
<div class="row mt-3 mb-3">
|
||||
<div class="col-12 col-md d-flex align-items-start mb-4 mb-md-0" t-if="len(slide.link_ids)">
|
||||
<span t-if="slide.link_ids" class="text-muted font-weight-bold mr-3">External sources</span>
|
||||
<div class="text-muted mr-auto border-left pl-3">
|
||||
<t t-foreach="slide.link_ids" t-as="link">
|
||||
<a t-att-href="link.link" t-esc="link.name"/><br />
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="slide.channel_id.allow_comment and slide.channel_id.channel_type == 'documentation'"
|
||||
class="col-12 col-md d-flex align-items-start justify-content-md-end mb-2 mb-md-0">
|
||||
<span class="text-muted font-weight-bold mr-3">Rating</span>
|
||||
<div class="text-muted border-left pl-3">
|
||||
<div class="o_wslides_js_slide_like mr-2">
|
||||
<span t-att-class="('o_wslides_js_slide_like_up %s') % ('disabled' if not slide.channel_id.can_vote else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
|
||||
<i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i>
|
||||
<span t-esc="slide.likes"/>
|
||||
</span>
|
||||
<span t-att-class="('o_wslides_js_slide_like_down ml-3 %s') % ('disabled' if not slide.channel_id.can_vote else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
|
||||
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
|
||||
<span t-esc="slide.dislikes"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<div class="my-3">
|
||||
<ul class="nav nav-tabs o_wslides_lesson_nav" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a href="#about" aria-controls="about" class="nav-link active" role="tab" data-toggle="tab">
|
||||
@@ -362,27 +333,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-12 col-md">
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th colspan="2" class="border-top-0">Share count</th>
|
||||
</tr>
|
||||
<tr class="border-top-0">
|
||||
<th class="border-top-0"><span id="facebook-badge">0</span></th>
|
||||
<td class="border-top-0 w-100">Facebook</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span id="twitter-badge">0</span></th>
|
||||
<td>Twitter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span id="linkedin-badge"/></th>
|
||||
<td>LinkedIn</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" t-if="slide.website_published" id="share">
|
||||
@@ -409,6 +359,35 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" t-if="slide.question_ids">
|
||||
<t t-call="website_slides.lesson_content_quiz"/>
|
||||
</div>
|
||||
<div class="row mt-3 mb-3">
|
||||
<div class="col-12 col-md d-flex align-items-start mb-4 mb-md-0" t-if="len(slide.link_ids)">
|
||||
<span t-if="slide.link_ids" class="text-muted font-weight-bold mr-3">External sources</span>
|
||||
<div class="text-muted mr-auto border-left pl-3">
|
||||
<t t-foreach="slide.link_ids" t-as="link">
|
||||
<a t-att-href="link.link" t-esc="link.name"/><br />
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="slide.channel_id.allow_comment and slide.channel_id.channel_type == 'documentation'"
|
||||
class="col-12 col-md d-flex align-items-start justify-content-md-end mb-2 mb-md-0">
|
||||
<span class="text-muted font-weight-bold mr-3">Rating</span>
|
||||
<div class="text-muted border-left pl-3">
|
||||
<div class="o_wslides_js_slide_like mr-2">
|
||||
<span t-att-class="('o_wslides_js_slide_like_up %s') % ('disabled' if not slide.channel_id.can_vote else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
|
||||
<i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i>
|
||||
<span t-esc="slide.likes"/>
|
||||
</span>
|
||||
<span t-att-class="('o_wslides_js_slide_like_down ml-3 %s') % ('disabled' if not slide.channel_id.can_vote else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
|
||||
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
|
||||
<span t-esc="slide.dislikes"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Slide sub-tempalte: render a quiz serverside. Should be sync with JS qweb template "slide.slide.quiz" -->
|
||||
@@ -427,7 +406,7 @@
|
||||
t-att-data-has-next="1 if next_slide else 0"
|
||||
t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None">
|
||||
<div t-foreach="slide_questions" t-as="question"
|
||||
t-att-class="'o_wslides_js_lesson_quiz_question mt-3 mb-4 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
|
||||
t-att-class="'o_wslides_js_lesson_quiz_question my-5 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
|
||||
<div class="h4">
|
||||
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question['question']"/>
|
||||
</div>
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
|
||||
<!-- Slide template for the fullscreen mode -->
|
||||
<template id="slide_fullscreen" name="Fullscreen">
|
||||
<t t-set="head">
|
||||
<link rel="canonical" t-att-href="slide.website_url" />
|
||||
</t>
|
||||
<t t-call="website.layout">
|
||||
<div class="o_wslides_fs_main d-flex flex-column font-weight-light">
|
||||
|
||||
<div class="o_wslides_slide_fs_header d-flex flex-shrink-0 text-white">
|
||||
<div class="d-flex">
|
||||
<a class="o_wslides_fs_toggle_sidebar d-flex align-items-center px-3 active" href="#" title="Lessons">
|
||||
<a class="o_wslides_fs_toggle_sidebar d-flex align-items-center px-3" href="#" title="Lessons">
|
||||
<i class="fa fa-bars"/><span class="d-none d-md-inline-block ml-1">Lessons</span>
|
||||
</a>
|
||||
<a class="o_wslides_fs_review d-flex align-items-center px-3" t-att-href="slide.channel_id.website_url" title="Reviews" t-if="slide.channel_id.allow_comment">
|
||||
<a class="o_wslides_fs_review d-flex align-items-center px-3" t-att-href="slide.channel_id.website_url + '?active_tab=review'" title="Reviews" t-if="slide.channel_id.allow_comment">
|
||||
<i class="fa fa-pencil"/><span class="d-none d-md-inline-block ml-1">Write a review</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -20,7 +23,7 @@
|
||||
<i class="fa fa-sign-out d-md-none"/><span class="d-none d-md-inline-block ml-1">Exit Fullscreen</span>
|
||||
</a>
|
||||
<a class="d-flex align-items-center px-3" t-attf-href="/slides/#{slug(slide.channel_id)}">
|
||||
<i class="fa fa-sign-out d-md-none"/><span class="d-none d-md-inline-block ml-1">Back to course</span>
|
||||
<i class="fa fa-home d-md-none"/><span class="d-none d-md-inline-block ml-1">Back to course</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,7 +31,7 @@
|
||||
<div class="o_wslides_fs_container d-flex position-relative overflow-hidden flex-grow-1">
|
||||
<div class="o_wslides_fs_content align-items-stretch justify-content-center d-flex flex-grow-1 order-2"></div>
|
||||
|
||||
<div class="o_wslides_fs_sidebar text-white flex-shrink-0 order-1">
|
||||
<div class="o_wslides_fs_sidebar o_wslides_fs_sidebar_hidden text-white flex-shrink-0 order-1">
|
||||
<div class="o_wslides_fs_sidebar_content d-flex flex-column px-3 pt-3 h-100">
|
||||
<div class="o_wslides_fs_sidebar_header mb-3">
|
||||
<a class="h5 d-block mb-1" t-attf-href="/slides/#{slug(slide.channel_id)}">
|
||||
|
||||
@@ -51,12 +51,6 @@
|
||||
<t t-foreach="course.channel_id.tag_ids" t-as="tag">
|
||||
<a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/>
|
||||
</t>
|
||||
<t t-foreach="course.channel_id.tag_ids" t-as="tag">
|
||||
<a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/>
|
||||
</t>
|
||||
<t t-foreach="course.channel_id.tag_ids" t-as="tag">
|
||||
<a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
Mongolia, 2019-06-14
|
||||
|
||||
BumanIT agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Baskhuu Lodoikhuu baskhuujacara@gmail.com https://github.com/jacara
|
||||
|
||||
List of contributors:
|
||||
|
||||
Baskhuu Lodoikhuu baskhuujacara@gmail.com https://github.com/jacara
|
||||
@@ -0,0 +1,11 @@
|
||||
Czech Republic, 2019-06-03
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Jakub Hrubý jakub@jhruby.cz https://github.com/jakubhruby
|
||||
@@ -678,3 +678,33 @@ state_co_30,co,"Guainía","GUA"
|
||||
state_co_31,co,"Guaviare","GUV"
|
||||
state_co_32,co,"Vaupés","VAU"
|
||||
state_co_33,co,"Vichada","VID"
|
||||
state_mn_01,mn,"Архангай","01"
|
||||
state_mn_02,mn,"Баян-Өлгий","02"
|
||||
state_mn_03,mn,"Баянхонгор","03"
|
||||
state_mn_04,mn,"Булган","04"
|
||||
state_mn_05,mn,"Говь-Алтай","05"
|
||||
state_mn_06,mn,"Дорноговь","06"
|
||||
state_mn_07,mn,"Дорнод","07"
|
||||
state_mn_08,mn,"Дундговь","08"
|
||||
state_mn_09,mn,"Завхан","09"
|
||||
state_mn_10,mn,"Өвөрхангай","10"
|
||||
state_mn_11,mn,"Өмнөговь","11"
|
||||
state_mn_12,mn,"Сүхбаатар","12"
|
||||
state_mn_13,mn,"Сэлэнгэ","13"
|
||||
state_mn_14,mn,"Төв","14"
|
||||
state_mn_15,mn,"Увс","15"
|
||||
state_mn_16,mn,"Ховд","16"
|
||||
state_mn_17,mn,"Хөвсгөл","17"
|
||||
state_mn_18,mn,"Хэнтий","18"
|
||||
state_mn_19,mn,"Дархан-Уул","19"
|
||||
state_mn_20,mn,"Орхон","20"
|
||||
state_mn_23,mn,"УБ - Хан Уул","23"
|
||||
state_mn_24,mn,"УБ - Баянзүрх","24"
|
||||
state_mn_25,mn,"УБ - Сүхбаатар","25"
|
||||
state_mn_26,mn,"УБ - Баянгол","26"
|
||||
state_mn_27,mn,"УБ - Багануур","27"
|
||||
state_mn_28,mn,"УБ - Багахангай","28"
|
||||
state_mn_29,mn,"УБ - Налайх","29"
|
||||
state_mn_32,mn,"Говьсүмбэр","32"
|
||||
state_mn_34,mn,"УБ - Сонгино Хайрхан","34"
|
||||
state_mn_35,mn,"УБ - Чингэлтэй","35"
|
||||
|
||||
|
@@ -222,6 +222,7 @@
|
||||
<field name="symbol">¥</field>
|
||||
<field name="rounding">1.00</field>
|
||||
<field name="active" eval="False"/>
|
||||
<field name="position">before</field>
|
||||
<field name="currency_unit_label">Yen</field>
|
||||
<field name="currency_subunit_label">Cen</field>
|
||||
</record>
|
||||
|
||||
@@ -251,7 +251,10 @@ class IrActionsActWindow(models.Model):
|
||||
existing = self.filtered(lambda rec: rec.id in ids)
|
||||
if len(existing) < len(self):
|
||||
# mark missing records in cache with a failed value
|
||||
exc = MissingError(_("Record does not exist or has been deleted."))
|
||||
exc = MissingError(
|
||||
_("Record does not exist or has been deleted.")
|
||||
+ '\n\n({} {}, {} {})'.format(_('Records:'), (self - existing).ids[:6], _('User:'), self._uid)
|
||||
)
|
||||
for record in (self - existing):
|
||||
record._cache.set_failed(self._fields, exc)
|
||||
return existing
|
||||
|
||||
@@ -353,9 +353,11 @@ class IrHttp(models.AbstractModel):
|
||||
headers = [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')]
|
||||
# cache
|
||||
etag = bool(request) and request.httprequest.headers.get('If-None-Match')
|
||||
status = status or (304 if filehash and etag == filehash else 200)
|
||||
status = status or 200
|
||||
if filehash:
|
||||
headers.append(('ETag', filehash))
|
||||
if etag == filehash and status == 200:
|
||||
status = 304
|
||||
headers.append(('Cache-Control', 'max-age=%s' % (STATIC_CACHE if unique else 0)))
|
||||
# content-disposition default name
|
||||
if download:
|
||||
@@ -398,11 +400,11 @@ class IrHttp(models.AbstractModel):
|
||||
status, content, filename, mimetype, filehash = self._binary_ir_attachment_redirect_content(record, default_mimetype=default_mimetype)
|
||||
if not content:
|
||||
status, content, filename, mimetype, filehash = self._binary_record_content(
|
||||
record, field=field, filename=None, filename_field=filename_field,
|
||||
record, field=field, filename=filename, filename_field=filename_field,
|
||||
default_mimetype='application/octet-stream')
|
||||
|
||||
status, headers, content = self._binary_set_headers(
|
||||
status, content, filename, mimetype, unique, filehash=False, download=download)
|
||||
status, content, filename, mimetype, unique, filehash=filehash, download=download)
|
||||
|
||||
return status, headers, content
|
||||
|
||||
|
||||
@@ -35,4 +35,10 @@ class IrLogging(models.Model):
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
super(IrLogging, self).init()
|
||||
self._cr.execute("ALTER TABLE ir_logging DROP CONSTRAINT IF EXISTS ir_logging_write_uid_fkey")
|
||||
self._cr.execute("select 1 from information_schema.constraint_column_usage where table_name = 'ir_logging' and constraint_name = 'ir_logging_write_uid_fkey'")
|
||||
if self._cr.rowcount:
|
||||
# DROP CONSTRAINT unconditionally takes an ACCESS EXCLUSIVE lock
|
||||
# on the table, even "IF EXISTS" is set and not matching; disabling
|
||||
# the relevant trigger instead acquires SHARE ROW EXCLUSIVE, which
|
||||
# still conflicts with the ROW EXCLUSIVE needed for an insert
|
||||
self._cr.execute("ALTER TABLE ir_logging DROP CONSTRAINT ir_logging_write_uid_fkey")
|
||||
|
||||
@@ -1275,6 +1275,7 @@ class IrModelAccess(models.Model):
|
||||
msg_params['groups_list'] = groups
|
||||
else:
|
||||
msg_tail = _("No group currently allows this operation.")
|
||||
msg_tail += ' - ({} {}, {} {})'.format(_('Operation:'), mode, _('User:'), self._uid)
|
||||
_logger.info('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, self._uid, model)
|
||||
msg = '%s %s' % (msg_heads[mode], msg_tail)
|
||||
raise AccessError(msg % msg_params)
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestACLFeedback(Feedback):
|
||||
self.record.sudo(self.user).write({'val': 10})
|
||||
self.assertEqual(
|
||||
ctx.exception.args[0],
|
||||
"""Sorry, you are not allowed to modify documents of type 'Object For Test Access Right' (test_access_right.some_obj). No group currently allows this operation."""
|
||||
"""Sorry, you are not allowed to modify documents of type 'Object For Test Access Right' (test_access_right.some_obj). No group currently allows this operation. - (Operation: write, User: %d)""" % self.user.id
|
||||
)
|
||||
|
||||
def test_one_group(self):
|
||||
@@ -57,12 +57,12 @@ class TestACLFeedback(Feedback):
|
||||
})
|
||||
self.assertEqual(
|
||||
ctx.exception.args[0],
|
||||
"""Sorry, you are not allowed to create documents of type 'Object For Test Access Right' (test_access_right.some_obj). This operation is allowed for the groups:\n\t- Group 0"""
|
||||
"""Sorry, you are not allowed to create documents of type 'Object For Test Access Right' (test_access_right.some_obj). This operation is allowed for the groups:\n\t- Group 0 - (Operation: create, User: %d)""" % self.user.id
|
||||
)
|
||||
|
||||
def test_two_groups(self):
|
||||
r = self.record.sudo(self.user)
|
||||
expected = """Sorry, you are not allowed to access documents of type 'Object For Test Access Right' (test_access_right.some_obj). This operation is allowed for the groups:\n\t- Group 0\n\t- Group 1"""
|
||||
expected = """Sorry, you are not allowed to access documents of type 'Object For Test Access Right' (test_access_right.some_obj). This operation is allowed for the groups:\n\t- Group 0\n\t- Group 1 - (Operation: read, User: %d)""" % self.user.id
|
||||
with self.assertRaises(AccessError) as ctx:
|
||||
# noinspection PyStatementEffect
|
||||
r.val
|
||||
|
||||
+3
-2
@@ -29,7 +29,7 @@ def log(logger, level, prefix, msg, depth=None):
|
||||
path_prefix = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
class PostgreSQLHandler(logging.Handler):
|
||||
""" PostgreSQL Loggin Handler will store logs in the database, by default
|
||||
""" PostgreSQL Logging Handler will store logs in the database, by default
|
||||
the current database, can be set using --log-db=DBNAME
|
||||
"""
|
||||
def emit(self, record):
|
||||
@@ -39,7 +39,8 @@ class PostgreSQLHandler(logging.Handler):
|
||||
if not dbname:
|
||||
return
|
||||
with tools.ignore(Exception), tools.mute_logger('odoo.sql_db'), sql_db.db_connect(dbname, allow_uri=True).cursor() as cr:
|
||||
cr.autocommit(True)
|
||||
# preclude risks of deadlocks
|
||||
cr.execute("SET LOCAL statement_timeout = 1000")
|
||||
msg = tools.ustr(record.msg)
|
||||
if record.args:
|
||||
msg = msg % record.args
|
||||
|
||||
@@ -487,7 +487,7 @@ class ChromeBrowser():
|
||||
self.chrome_process.wait()
|
||||
if self.user_data_dir and os.path.isdir(self.user_data_dir) and self.user_data_dir != '/':
|
||||
self._logger.info('Removing chrome user profile "%s"', self.user_data_dir)
|
||||
shutil.rmtree(self.user_data_dir)
|
||||
shutil.rmtree(self.user_data_dir, ignore_errors=True)
|
||||
# Restore previous signal handler
|
||||
if self.sigxcpu_handler and os.name == 'posix':
|
||||
signal.signal(signal.SIGXCPU, self.sigxcpu_handler)
|
||||
@@ -828,12 +828,12 @@ class HttpCase(TransactionCase):
|
||||
self.opener = requests.Session()
|
||||
self.opener.cookies['session_id'] = self.session_id
|
||||
|
||||
def url_open(self, url, data=None, timeout=10):
|
||||
def url_open(self, url, data=None, timeout=10, headers=None):
|
||||
if url.startswith('/'):
|
||||
url = "http://%s:%s%s" % (HOST, PORT, url)
|
||||
if data:
|
||||
return self.opener.post(url, data=data, timeout=timeout)
|
||||
return self.opener.get(url, timeout=timeout)
|
||||
return self.opener.post(url, data=data, timeout=timeout, headers=headers)
|
||||
return self.opener.get(url, timeout=timeout, headers=headers)
|
||||
|
||||
def _wait_remaining_requests(self):
|
||||
t0 = int(time.time())
|
||||
|
||||
Reference in New Issue
Block a user