[MERGE] forward port branch saas-11.3 up to 3cdcbce93c
This commit is contained in:
+15
-5
@@ -362,6 +362,11 @@ file_filter = addons/im_livechat/i18n/<lang>.po
|
||||
source_file = addons/im_livechat/i18n/im_livechat.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.im_support]
|
||||
file_filter = addons/im_support/i18n/<lang>.po
|
||||
source_file = addons/im_support/i18n/im_support.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.link_tracker]
|
||||
file_filter = addons/link_tracker/i18n/<lang>.po
|
||||
source_file = addons/link_tracker/i18n/link_tracker.pot
|
||||
@@ -407,6 +412,11 @@ file_filter = addons/mrp/i18n/<lang>.po
|
||||
source_file = addons/mrp/i18n/mrp.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.mrp_bom_cost]
|
||||
file_filter = addons/mrp_bom_cost/i18n/<lang>.po
|
||||
source_file = addons/mrp_bom_cost/i18n/mrp_bom_cost.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.mrp_byproduct]
|
||||
file_filter = addons/mrp_byproduct/i18n/<lang>.po
|
||||
source_file = addons/mrp_byproduct/i18n/mrp_byproduct.pot
|
||||
@@ -552,11 +562,6 @@ file_filter = addons/product_expiry/i18n/<lang>.po
|
||||
source_file = addons/product_expiry/i18n/product_expiry.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.product_extended]
|
||||
file_filter = addons/product_extended/i18n/<lang>.po
|
||||
source_file = addons/product_extended/i18n/product_extended.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.product_margin]
|
||||
file_filter = addons/product_margin/i18n/<lang>.po
|
||||
source_file = addons/product_margin/i18n/product_margin.pot
|
||||
@@ -722,6 +727,11 @@ file_filter = addons/transifex/i18n/<lang>.po
|
||||
source_file = addons/transifex/i18n/transifex.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.uom]
|
||||
file_filter = addons/uom/i18n/<lang>.po
|
||||
source_file = addons/uom/i18n/uom.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.utm]
|
||||
file_filter = addons/utm/i18n/<lang>.po
|
||||
source_file = addons/utm/i18n/utm.pot
|
||||
|
||||
@@ -35,22 +35,6 @@ class ProductTemplate(models.Model):
|
||||
domain=[('deprecated', '=', False)],
|
||||
help="The expense is accounted for when a vendor bill is validated, except in anglo-saxon accounting with perpetual inventory valuation in which case the expense (Cost of Goods Sold account) is recognized at the customer invoice validation. If the field is empty, it uses the one defined in the product category.")
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
#TODO: really? i don't see the reason we'd need that constraint..
|
||||
check = self.ids and 'uom_po_id' in vals
|
||||
if check:
|
||||
self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)])
|
||||
uoms = dict(self._cr.fetchall())
|
||||
res = super(ProductTemplate, self).write(vals)
|
||||
if check:
|
||||
self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)])
|
||||
if dict(self._cr.fetchall()) != uoms:
|
||||
products = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids)])
|
||||
if self.env['account.move.line'].search_count([('product_id', 'in', products.ids)]):
|
||||
raise UserError(_('You can not change the unit of measure of a product that has been already used in an account journal item. If you need to change the unit of measure, you should archive this product and create a new one.'))
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _get_product_accounts(self):
|
||||
return {
|
||||
|
||||
@@ -203,7 +203,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
values['name'] = _('Unknown Partner')
|
||||
values['trust'] = False
|
||||
|
||||
if at_least_one_amount or self._context.get('include_nullified_amount'):
|
||||
if at_least_one_amount or (self._context.get('include_nullified_amount') and lines[partner['partner_id']]):
|
||||
res.append(values)
|
||||
|
||||
return res, total, lines
|
||||
|
||||
@@ -38,7 +38,7 @@ class ReportGeneralLedger(models.AbstractModel):
|
||||
init_wheres.append(init_where_clause.strip())
|
||||
init_filters = " AND ".join(init_wheres)
|
||||
filters = init_filters.replace('account_move_line__move_id', 'm').replace('account_move_line', 'l')
|
||||
sql = ("""SELECT 0 AS lid, l.account_id AS account_id, '' AS ldate, '' AS lcode, NULL AS amount_currency, '' AS lref, 'Initial Balance' AS lname, COALESCE(SUM(l.debit),0.0) AS debit, COALESCE(SUM(l.credit),0.0) AS credit, COALESCE(SUM(l.debit),0) - COALESCE(SUM(l.credit), 0) as balance, '' AS lpartner_id,\
|
||||
sql = ("""SELECT 0 AS lid, l.account_id AS account_id, '' AS ldate, '' AS lcode, 0.0 AS amount_currency, '' AS lref, 'Initial Balance' AS lname, COALESCE(SUM(l.debit),0.0) AS debit, COALESCE(SUM(l.credit),0.0) AS credit, COALESCE(SUM(l.debit),0) - COALESCE(SUM(l.credit), 0) as balance, '' AS lpartner_id,\
|
||||
'' AS move_name, '' AS mmove_id, '' AS currency_code,\
|
||||
NULL AS currency_id,\
|
||||
'' AS invoice_id, '' AS invoice_type, '' AS invoice_number,\
|
||||
|
||||
@@ -760,6 +760,63 @@ class TestReconciliation(AccountingTestCase):
|
||||
self.assertEqual(reversed_bank_line.full_reconcile_id.id, bank_line.full_reconcile_id.id)
|
||||
self.assertEqual(reversed_customer_line.full_reconcile_id.id, customer_line.full_reconcile_id.id)
|
||||
|
||||
def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False):
|
||||
#we create an invoice in given currency
|
||||
invoice = self.account_invoice_model.create({'partner_id': partner_id,
|
||||
'reference_type': 'none',
|
||||
'currency_id': currency_id,
|
||||
'name': type == 'out_invoice' and 'invoice to client' or 'invoice to vendor',
|
||||
'account_id': self.account_rcv.id,
|
||||
'type': type,
|
||||
'date_invoice': time.strftime('%Y') + '-07-01',
|
||||
})
|
||||
self.account_invoice_line_model.create({'product_id': self.product.id,
|
||||
'quantity': 1,
|
||||
'price_unit': invoice_amount,
|
||||
'invoice_id': invoice.id,
|
||||
'name': 'product that cost ' + str(invoice_amount),
|
||||
'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id,
|
||||
})
|
||||
|
||||
#validate invoice
|
||||
invoice.action_invoice_open()
|
||||
return invoice
|
||||
|
||||
def test_aged_report(self):
|
||||
AgedReport = self.env['report.account.report_agedpartnerbalance'].with_context(include_nullified_amount=True)
|
||||
account_type = ['receivable']
|
||||
report_date_to = time.strftime('%Y') + '-07-15'
|
||||
partner = self.env['res.partner'].create({'name': 'AgedPartner'})
|
||||
currency = self.env.user.company_id.currency_id
|
||||
|
||||
invoice = self.create_invoice_partner(currency_id=currency.id, partner_id=partner.id)
|
||||
journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE', 'currency_id': currency.id})
|
||||
|
||||
statement = self.make_payment(invoice, journal, 50)
|
||||
|
||||
# Case 1: The invoice and payment are reconciled: Nothing should appear
|
||||
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30)
|
||||
|
||||
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
|
||||
self.assertEqual(partner_lines, [], 'The aged receivable shouldn\'t have lines at this point')
|
||||
self.assertFalse(partner.id in amls, 'The aged receivable should not have amls either')
|
||||
|
||||
# Case 2: The invoice and payment are not reconciled: we should have one line on the report
|
||||
# and 2 amls
|
||||
invoice.move_id.line_ids.with_context(invoice_id=invoice.id).remove_move_reconcile()
|
||||
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30)
|
||||
|
||||
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
|
||||
self.assertEqual(partner_lines, [{'trust': 'normal', '1': 0.0, '0': 0.0, 'direction': 0.0, 'partner_id': partner.id, '3': 0.0, 'total': 0.0, 'name': 'AgedPartner', '4': 0.0, '2': 0.0}],
|
||||
'We should have a line in the report for the partner')
|
||||
self.assertEqual(len(amls[partner.id]), 2, 'We should have 2 account move lines for the partner')
|
||||
|
||||
positive_line = [line for line in amls[partner.id] if line['line'].balance > 0]
|
||||
negative_line = [line for line in amls[partner.id] if line['line'].balance < 0]
|
||||
|
||||
self.assertEqual(positive_line[0]['amount'], 50.0, 'The amount of the amls should be 50')
|
||||
self.assertEqual(negative_line[0]['amount'], -50.0, 'The amount of the amls should be -50')
|
||||
|
||||
def test_partial_reconcile_currencies_02(self):
|
||||
####
|
||||
# Day 1: Invoice Cust/001 to customer (expressed in USD)
|
||||
|
||||
@@ -162,12 +162,12 @@ class AccountPayment(models.Model):
|
||||
|
||||
# Prepare the stub lines
|
||||
if not credits:
|
||||
stub_lines = [self.make_stub_line(inv) for inv in invoices]
|
||||
stub_lines = [self._check_make_stub_line(inv) for inv in invoices]
|
||||
else:
|
||||
stub_lines = [{'header': True, 'name': "Bills"}]
|
||||
stub_lines += [self.make_stub_line(inv) for inv in debits]
|
||||
stub_lines += [self._check_make_stub_line(inv) for inv in debits]
|
||||
stub_lines += [{'header': True, 'name': "Refunds"}]
|
||||
stub_lines += [self.make_stub_line(inv) for inv in credits]
|
||||
stub_lines += [self._check_make_stub_line(inv) for inv in credits]
|
||||
|
||||
# Crop the stub lines or split them on multiple pages
|
||||
if not multi_stub:
|
||||
|
||||
@@ -17,7 +17,6 @@ class PaymentPortal(http.Controller):
|
||||
:return html: form containing all values related to the acquirer to
|
||||
redirect customers to the acquirer website """
|
||||
success_url = kwargs.get('success_url', '/my')
|
||||
callback_method = kwargs.get('callback_method', '')
|
||||
|
||||
invoice_sudo = request.env['account.invoice'].sudo().browse(invoice_id)
|
||||
if not invoice_sudo:
|
||||
@@ -28,11 +27,10 @@ class PaymentPortal(http.Controller):
|
||||
except:
|
||||
return False
|
||||
|
||||
if request.env.user == request.env.ref('base.public_user'):
|
||||
save_token = False # we avoid to create a token for the public user
|
||||
vals = {
|
||||
'acquirer_id': acquirer_id,
|
||||
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)], limit=1).id,
|
||||
'callback_res_id': invoice_sudo.id,
|
||||
'callback_method': callback_method,
|
||||
}
|
||||
|
||||
if save_token:
|
||||
@@ -55,7 +53,6 @@ class PaymentPortal(http.Controller):
|
||||
""" Use a token to perform a s2s transaction """
|
||||
error_url = kwargs.get('error_url', '/my')
|
||||
success_url = kwargs.get('success_url', '/my')
|
||||
callback_method = kwargs.get('callback_method', '')
|
||||
access_token = kwargs.get('access_token')
|
||||
params = {}
|
||||
if access_token:
|
||||
@@ -67,18 +64,17 @@ class PaymentPortal(http.Controller):
|
||||
return request.redirect(_build_url_w_params(error_url, params))
|
||||
|
||||
try:
|
||||
pm_id = int(pm_id)
|
||||
token = request.env['payment.token'].sudo().browse(int(pm_id))
|
||||
except (ValueError, TypeError):
|
||||
token = False
|
||||
token_owner = invoice_sudo.partner_id if request.env.user == request.env.ref('base.public_user') else request.env.user.partner_id
|
||||
if not token or token.partner_id != token_owner:
|
||||
params['error'] = 'pay_invoice_invalid_token'
|
||||
return request.redirect(_build_url_w_params(error_url, params))
|
||||
|
||||
vals = {
|
||||
'payment_token_id': pm_id,
|
||||
'payment_token_id': token.id,
|
||||
'type': 'server2server',
|
||||
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)],
|
||||
limit=1).id,
|
||||
'callback_res_id': invoice_sudo.id,
|
||||
'callback_method': callback_method,
|
||||
}
|
||||
|
||||
invoice_sudo._create_payment_transaction(vals)
|
||||
|
||||
@@ -9,5 +9,18 @@ class PortalAccount(PortalAccount):
|
||||
|
||||
def _invoice_get_page_view_values(self, invoice, access_token, **kwargs):
|
||||
values = super(PortalAccount, self)._invoice_get_page_view_values(invoice, access_token, **kwargs)
|
||||
values.update(request.env['payment.acquirer']._get_available_payment_input(invoice.partner_id, invoice.company_id))
|
||||
payment_inputs = request.env['payment.acquirer']._get_available_payment_input(company=invoice.company_id)
|
||||
# if not connected (using public user), the method _get_available_payment_input will return public user tokens
|
||||
is_public_user = request.env.ref('base.public_user') == request.env.user
|
||||
if is_public_user:
|
||||
# we should not display payment tokens owned by the public user
|
||||
payment_inputs.pop('pms', None)
|
||||
token_count = request.env['payment.token'].sudo().search_count([('acquirer_id.company_id', '=', invoice.company_id.id),
|
||||
('partner_id', '=', invoice.partner_id.id),
|
||||
])
|
||||
values['existing_token'] = token_count > 0
|
||||
values.update(payment_inputs)
|
||||
# if the current user is connected we set partner_id to his partner otherwise we set it as the invoice partner
|
||||
# we do this to force the creation of payment tokens to the correct partner and avoid token linked to the public user
|
||||
values['partner_id'] = invoice.partner_id if is_public_user else request.env.user.partner_id,
|
||||
return values
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" t-if="existing_token">
|
||||
<div class="col-md-offset-3 col-md-6">
|
||||
<i class="fa fa-info"></i> You have credits card registered, you can log-in to be able to use them.
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -196,5 +196,5 @@ class ResUsers(models.Model):
|
||||
sup = super(ResUsers, self)
|
||||
if not default or not default.get('email'):
|
||||
# avoid sending email to the user we are duplicating
|
||||
sup = super(ResUsers, self.with_context(reset_password=False))
|
||||
sup = super(ResUsers, self.with_context(no_reset_password=True))
|
||||
return sup.copy(default=default)
|
||||
|
||||
@@ -18,6 +18,9 @@ var BarcodeParser = Class.extend({
|
||||
// only when those data have been loaded
|
||||
load: function(){
|
||||
var self = this;
|
||||
if (!this.nomenclature_id) {
|
||||
return;
|
||||
}
|
||||
var id = this.nomenclature_id[0];
|
||||
rpc.query({
|
||||
model: 'barcode.nomenclature',
|
||||
|
||||
@@ -16,7 +16,8 @@ For example, in legal reports, some countries need to split the street into seve
|
||||
with the street name, the house number, and room number.
|
||||
""",
|
||||
'data': [
|
||||
'views/base_address_extended.xml'
|
||||
'views/base_address_extended.xml',
|
||||
'data/base_address_extended_data.xml',
|
||||
],
|
||||
'depends': ['base'],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="base.nl" model="res.country">
|
||||
<field eval="'%(street_name)s %(street_number)s/%(street_number2)s'" name="street_format" />
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -4,7 +4,7 @@ import base64
|
||||
|
||||
import babel.dates
|
||||
import collections
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, MAXYEAR
|
||||
from dateutil import parser
|
||||
from dateutil import rrule
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@@ -633,7 +633,7 @@ class Meeting(models.Model):
|
||||
recurring_date = todate(meeting.recurrent_id_date)
|
||||
rset1.exdate(recurring_date)
|
||||
invalidate = True
|
||||
return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1]
|
||||
return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1 if d.year < MAXYEAR]
|
||||
|
||||
@api.multi
|
||||
def _get_recurrency_end_date(self):
|
||||
@@ -903,8 +903,8 @@ class Meeting(models.Model):
|
||||
startdate = startdate.astimezone(pytz.utc) # Convert to UTC
|
||||
meeting.start = fields.Datetime.to_string(startdate)
|
||||
else:
|
||||
meeting.start = meeting.start_datetime
|
||||
meeting.stop = meeting.stop_datetime
|
||||
meeting.write({'start': meeting.start_datetime,
|
||||
'stop': meeting.stop_datetime})
|
||||
|
||||
@api.depends('byday', 'recurrency', 'final_date', 'rrule_type', 'month_by', 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'day', 'week_list')
|
||||
def _compute_rrule(self):
|
||||
|
||||
@@ -40,8 +40,10 @@ var CalendarNotification = Notification.extend({
|
||||
},
|
||||
|
||||
'click .link2showed': function() {
|
||||
this.destroy(true);
|
||||
this._rpc({route: '/calendar/notify_ack'});
|
||||
var self = this;
|
||||
this._rpc({route: '/calendar/notify_ack'}).always(function() {
|
||||
self.destroy();
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -5,3 +5,11 @@ from . import controllers
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
teams = env['crm.team'].search([('dashboard_graph_model', '=', 'crm.opportunity.report')])
|
||||
teams.update({'dashboard_graph_model': None})
|
||||
|
||||
@@ -54,4 +54,5 @@
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
}
|
||||
|
||||
@@ -450,7 +450,8 @@ class Lead(models.Model):
|
||||
'res_id': self.id,
|
||||
'views': [(form_view.id, 'form'),],
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'inline'
|
||||
'target': 'inline',
|
||||
'context': {'default_type': 'opportunity'}
|
||||
}
|
||||
|
||||
# ----------------------------------------
|
||||
|
||||
@@ -56,8 +56,7 @@
|
||||
<group string="Assign these opportunities to">
|
||||
<field name="team_id"/>
|
||||
<field name="user_ids" widget="many2many_tags" domain="[('share', '=', False)]"/>
|
||||
<!-- Uncomment me in trunk -->
|
||||
<!-- <field name="force_assignation" /> -->
|
||||
<field name="force_assignation"/>
|
||||
</group>
|
||||
<label for="opportunity_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/>
|
||||
<group attrs="{'invisible': [('deduplicate', '=', False)]}">
|
||||
|
||||
@@ -124,3 +124,7 @@ class SaleOrderLine(models.Model):
|
||||
if not line.product_id or not line.product_uom or not line.product_uom_qty:
|
||||
return 0.0
|
||||
line.product_qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id)
|
||||
|
||||
def _is_delivery(self):
|
||||
self.ensure_one()
|
||||
return self.is_delivery
|
||||
|
||||
@@ -41,5 +41,12 @@
|
||||
]
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="event_registration_portal">
|
||||
<field name="name">Event/Registration: Portal</field>
|
||||
<field name="model_id" ref="model_event_registration"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="domain_force">['|', ('email', '=', user.partner_id.email), ('partner_id', '=', user.partner_id.id)]
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -5,7 +5,8 @@ access_event_event_portal,event.event.portal,model_event_event,,1,0,0,0
|
||||
access_event_event_user,event.event.user,model_event_event,event.group_event_user,1,0,0,0
|
||||
access_event_event_manager,event.event.manager,model_event_event,event.group_event_manager,1,1,1,1
|
||||
access_event_registration,event.registration,model_event_registration,event.group_event_user,1,1,1,1
|
||||
access_event_registration_portal,event.registration,model_event_registration,,0,0,0,0
|
||||
access_event_registration_employee,event.registration,model_event_registration,base.group_user,1,0,0,0
|
||||
access_event_registration_portal,event.registration,model_event_registration,base.group_portal,1,0,0,0
|
||||
access_event_mail,event.mail,model_event_mail,event.group_event_user,1,0,0,0
|
||||
access_event_mail_manager,event.mail manager,model_event_mail,event.group_event_manager,1,1,1,1
|
||||
access_event_mail_registration,event.mail.registration,model_event_mail_registration,event.group_event_user,1,0,0,0
|
||||
|
||||
|
@@ -26,6 +26,7 @@ this event.
|
||||
'data/event_sale_data.xml',
|
||||
'report/event_event_templates.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/event_security.xml',
|
||||
'wizard/event_edit_registration.xml',
|
||||
],
|
||||
'demo': ['data/event_demo.xml'],
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<record id="sales_team.group_sale_salesman" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('event.group_event_user'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -11,8 +11,8 @@
|
||||
<page string="Received Badges" attrs="{'invisible': [('user_id', '=', False)]}">
|
||||
<field name="has_badges" invisible="1"/>
|
||||
<button string="Grant a Badge" type="action" name="%(action_reward_wizard)d"/> to reward this employee for a good action
|
||||
<div class="o_view_nocontent" attrs="{'invisible': [('has_badges', '=', True)]}">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
<div class="o_field_nocontent" attrs="{'invisible': [('has_badges', '=', True)]}">
|
||||
<p>
|
||||
Grant this employee his first badge
|
||||
</p><p class="oe_grey">
|
||||
Badges are rewards of good work. Give them to people you believe deserve it.
|
||||
|
||||
@@ -40,12 +40,17 @@ class HolidaysRequest(models.Model):
|
||||
_order = "date_from desc"
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
def _default_domain_holiday_status(self):
|
||||
if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
|
||||
return [('valid', '=', True)]
|
||||
return [('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both'])]
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super(HolidaysRequest, self).default_get(fields_list)
|
||||
|
||||
LeaveType = self.env['hr.leave.type'].with_context(employee_id=defaults.get('employee_id'), default_date_from=defaults.get('date_from', fields.Datetime.now()))
|
||||
lt = LeaveType.search([('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both'])])
|
||||
lt = LeaveType.search(self._default_domain_holiday_status())
|
||||
|
||||
defaults['holiday_status_id'] = lt[0].id if len(lt) > 0 else defaults.get('holiday_status_id')
|
||||
return defaults
|
||||
@@ -77,7 +82,7 @@ class HolidaysRequest(models.Model):
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange')
|
||||
holiday_status_id = fields.Many2one("hr.leave.type", string="Leave Type", required=True, readonly=True,
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]},
|
||||
domain="[('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both'])]")
|
||||
domain=lambda self: self._default_domain_holiday_status())
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', index=True, readonly=True,
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=_default_employee, track_visibility='onchange')
|
||||
manager_id = fields.Many2one('hr.employee', string='Manager', readonly=True)
|
||||
|
||||
@@ -40,12 +40,17 @@ class HolidaysAllocation(models.Model):
|
||||
_description = "Leaves Allocation"
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
def _default_domain_holiday_status_id(self):
|
||||
if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
|
||||
return [('valid', '=', True), ('limit', '=', False)]
|
||||
return [('valid', '=', True), ('employee_applicability', 'in', ['allocation', 'both']), ('limit', '=', False)]
|
||||
|
||||
def _default_employee(self):
|
||||
return self.env.context.get('default_employee_id') or self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
|
||||
|
||||
def _default_holiday_status_id(self):
|
||||
LeaveType = self.env['hr.leave.type'].with_context(employee_id=self._default_employee().id)
|
||||
lt = LeaveType.search([('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both']), ('limit', '=', False)])
|
||||
lt = LeaveType.search(self._default_domain_holiday_status_id())
|
||||
return lt[:1]
|
||||
|
||||
name = fields.Char('Description')
|
||||
@@ -67,7 +72,7 @@ class HolidaysAllocation(models.Model):
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange')
|
||||
holiday_status_id = fields.Many2one("hr.leave.type", string="Leave Type", required=True, readonly=True,
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]},
|
||||
domain="[('valid', '=', True), ('employee_applicability', 'in', ['allocation', 'both']), ('limit', '=', False)]", default=_default_holiday_status_id)
|
||||
domain=lambda self: self._default_domain_holiday_status_id(), default=_default_holiday_status_id)
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', index=True, readonly=True,
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=_default_employee, track_visibility='onchange')
|
||||
notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
|
||||
|
||||
@@ -86,6 +86,11 @@ class HrSalaryRuleCategory(models.Model):
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
|
||||
@api.constrains('parent_id')
|
||||
def _check_parent_id(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rule Category.'))
|
||||
|
||||
|
||||
class HrSalaryRule(models.Model):
|
||||
_name = 'hr.salary.rule'
|
||||
@@ -166,6 +171,11 @@ class HrSalaryRule(models.Model):
|
||||
input_ids = fields.One2many('hr.rule.input', 'input_id', string='Inputs', copy=True)
|
||||
note = fields.Text(string='Description')
|
||||
|
||||
@api.constrains('parent_rule_id')
|
||||
def _check_parent_rule_id(self):
|
||||
if not self._check_recursion(parent='parent_rule_id'):
|
||||
raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rules.'))
|
||||
|
||||
@api.multi
|
||||
def _recursive_search_of_rules(self):
|
||||
"""
|
||||
|
||||
@@ -16,7 +16,7 @@ class LivechatController(http.Controller):
|
||||
# _get_asset return the bundle html code (script and link list) but we want to use the attachment content
|
||||
xmlid = 'im_livechat.external_lib'
|
||||
files, remains = request.env["ir.qweb"]._get_asset_content(xmlid, options=request.context)
|
||||
asset = AssetsBundle(xmlid, files, remains)
|
||||
asset = AssetsBundle(xmlid, files)
|
||||
|
||||
mock_attachment = getattr(asset, ext)()
|
||||
if isinstance(mock_attachment, list): # suppose that CSS asset will not required to be split in pages
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class ImSupport(http.Controller):
|
||||
|
||||
@http.route('/im_support/tests', type='http', auth="user")
|
||||
def test_suite(self, mod=None, **kwargs):
|
||||
return request.render('im_support.support_qunit_suite')
|
||||
@@ -42,9 +42,9 @@ Discuss.include({
|
||||
if (!channel.available) {
|
||||
self.basicComposer.do_hide();
|
||||
}
|
||||
$buttonAddAttachment.hide();
|
||||
$buttonAddAttachment.toggleClass('o_hidden', true);
|
||||
} else {
|
||||
$buttonAddAttachment.show();
|
||||
$buttonAddAttachment.toggleClass('o_hidden', false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
odoo.define('im_support.test_utils', function (require) {
|
||||
"use strict";
|
||||
|
||||
var supportSession = require('im_support.SupportSession');
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
|
||||
/**
|
||||
* Extended version of addMockEnvironment that mocks RPCs done to the Support
|
||||
* server (CORS), and enables a longpolling to the Support server, optionally.
|
||||
*
|
||||
* @param {Widget} widget
|
||||
* @param {Object} params
|
||||
* @param {function} [params.mockSupportRPC]
|
||||
* @param {boolean} [params.enableSupportPoll=false]
|
||||
*/
|
||||
function addMockSupportEnvironment(widget, params) {
|
||||
// mock CORS RPCs
|
||||
var originalRPC = supportSession.rpc;
|
||||
var defaultMockSupportRPC = function (route, args) {
|
||||
if (route === '/odoo_im_support/get_support_channel') {
|
||||
return $.when({
|
||||
available: true,
|
||||
channel_type: 'livechat',
|
||||
public: 'private',
|
||||
uuid: args.channel_uuid,
|
||||
});
|
||||
}
|
||||
if (route === '/odoo_im_support/fetch_messages') { // fetching history
|
||||
return $.when([]);
|
||||
}
|
||||
if (route === '/odoo_im_support/chat_post') {
|
||||
return $.when();
|
||||
}
|
||||
if (route === '/longpolling/support_poll') {
|
||||
return $.Deferred();
|
||||
}
|
||||
};
|
||||
supportSession.rpc = function (route, args) {
|
||||
var result;
|
||||
if (params.mockSupportRPC) {
|
||||
var _super = this._super;
|
||||
this._super = defaultMockSupportRPC.bind(this, route, args);
|
||||
result = params.mockSupportRPC.apply(this, arguments);
|
||||
this._super = _super;
|
||||
} else {
|
||||
result = defaultMockSupportRPC.apply(this, arguments);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
testUtils.addMockEnvironment(widget, params);
|
||||
var widgetDestroy = widget.destroy;
|
||||
widget.destroy = function () {
|
||||
supportSession.rpc = originalRPC;
|
||||
widgetDestroy.call(this);
|
||||
};
|
||||
|
||||
// intercepts the getItem call to localStorage service for key
|
||||
// 'im_support.poll_timeout', to simulate a pending longpolling connection to
|
||||
// the Support server, such that the initSupport initiates a poll connection
|
||||
if (params.enableSupportPoll) {
|
||||
testUtils.intercept(widget, 'call_service', function (ev) {
|
||||
if (ev.data.service === 'local_storage') {
|
||||
if (ev.data.method === 'getItem' && ev.data.args[0] === 'im_support.poll_timeout') {
|
||||
ev.data.callback(Date.now() + (60 * 1000));
|
||||
// hack to prevent the default mock to handle the request and erase the above value
|
||||
ev.data.callback = function () {};
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
// manually call initSupport (which is supposed to be called at webclient startup)
|
||||
widget.call('chat_manager', 'initSupport');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
addMockSupportEnvironment: addMockSupportEnvironment,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
odoo.define('im_support.systray_no_support_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The purpose of these tests is to ensure that im_support doesn't have an impact
|
||||
* on the webclient when the support is not available.
|
||||
*/
|
||||
|
||||
var ChatManager = require('mail.ChatManager');
|
||||
var systray = require('mail.systray');
|
||||
var mailTestUtils = require('mail.testUtils');
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createBusService = mailTestUtils.createBusService;
|
||||
|
||||
QUnit.module('im_support', {}, function () {
|
||||
|
||||
QUnit.module('systray', {
|
||||
beforeEach: function () {
|
||||
this.services = [ChatManager, createBusService()];
|
||||
this.data = {
|
||||
'mail.message': {
|
||||
fields: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('messaging menu does not display the Support channel if not available', function (assert) {
|
||||
// the Support channel should only be displayed if a support_token and a support_origin are
|
||||
// specified in the session, which is not the case for this test
|
||||
assert.expect(1);
|
||||
|
||||
var messagingMenu = new systray.MessagingMenu();
|
||||
testUtils.addMockEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
data: this.data,
|
||||
});
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
messagingMenu.$('.dropdown-toggle').click();
|
||||
assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
|
||||
0, "should not display the Support channel");
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,342 @@
|
||||
odoo.define('im_support.systray_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var supportBus = require('im_support.SupportBus');
|
||||
var imSupportTestUtils = require('im_support.test_utils');
|
||||
|
||||
var ChatManager = require('mail.ChatManager');
|
||||
var ChatWindowManager = require('mail.ChatWindowManager');
|
||||
var systray = require('mail.systray');
|
||||
var mailTestUtils = require('mail.testUtils');
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var addMockSupportEnvironment = imSupportTestUtils.addMockSupportEnvironment;
|
||||
var createBusService = mailTestUtils.createBusService;
|
||||
|
||||
QUnit.module('im_support', {}, function () {
|
||||
|
||||
QUnit.module('systray', {
|
||||
beforeEach: function () {
|
||||
this.services = [ChatManager, ChatWindowManager, createBusService()];
|
||||
this.supportParams = {
|
||||
db_uuid: 'some_uuid',
|
||||
support_token: 'ABCDEFGHIJ',
|
||||
support_origin: 'something.com',
|
||||
};
|
||||
this.data = {
|
||||
'mail.message': {
|
||||
fields: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('messaging menu displays the Support channel', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var messagingMenu = new systray.MessagingMenu();
|
||||
addMockSupportEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
session: this.supportParams,
|
||||
data: this.data,
|
||||
});
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
messagingMenu.$('.dropdown-toggle').click();
|
||||
assert.strictEqual(messagingMenu.$('.o_mail_navbar_dropdown_bottom .o_mail_channel_preview[data-channel_id=SupportChannel]').length,
|
||||
1, "should display the Support channel");
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('clicking on Support channel: channel not available', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var messagingMenu = new systray.MessagingMenu();
|
||||
addMockSupportEnvironment(messagingMenu, {
|
||||
mockRPC: function (route, args) {
|
||||
if (!_.str.contains(route, '/static/')) {
|
||||
assert.step(args.method || route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
mockSupportRPC: function (route) {
|
||||
assert.step('cors: ' + route);
|
||||
if (route === '/odoo_im_support/get_support_channel') {
|
||||
return this._super.apply(this, arguments).then(function (supportChannel) {
|
||||
supportChannel.available = false;
|
||||
return supportChannel;
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
services: this.services,
|
||||
session: this.supportParams,
|
||||
data: this.data,
|
||||
});
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
messagingMenu.$('.dropdown-toggle').click();
|
||||
assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
|
||||
1, "should display the Support channel");
|
||||
|
||||
messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').click();
|
||||
assert.strictEqual($('.o_chat_window').length, 1,
|
||||
"should have open a chat window");
|
||||
assert.strictEqual($('.o_chat_window .o_chat_title').text().trim(), 'Support (offline)',
|
||||
"should display the offline status in the header");
|
||||
assert.strictEqual($('.o_chat_window .o_composer_input').length, 0,
|
||||
"should have no composer");
|
||||
|
||||
assert.verifySteps([
|
||||
'/mail/init_messaging',
|
||||
'message_fetch',
|
||||
'cors: /odoo_im_support/get_support_channel',
|
||||
'cors: /odoo_im_support/fetch_messages',
|
||||
]);
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('clicking on Support channel: channel available', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var messagingMenu = new systray.MessagingMenu();
|
||||
addMockSupportEnvironment(messagingMenu, {
|
||||
mockRPC: function (route, args) {
|
||||
if (!_.str.contains(route, '/static/')) {
|
||||
assert.step(args.method || route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
mockSupportRPC: function (route) {
|
||||
if (route.split('/')[1] === 'odoo_im_support') {
|
||||
assert.step('cors: ' + route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
services: this.services,
|
||||
session: this.supportParams,
|
||||
data: this.data,
|
||||
});
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
messagingMenu.$('.dropdown-toggle').click();
|
||||
assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
|
||||
1, "should display the Support channel");
|
||||
|
||||
messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').click();
|
||||
assert.strictEqual($('.o_chat_window').length, 1,
|
||||
"should have open a chat window");
|
||||
assert.strictEqual($('.o_chat_window .o_chat_title').text().trim(), 'Support',
|
||||
"should display the correct channel title");
|
||||
assert.strictEqual($('.o_chat_window .o_composer_input').length, 1,
|
||||
"should have a composer");
|
||||
|
||||
assert.verifySteps([
|
||||
'/mail/init_messaging',
|
||||
'message_fetch',
|
||||
'cors: /odoo_im_support/get_support_channel',
|
||||
'cors: /odoo_im_support/fetch_messages',
|
||||
]);
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('post messages in Support channel', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var messagingMenu = new systray.MessagingMenu();
|
||||
addMockSupportEnvironment(messagingMenu, {
|
||||
mockRPC: function (route, args) {
|
||||
if (!_.str.contains(route, '/static/')) {
|
||||
assert.step(args.method || route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
mockSupportRPC: function (route) {
|
||||
if (route.split('/')[1] === 'odoo_im_support') {
|
||||
assert.step('cors: ' + route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
services: this.services,
|
||||
session: this.supportParams,
|
||||
data: this.data,
|
||||
});
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
messagingMenu.$('.dropdown-toggle').click();
|
||||
assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
|
||||
1, "should display the Support channel");
|
||||
|
||||
messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').click();
|
||||
assert.strictEqual($('.o_chat_window .o_composer_input').length, 1,
|
||||
"should have a composer");
|
||||
|
||||
$('.o_chat_window .o_composer_input .o_input')
|
||||
.val('some message')
|
||||
.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER}));
|
||||
|
||||
assert.verifySteps([
|
||||
'/mail/init_messaging',
|
||||
'message_fetch',
|
||||
'cors: /odoo_im_support/get_support_channel',
|
||||
'cors: /odoo_im_support/fetch_messages',
|
||||
'cors: /odoo_im_support/chat_post',
|
||||
]);
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('fold Support channel', function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
var messagingMenu = new systray.MessagingMenu();
|
||||
addMockSupportEnvironment(messagingMenu, {
|
||||
mockRPC: function (route, args) {
|
||||
if (!_.str.contains(route, '/static/')) {
|
||||
assert.step(args.method || route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
mockSupportRPC: function (route) {
|
||||
if (route.split('/')[1] === 'odoo_im_support') {
|
||||
assert.step('cors: ' + route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
services: this.services,
|
||||
session: this.supportParams,
|
||||
data: this.data,
|
||||
});
|
||||
testUtils.intercept(messagingMenu, 'call_service', function (ev) {
|
||||
if (ev.data.service === 'local_storage') {
|
||||
assert.step('LocalStorage: ' + ev.data.method + ' ' + ev.data.args);
|
||||
}
|
||||
}, true);
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
messagingMenu.$('.dropdown-toggle').click();
|
||||
assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
|
||||
1, "should display the Support channel");
|
||||
|
||||
messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').click();
|
||||
assert.strictEqual($('.o_chat_window').length, 1,
|
||||
"should have open a chat window");
|
||||
|
||||
// fold, re-open and close channel
|
||||
$('.o_chat_window .o_chat_title').click();
|
||||
$('.o_chat_window .o_chat_title').click();
|
||||
$('.o_chat_window .o_chat_window_close').click();
|
||||
|
||||
assert.verifySteps([
|
||||
'/mail/init_messaging',
|
||||
'message_fetch',
|
||||
'cors: /odoo_im_support/get_support_channel',
|
||||
'LocalStorage: setItem im_support.channel_state,open',
|
||||
'cors: /odoo_im_support/fetch_messages',
|
||||
'LocalStorage: setItem im_support.channel_state,folded',
|
||||
'LocalStorage: setItem im_support.channel_state,open',
|
||||
'LocalStorage: setItem im_support.channel_state,closed',
|
||||
]);
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('restore Support channel if necessary', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var messagingMenu = new systray.MessagingMenu();
|
||||
addMockSupportEnvironment(messagingMenu, {
|
||||
enableSupportPoll: true,
|
||||
mockRPC: function (route, args) {
|
||||
if (!_.str.contains(route, '/static/')) {
|
||||
assert.step(args.method || route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
mockSupportRPC: function (route) {
|
||||
if (route.split('/')[1] === 'odoo_im_support') {
|
||||
assert.step('cors: ' + route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
services: this.services,
|
||||
session: this.supportParams,
|
||||
data: this.data,
|
||||
});
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
assert.strictEqual($('.o_chat_window').length, 1,
|
||||
"should have open a chat window");
|
||||
|
||||
assert.verifySteps([
|
||||
'/mail/init_messaging',
|
||||
'cors: /odoo_im_support/get_support_channel',
|
||||
'cors: /odoo_im_support/fetch_messages',
|
||||
]);
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('receive messages in the Support channel', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var supportChannelID;
|
||||
|
||||
var messagingMenu = new systray.MessagingMenu();
|
||||
addMockSupportEnvironment(messagingMenu, {
|
||||
enableSupportPoll: true,
|
||||
mockRPC: function (route, args) {
|
||||
if (!_.str.contains(route, '/static/')) {
|
||||
assert.step(args.method || route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
mockSupportRPC: function (route) {
|
||||
if (route.split('/')[1] === 'odoo_im_support') {
|
||||
assert.step('cors: ' + route);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
services: this.services,
|
||||
session: this.supportParams,
|
||||
data: this.data,
|
||||
});
|
||||
messagingMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
assert.strictEqual($('.o_chat_window').length, 1,
|
||||
"should have open a chat window");
|
||||
assert.strictEqual($('.o_chat_window .o_thread_message').length, 0,
|
||||
"there should be no message in the thread");
|
||||
|
||||
// simulate an incoming message on the supportBus
|
||||
var data = {
|
||||
author_id: [42, 'An operator'],
|
||||
body: 'A message',
|
||||
channel_ids: [supportChannelID],
|
||||
};
|
||||
var notification = [[false, 'mail.channel'], data];
|
||||
supportBus.trigger('notification', [notification]);
|
||||
|
||||
assert.strictEqual($('.o_chat_window .o_thread_message').length, 1,
|
||||
"there should be a new message in the thread");
|
||||
assert.strictEqual($('.o_chat_window .o_thread_message .o_thread_author ').text().trim(),
|
||||
'An operator', "should correctly display the author");
|
||||
assert.strictEqual($('.o_chat_window .o_thread_message .o_thread_message_content ').text().trim(),
|
||||
'A message', "message is correct");
|
||||
|
||||
assert.verifySteps([
|
||||
'/mail/init_messaging',
|
||||
'cors: /odoo_im_support/get_support_channel',
|
||||
'cors: /odoo_im_support/fetch_messages',
|
||||
]);
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_js
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class IMSupportSuite(odoo.tests.HttpCase):
|
||||
|
||||
def test_im_support_js(self):
|
||||
self.phantom_js('/im_support/tests?mod=web&failfast', "", "", login='admin', timeout=180)
|
||||
@@ -12,5 +12,34 @@
|
||||
<script type="text/javascript" src="/im_support/static/src/js/systray.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" name="im_support_tests" inherit_id="web.qunit_suite">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<script type="text/javascript" src="/im_support/static/tests/systray_no_support_tests.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="im_support.support_qunit_suite">
|
||||
<t t-call="web.layout">
|
||||
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
|
||||
<t t-set="title">IM Support Tests</t>
|
||||
<t t-set="head">
|
||||
<script type="text/javascript">
|
||||
odoo.session_info = {
|
||||
support_token: 'ABCDEFGHIJ',
|
||||
support_origin: 'https://something.com'
|
||||
};
|
||||
</script>
|
||||
<t t-call="web.js_tests_assets"/>
|
||||
|
||||
<script type="text/javascript" src="/im_support/static/tests/helpers/test_utils.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/im_support/static/tests/systray_tests.js"></script>
|
||||
</t>
|
||||
|
||||
<div id="qunit"/>
|
||||
<div id="qunit-fixture"/>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -6,6 +6,7 @@ import re
|
||||
|
||||
from operator import itemgetter
|
||||
from email.utils import formataddr
|
||||
from openerp.http import request
|
||||
|
||||
from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
@@ -334,11 +335,12 @@ class Message(models.Model):
|
||||
|
||||
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
|
||||
attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype'])
|
||||
safari = request and request.httprequest.user_agent.browser == 'safari'
|
||||
attachments_tree = dict((attachment['id'], {
|
||||
'id': attachment['id'],
|
||||
'filename': attachment['datas_fname'],
|
||||
'name': attachment['name'],
|
||||
'mimetype': attachment['mimetype'],
|
||||
'mimetype': 'application/octet-stream' if safari and 'video' in attachment['mimetype'] else attachment['mimetype'],
|
||||
}) for attachment in attachments_data)
|
||||
|
||||
# 3. Tracking values
|
||||
|
||||
@@ -24,6 +24,7 @@ var QWeb = core.qweb;
|
||||
var Chatter = Widget.extend({
|
||||
template: 'mail.Chatter',
|
||||
custom_events: {
|
||||
discard_record_changes: '_onDiscardRecordChanges',
|
||||
reload_mail_fields: '_onReloadMailFields',
|
||||
},
|
||||
events: {
|
||||
@@ -154,6 +155,37 @@ var Chatter = Widget.extend({
|
||||
this.composer.clear_composer();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Discard changes on the record.
|
||||
*
|
||||
* @private
|
||||
* @returns {$.Deferred} resolved if successfully discarding changes on
|
||||
* the record, rejected otherwise
|
||||
*/
|
||||
_discardChanges: function () {
|
||||
var def = $.Deferred();
|
||||
this.trigger_up('discard_changes', {
|
||||
recordID: this.record.id,
|
||||
onSuccess: def.resolve.bind(def),
|
||||
onFailure: def.reject.bind(def),
|
||||
});
|
||||
return def;
|
||||
},
|
||||
/**
|
||||
* Discard changes on the record if the message will reload the record
|
||||
* after posting it
|
||||
*
|
||||
* @private
|
||||
* @param {Object} message
|
||||
* @return {$.Deferred} resolved if no reload or proceed to discard the
|
||||
* changes on the record, rejected otherwise
|
||||
*/
|
||||
_discardOnReload: function (message) {
|
||||
if (this._reloadAfterPost(message)) {
|
||||
return this._discardChanges();
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -199,11 +231,13 @@ var Chatter = Widget.extend({
|
||||
self.composer.focus();
|
||||
}
|
||||
self.composer.on('post_message', self, function (message) {
|
||||
self.fields.thread.postMessage(message).then(function () {
|
||||
self._closeComposer(true);
|
||||
if (self.postRefresh === 'always' || (self.postRefresh === 'recipients' && message.partner_ids.length)) {
|
||||
self.trigger_up('reload');
|
||||
}
|
||||
self._discardOnReload(message).then(function () {
|
||||
self.fields.thread.postMessage(message).then(function () {
|
||||
self._closeComposer(true);
|
||||
if (self._reloadAfterPost(message)) {
|
||||
self.trigger_up('reload');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
self.composer.on('need_refresh', self, self.trigger_up.bind(self, 'reload'));
|
||||
@@ -215,6 +249,24 @@ var Chatter = Widget.extend({
|
||||
self.$('.o_chatter_button_log_note').toggleClass('o_active', self.composer.options.is_log);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* State if the record will be reloaded after posting a message.
|
||||
* Useful to warn the user of unsaved changes if the record is dirty.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} message
|
||||
* @param {Array} [message.partner_ids] list of recipients of a message
|
||||
* @return {boolean} true if record will be reloaded after posting the
|
||||
* message, false otherwise
|
||||
*/
|
||||
_reloadAfterPost: function (message) {
|
||||
return this.postRefresh === 'always' ||
|
||||
(
|
||||
this.postRefresh === 'recipients' &&
|
||||
message.partner_ids &&
|
||||
message.partner_ids.length
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Deferred} def
|
||||
@@ -314,8 +366,16 @@ var Chatter = Widget.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Discard changes on the record.
|
||||
* This is notified by the composer, when opening the full-composer.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {function} ev.data.proceed callback to tell to proceed
|
||||
*/
|
||||
_onDiscardRecordChanges: function (ev) {
|
||||
this._discardChanges().then(ev.data.proceed);
|
||||
},
|
||||
_onOpenComposerMessage: function () {
|
||||
var self = this;
|
||||
if (!this.suggested_partners_def) {
|
||||
|
||||
@@ -202,14 +202,23 @@ var ChatterComposer = composer.BasicComposer.extend({
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var recipient_done = $.Deferred();
|
||||
if (this.options.is_log) {
|
||||
recipient_done.resolve([]);
|
||||
} else {
|
||||
var checked_suggested_partners = this.get_checked_suggested_partners();
|
||||
recipient_done = this.check_suggested_partners(checked_suggested_partners);
|
||||
}
|
||||
recipient_done.then(function (partner_ids) {
|
||||
var recipientDoneDef = $.Deferred();
|
||||
|
||||
// any operation on the full-composer will reload the record, so
|
||||
// warn the user that any unsaved changes on the record will be lost.
|
||||
this.trigger_up('discard_record_changes', {
|
||||
proceed: function () {
|
||||
if (self.options.is_log) {
|
||||
recipientDoneDef.resolve([]);
|
||||
} else {
|
||||
var checked_suggested_partners = self.get_checked_suggested_partners();
|
||||
self.check_suggested_partners(checked_suggested_partners)
|
||||
.then(recipientDoneDef.resolve.bind(recipientDoneDef));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
recipientDoneDef.then(function (partner_ids) {
|
||||
var context = {
|
||||
default_parent_id: self.id,
|
||||
default_body: utils.get_text2html(self.$input.val()),
|
||||
|
||||
@@ -108,7 +108,7 @@ var ChatWindowManager = AbstractService.extend({
|
||||
uuid: session.uuid,
|
||||
name: session.name,
|
||||
keep_unread: options.passively, // don't automatically mark unread messages as seen
|
||||
window: new ExtendedChatWindow(web_client, session.id, prefix + session.name, session.is_folded, session.unread_counter, windowOptions),
|
||||
window: new ExtendedChatWindow(this, session.id, prefix + session.name, session.is_folded, session.unread_counter, windowOptions),
|
||||
};
|
||||
chatSession.window.on("close_chat_session", null, function () {
|
||||
self._closeChat(chatSession);
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
|
||||
&.o_chat_inline_composer {
|
||||
.o_composer_container {
|
||||
@include o-flex(1, 0, auto);
|
||||
@include o-flex(1, 1, auto);
|
||||
}
|
||||
.o_composer {
|
||||
padding: $o-mail-chatter-gap $o-mail-chatter-gap 0 $o-mail-chatter-gap;
|
||||
|
||||
@@ -611,6 +611,285 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert)
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('chatter: discard changes on message post with post_refresh "always"', function (assert) {
|
||||
// After posting a message that always reloads the record, if the record
|
||||
// is dirty (= has some unsaved changes), we should warn the user that
|
||||
// these changes will be lost if he proceeds.
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'<div class="oe_chatter">' +
|
||||
'<field name="message_ids" widget="mail_thread"' +
|
||||
' options="{\'display_log_button\': True, \'post_refresh\': \'always\'}"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
session: {},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
|
||||
return $.when({2: []});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
// Make record dirty
|
||||
form.$('.o_form_sheet input').val('trululu').trigger('input');
|
||||
|
||||
// Send a message
|
||||
form.$('.o_chatter_button_new_message').click();
|
||||
form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
|
||||
form.$('.oe_chatter .o_composer_button_send').click();
|
||||
|
||||
var $modal = $('.modal-dialog');
|
||||
assert.strictEqual($modal.length, 1, "should have a modal opened");
|
||||
assert.strictEqual($modal.find('.modal-body').text(),
|
||||
"The record has been modified, your changes will be discarded. Do you want to proceed?",
|
||||
"should warn the user that any unsaved changes will be lost");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('chatter: discard changes on message post without post_refresh', function (assert) {
|
||||
// After posting a message, if the record is dirty and there are no
|
||||
// post_refresh rule, it will not discard the changes on the record.
|
||||
assert.expect(2);
|
||||
|
||||
var hasDiscardChanges = false; // set if `discard_changes` has been triggered up
|
||||
var messages = [];
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'<div class="oe_chatter">' +
|
||||
'<field name="message_ids" widget="mail_thread"' +
|
||||
' options="{\'display_log_button\': True}"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
session: {},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
|
||||
return $.when({2: []});
|
||||
}
|
||||
if (args.method === 'message_format') {
|
||||
var requested_msgs = _.filter(messages, function (msg) {
|
||||
return _.contains(args.args[0], msg.id);
|
||||
});
|
||||
return $.when(requested_msgs);
|
||||
}
|
||||
if (args.method === 'message_post') {
|
||||
messages.push({
|
||||
attachment_ids: [],
|
||||
author_id: ["42", "Me"],
|
||||
body: args.kwargs.body,
|
||||
date: moment().format('YYYY-MM-DD HH:MM:SS'), // now
|
||||
displayed_author: "Me",
|
||||
id: 42,
|
||||
is_note: args.kwargs.subtype === 'mail.mt_note',
|
||||
is_starred: false,
|
||||
model: 'partner',
|
||||
res_id: 2,
|
||||
});
|
||||
return $.when(42);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
intercepts: {
|
||||
discard_changes: function () {
|
||||
hasDiscardChanges = true; // should not do that
|
||||
},
|
||||
},
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
// Make record dirty
|
||||
form.$('.o_form_sheet input').val('trululu').trigger('input');
|
||||
|
||||
// Send a message
|
||||
form.$('.o_chatter_button_new_message').click();
|
||||
form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
|
||||
form.$('.oe_chatter .o_composer_button_send').click();
|
||||
|
||||
var $modal = $('.modal-dialog');
|
||||
assert.strictEqual($modal.length, 0, "should have no modal opened");
|
||||
assert.notOk(hasDiscardChanges);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('chatter: discard changes on message post with post_refresh "recipients"', function (assert) {
|
||||
// After posting a message with mentions, the record will be reloaded,
|
||||
// as the rpc `message_post` may make changes on some fields of the record.
|
||||
// If the record is dirty (= has some unsaved changes), we should warn the
|
||||
// user that these changes will be lost if he proceeds.
|
||||
assert.expect(2);
|
||||
var done = assert.async();
|
||||
|
||||
var getSuggestionsDef = $.Deferred();
|
||||
|
||||
var messages = [];
|
||||
var bus = new Bus();
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'<div class="oe_chatter">' +
|
||||
'<field name="message_ids" widget="mail_thread"' +
|
||||
' options="{\'display_log_button\': True, \'post_refresh\': \'recipients\'}"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
session: {},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
|
||||
return $.when({2: [[42, "Me"]]});
|
||||
}
|
||||
if (args.method === 'get_mention_suggestions') {
|
||||
getSuggestionsDef.resolve();
|
||||
return $.when([{email: "me@odoo.com", id: 42, name: "Me"}]);
|
||||
}
|
||||
if (args.method === 'message_format') {
|
||||
var requested_msgs = _.filter(messages, function (msg) {
|
||||
return _.contains(args.args[0], msg.id);
|
||||
});
|
||||
return $.when(requested_msgs);
|
||||
}
|
||||
if (args.method === 'message_post') {
|
||||
messages.push({
|
||||
attachment_ids: [],
|
||||
author_id: ["42", "Me"],
|
||||
body: args.kwargs.body,
|
||||
date: moment().format('YYYY-MM-DD HH:MM:SS'), // now
|
||||
displayed_author: "Me",
|
||||
id: 42,
|
||||
is_note: args.kwargs.subtype === 'mail.mt_note',
|
||||
is_starred: false,
|
||||
model: 'partner',
|
||||
res_id: 2,
|
||||
});
|
||||
return $.when(42);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
// Make record dirty
|
||||
form.$('.o_form_sheet input').val('trululu').trigger('input');
|
||||
|
||||
// create a new message
|
||||
form.$('.o_chatter_button_new_message').click();
|
||||
|
||||
// Add a user as mention
|
||||
form.$('.oe_chatter .o_composer_text_field:first()').val("@");
|
||||
|
||||
var $input = form.$('.oe_chatter .o_composer_text_field:first()');
|
||||
$input.val('@');
|
||||
// the cursor position must be set for the mention manager to detect that we are mentionning
|
||||
$input[0].selectionStart = 1;
|
||||
$input[0].selectionEnd = 1;
|
||||
$input.trigger('keyup');
|
||||
|
||||
getSuggestionsDef
|
||||
.then(concurrency.delay.bind(concurrency, 0))
|
||||
.then(function () {
|
||||
// click on mention
|
||||
$input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER}));
|
||||
|
||||
// untick recipient as follower (prompts a res.partner form otherwise)
|
||||
form.$('.o_checkbox input').prop('checked', false);
|
||||
|
||||
// send message
|
||||
form.$('.oe_chatter .o_composer_button_send').click();
|
||||
|
||||
var $modal = $('.modal-dialog');
|
||||
assert.strictEqual($modal.length, 1, "should have a modal opened");
|
||||
assert.strictEqual($modal.find('.modal-body').text(),
|
||||
"The record has been modified, your changes will be discarded. Do you want to proceed?",
|
||||
"should warn the user that any unsaved changes will be lost");
|
||||
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('chatter: discard changes on opening full-composer', function (assert) {
|
||||
// When we open the full-composer, any following operations by the user
|
||||
// will reload the record (even closing the full-composer). Therefore,
|
||||
// we should warn the user when we open the full-composer if the record
|
||||
// is dirty (= has some unsaved changes).
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'<div class="oe_chatter">' +
|
||||
'<field name="message_ids" widget="mail_thread"' +
|
||||
' options="{\'display_log_button\': True,' +
|
||||
' \'post_refresh\': \'always\'}"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
session: {},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
|
||||
return $.when({2: []});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
// Make record dirty
|
||||
form.$('.o_form_sheet input').val('trululu').trigger('input');
|
||||
|
||||
// Open full-composer
|
||||
form.$('.o_chatter_button_new_message').click();
|
||||
form.$('.o_composer_button_full_composer').click();
|
||||
|
||||
var $modal = $('.modal-dialog');
|
||||
assert.strictEqual($modal.length, 1, "should have a modal opened");
|
||||
assert.strictEqual($modal.find('.modal-body').text(),
|
||||
"The record has been modified, your changes will be discarded. Do you want to proceed?",
|
||||
"should warn the user that any unsaved changes will be lost");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('chatter: Attachment viewer', function (assert) {
|
||||
assert.expect(6);
|
||||
this.data.partner.records[0].message_ids = [1];
|
||||
|
||||
@@ -38,6 +38,9 @@ function createBusService(bus) {
|
||||
* Do nothing
|
||||
*/
|
||||
start_polling: function () {},
|
||||
is_odoo_focused: function () {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -46,6 +46,13 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="js_test_assets" name="mail_js_test_assets" inherit_id="web.js_tests_assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/mail/static/tests/helpers/mock_server.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/helpers/test_utils.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<script type="text/javascript" src="/mail/static/tests/chat_window_tests.js"></script>
|
||||
@@ -54,16 +61,12 @@
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_moderation_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/helpers/mock_server.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/helpers/test_utils.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_mobile_suite" name="mail_mobile_tests" inherit_id="web.qunit_mobile_suite">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_mobile_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/helpers/mock_server.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/helpers/test_utils.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
name="action_mass_mailing_attendees"
|
||||
icon="fa-envelope-o"
|
||||
attrs="{'invisible': [('seats_expected', '=', 0)]}">
|
||||
Mail Attendees
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Mail Attendees</span>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
</field>
|
||||
|
||||
@@ -164,7 +164,7 @@ class ReportBomStructure(models.AbstractModel):
|
||||
if line.child_bom_id:
|
||||
qty = line.product_uom_id._compute_quantity(line.product_qty * factor, line.child_bom_id.product_uom_id)
|
||||
sub_price = self._get_price(line.child_bom_id, qty)
|
||||
price += sub_price * qty
|
||||
price += sub_price
|
||||
else:
|
||||
prod_qty = line.product_qty * factor
|
||||
price += (line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * prod_qty)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<template id="report_mrp_bom">
|
||||
<t t-raw="'<base href=%s>' % base_url"/>
|
||||
<div class="container o_mrp_bom_report_page">
|
||||
<t t-if="data['components'] or data['lines']">
|
||||
<t t-if="data.get('components') or data.get('lines')">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>BoM Structure & Cost</h1>
|
||||
|
||||
@@ -54,8 +54,8 @@ odoo.define('payment_stripe.stripe', function(require) {
|
||||
}).done(function(data){
|
||||
handler.isTokenGenerate = false;
|
||||
window.location.href = data;
|
||||
}).fail(function(){
|
||||
var msg = arguments && arguments[1] && arguments[1].data && arguments[1].data.arguments && arguments[1].data.arguments[0];
|
||||
}).fail(function(data){
|
||||
var msg = data && data.data && data.data.message;
|
||||
var wizard = $(qweb.render('stripe.error', {'msg': msg || _t('Payment error')}));
|
||||
wizard.appendTo($('body')).modal({'keyboard': true});
|
||||
});
|
||||
|
||||
@@ -56,31 +56,14 @@ class StripeTest(StripeCommon):
|
||||
# ----------------------------------------
|
||||
# Test: button direct rendering
|
||||
# ----------------------------------------
|
||||
form_values = {
|
||||
'amount': 320.0,
|
||||
'currency': 'EUR',
|
||||
'address_line1': 'Huge Street 2/543',
|
||||
'address_city': 'Sin City',
|
||||
'address_country': 'Belgium',
|
||||
'email': 'norbert.buyer@example.com',
|
||||
'address_zip': '1000',
|
||||
'name': 'Norbert Buyer',
|
||||
'phone': '0032 12 34 56 78'
|
||||
}
|
||||
|
||||
# render the button
|
||||
res = self.stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values)
|
||||
post_url = "https://checkout.stripe.com/checkout.js"
|
||||
email = "norbert.buyer@example.com"
|
||||
res = self.stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values).decode('utf-8')
|
||||
popup_script_src = 'script src="https://checkout.stripe.com/checkout.js"'
|
||||
# check form result
|
||||
if "https://checkout.stripe.com/checkout.js" in res[0]:
|
||||
self.assertEqual(post_url, 'https://checkout.stripe.com/checkout.js', 'Stripe: wrong form POST url')
|
||||
self.assertIn(popup_script_src, res, "Stripe: popup script not found in template render")
|
||||
# Generated and received
|
||||
if email in res[0]:
|
||||
self.assertEqual(
|
||||
email, form_values.get('email'),
|
||||
'Stripe: wrong value for input %s: received %s instead of %s' % (email, email, form_values.get('email'))
|
||||
)
|
||||
self.assertIn(self.buyer_values.get('partner_email'), res, 'Stripe: email input not found in rendered template')
|
||||
|
||||
def test_30_stripe_form_management(self):
|
||||
self.assertEqual(self.stripe.environment, 'test', 'test without test environment')
|
||||
|
||||
@@ -326,10 +326,11 @@ class PosConfig(models.Model):
|
||||
def name_get(self):
|
||||
result = []
|
||||
for config in self:
|
||||
if (not config.session_ids) or (config.session_ids[0].state == 'closed'):
|
||||
last_session = self.env['pos.session'].search([('config_id', '=', config.id)], limit=1)
|
||||
if (not last_session) or (last_session.state == 'closed'):
|
||||
result.append((config.id, config.name + ' (' + _('not used') + ')'))
|
||||
continue
|
||||
result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')'))
|
||||
result.append((config.id, config.name + ' (' + last_session.user_id.name + ')'))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -418,8 +418,10 @@ class PosOrder(models.Model):
|
||||
def _get_pos_anglo_saxon_price_unit(self, product, partner_id, quantity):
|
||||
price_unit = product._get_anglo_saxon_price_unit()
|
||||
if product._get_invoice_policy() == "delivery":
|
||||
moves = self.filtered(lambda o: o.partner_id.id == partner_id).mapped('picking_id.move_lines').filtered(lambda m: m.product_id.id == product.id)
|
||||
moves.sorted(lambda x: x.date)
|
||||
moves = self.filtered(lambda o: o.partner_id.id == partner_id)\
|
||||
.mapped('picking_id.move_lines')\
|
||||
.filtered(lambda m: m.product_id.id == product.id)\
|
||||
.sorted(lambda x: x.date)
|
||||
average_price_unit = product._compute_average_price(0, quantity, moves)
|
||||
price_unit = average_price_unit or price_unit
|
||||
# In the SO part, the entries will be inverted by function compute_invoice_totals
|
||||
|
||||
@@ -215,7 +215,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
var self = this;
|
||||
|
||||
function status(){
|
||||
self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})
|
||||
self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500})
|
||||
.then(function(driver_status){
|
||||
self.set_connection_status('connected',driver_status);
|
||||
},function(){
|
||||
@@ -239,7 +239,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
callbacks[i](params);
|
||||
}
|
||||
if(this.get('status').status !== 'disconnected'){
|
||||
return this.connection.rpc('/hw_proxy/' + name, params || {});
|
||||
return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true});
|
||||
}else{
|
||||
return (new $.Deferred()).reject();
|
||||
}
|
||||
@@ -588,7 +588,7 @@ var BarcodeReader = core.Class.extend({
|
||||
this.remote_active = 1;
|
||||
|
||||
function waitforbarcode(){
|
||||
return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500})
|
||||
return self.proxy.connection.rpc('/hw_proxy/scanner',{},{shadow: true, timeout:7500})
|
||||
.then(function(barcode){
|
||||
if(!self.remote_scanning){
|
||||
self.remote_active = 0;
|
||||
|
||||
@@ -92,11 +92,11 @@
|
||||
<field name="cash_register_balance_end" nolabel="1" attrs="{'invisible' : [('state', '=', 'opening_control')]}" class="oe_bold oe_pos_total" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||
</group>
|
||||
<div class="oe_clear"/>
|
||||
<div attrs="{'invisible' : ['|', ('cash_journal_id', '=', False), ('state', '!=', 'opening_control')]}" class="o_view_nocontent" groups="point_of_sale.group_pos_manager">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
You can define another list of available currencies on the
|
||||
<i>Cash Registers</i> tab of the <b><field name="cash_journal_id" class="oe_inline"/></b>
|
||||
payment method.
|
||||
<div attrs="{'invisible' : ['|', ('cash_journal_id', '=', False), ('state', '!=', 'opening_control')]}" class="o_field_nocontent" groups="point_of_sale.group_pos_manager">
|
||||
<p>
|
||||
You can define another list of available currencies on the
|
||||
<i>Cash Registers</i> tab of the <b><field name="cash_journal_id" class="oe_inline"/></b>
|
||||
payment method.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,11 +27,14 @@ var Printer = core.Class.extend(mixins.PropertiesMixin,{
|
||||
function send_printing_job(){
|
||||
if(self.receipt_queue.length > 0){
|
||||
var r = self.receipt_queue.shift();
|
||||
self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000})
|
||||
var options = {shadow: true, timeout: 5000};
|
||||
self.connection.rpc('/hw_proxy/print_xml_receipt', {receipt: r}, options)
|
||||
.then(function(){
|
||||
send_printing_job();
|
||||
},function(){
|
||||
},function(error, event){
|
||||
self.receipt_queue.unshift(r);
|
||||
console.log('There was an error while trying to print the order:');
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -55,7 +58,7 @@ models.load_models({
|
||||
|
||||
for(var i = 0; i < printers.length; i++){
|
||||
if(active_printers[printers[i].id]){
|
||||
var url = printers[i].proxy_ip;
|
||||
var url = printers[i].proxy_ip || '';
|
||||
if(url.indexOf('//') < 0){
|
||||
url = 'http://'+url;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@
|
||||
[<span t-esc="product.code"/>]
|
||||
</t>
|
||||
<span t-esc="product.name"/>
|
||||
<span t-foreach="product.attribute_value_ids" t-as="attribute_value">
|
||||
<span t-if="attribute_value_first">-</span>
|
||||
<span t-if="not attribute_value_last" t-esc="attribute_value.name+','"/>
|
||||
<span t-else="" t-esc="attribute_value.name"/>
|
||||
</span>
|
||||
</td>
|
||||
<t t-foreach="data['quantities']" t-as="quantity">
|
||||
<td><strong t-esc="categ_data['prices'][product.id][quantity]"
|
||||
|
||||
@@ -109,6 +109,11 @@
|
||||
<field name="packaging_ids" nolabel="1" context="{'default_product_id': product_variant_id,'tree_view_ref':'product.product_packaging_tree_view', 'form_view_ref':'product.product_packaging_form_view'}"/>
|
||||
</group>
|
||||
</page>
|
||||
<group name="description_internal">
|
||||
<group string="Description for Internal">
|
||||
<field name="description" nolabel="1" placeholder="This note is only for internal purposes."/>
|
||||
</group>
|
||||
</group>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
|
||||
@@ -935,12 +935,12 @@ class Task(models.Model):
|
||||
@api.multi
|
||||
def message_get_suggested_recipients(self):
|
||||
recipients = super(Task, self).message_get_suggested_recipients()
|
||||
for task in self.filtered('partner_id'):
|
||||
reason = _('Customer Email') if task.partner_id.email else _('Customer')
|
||||
for task in self:
|
||||
if task.partner_id:
|
||||
reason = _('Customer Email') if task.partner_id.email else _('Customer')
|
||||
task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason)
|
||||
elif task.email_from:
|
||||
task._message_add_suggested_recipient(recipients, partner=task.email_from, reason=reason)
|
||||
task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email'))
|
||||
return recipients
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -324,6 +324,8 @@ class PurchaseOrder(models.Model):
|
||||
for inv in order.invoice_ids:
|
||||
if inv and inv.state not in ('cancel', 'draft'):
|
||||
raise UserError(_("Unable to cancel this purchase order. You must first cancel the related vendor bills."))
|
||||
order.order_line.write({'move_dest_ids':[(5,0,0)]})
|
||||
|
||||
self.write({'state': 'cancel'})
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -313,8 +313,12 @@ class PurchaseOrder(models.Model):
|
||||
self.payment_term_id = payment_term.id,
|
||||
self.company_id = requisition.company_id.id
|
||||
self.currency_id = requisition.currency_id.id
|
||||
self.origin = requisition.name
|
||||
self.partner_ref = requisition.name # to control vendor bill based on agreement reference
|
||||
if not self.origin or requisition.name not in self.origin.split(', '):
|
||||
if self.origin:
|
||||
if requisition.name:
|
||||
self.origin = self.origin + ', ' + requisition.name
|
||||
else:
|
||||
self.origin = requisition.name
|
||||
self.notes = requisition.description
|
||||
self.date_order = requisition.date_end or fields.Datetime.now()
|
||||
self.picking_type_id = requisition.picking_type_id.id
|
||||
|
||||
@@ -36,7 +36,8 @@ class ProcurementRule(models.Model):
|
||||
cache[domain] = po
|
||||
if not po:
|
||||
vals = self._prepare_purchase_order(product_id, product_qty, product_uom, origin, values, partner)
|
||||
po = self.env['purchase.order'].sudo().create(vals)
|
||||
company_id = values.get('company_id') and values['company_id'].id or self.env.user.company_id.id
|
||||
po = self.env['purchase.order'].with_context(force_company=company_id).sudo().create(vals)
|
||||
cache[domain] = po
|
||||
elif not po.origin or origin not in po.origin.split(', '):
|
||||
if po.origin:
|
||||
|
||||
@@ -41,6 +41,8 @@ def datetime_to_string(dt):
|
||||
|
||||
def float_to_time(hours):
|
||||
""" Convert a number of hours into a time object. """
|
||||
if float_hour == 24.0:
|
||||
return time.max
|
||||
fractional, integral = math.modf(hours)
|
||||
return time(int(integral), int(60 * fractional), 0)
|
||||
|
||||
@@ -400,7 +402,9 @@ class ResourceCalendarAttendance(models.Model):
|
||||
], 'Day of Week', required=True, index=True, default='0')
|
||||
date_from = fields.Date(string='Starting Date')
|
||||
date_to = fields.Date(string='End Date')
|
||||
hour_from = fields.Float(string='Work from', required=True, index=True, help="Start and End time of working.")
|
||||
hour_from = fields.Float(string='Work from', required=True, index=True,
|
||||
help="Start and End time of working.\n"
|
||||
"A specific value of 24:00 is interpreted as 23:59:59.999999.")
|
||||
hour_to = fields.Float(string='Work to', required=True)
|
||||
calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade')
|
||||
day_period = fields.Selection([('morning', 'Morning'), ('afternoon', 'Afternoon')], required=True, default='morning')
|
||||
|
||||
@@ -184,6 +184,21 @@ class TestCalendar(TestResourceCommon):
|
||||
leave.unlink()
|
||||
|
||||
# leave of very small size
|
||||
def test_calendar_working_hours_24(self):
|
||||
self.att_4 = self.env['resource.calendar.attendance'].create({
|
||||
'name': 'Att4',
|
||||
'calendar_id': self.calendar.id,
|
||||
'dayofweek': '2',
|
||||
'hour_from': 0,
|
||||
'hour_to': 24
|
||||
})
|
||||
res = self.calendar.get_work_hours_count(
|
||||
Datetime.from_string('2018-06-19 23:00:00'),
|
||||
Datetime.from_string('2018-06-21 01:00:00'),
|
||||
self.resource1_id,
|
||||
compute_leaves=True)
|
||||
self.assertAlmostEqual(res, 24.0)
|
||||
|
||||
leave = self.env['resource.calendar.leaves'].create({
|
||||
'name': 'zero_length',
|
||||
'calendar_id': self.calendar_patel.id,
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
% set is_online = access_action and access_action['type'] == 'ir.actions.act_url'
|
||||
% set doc_name = 'quotation' if object.state in ('draft', 'sent') else 'order confirmation'
|
||||
% set pay_sign_name = object.get_portal_confirmation_action()
|
||||
% set access_name = is_online and object.state in ('draft', 'sent') and pay_sign_name in ('pay', 'sign') and 'Accept and %s online' % pay_sign_name or 'View %s' % doc_name
|
||||
% set access_name = is_online and object.state in ('draft', 'sent') and pay_sign_name == 'pay' and 'Accept and pay online' or pay_sign_name == 'sign' and 'Accept and sign online' or 'View %s' % doc_name
|
||||
% set access_url = is_online and object.get_mail_url() or ''
|
||||
|
||||
% if object.partner_id.parent_id:
|
||||
|
||||
@@ -19,7 +19,7 @@ class AccountAnalyticLine(models.Model):
|
||||
@api.model
|
||||
def create(self, values):
|
||||
result = super(AccountAnalyticLine, self).create(values)
|
||||
if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy != 'no': # allow to force a False value for so_line
|
||||
if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy != 'no' and result.amount <= 0: # allow to force a False value for so_line
|
||||
result.sudo()._sale_determine_order_line()
|
||||
return result
|
||||
|
||||
|
||||
@@ -1272,3 +1272,8 @@ class SaleOrderLine(models.Model):
|
||||
discount = (new_list_price - price) / new_list_price * 100
|
||||
if discount > 0:
|
||||
self.discount = discount
|
||||
|
||||
|
||||
def _is_delivery(self):
|
||||
self.ensure_one()
|
||||
return False
|
||||
@@ -362,7 +362,7 @@
|
||||
<h2>Invoicing</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div id="sales_settings_invoicing_policy"
|
||||
class="col-xs-12 col-md-6 o_setting_box"
|
||||
class="col-xs-12 col-md-6 o_setting_box hidden"
|
||||
title="This default value is applied to any new product created. This can be changed in the product detail form.">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="default_invoice_policy"/>
|
||||
|
||||
@@ -48,6 +48,8 @@ tour.register('sale_tour', {
|
||||
in_modal: false,
|
||||
run: function (actions) {
|
||||
actions.auto();
|
||||
// There might be a modal because of the view:
|
||||
// sale.order.form.editable.list, enabled by some groups
|
||||
if ($(".modal-footer .btn-primary").length) {
|
||||
actions.auto(".modal-footer .btn-primary");
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ class AccountInvoiceLine(models.Model):
|
||||
qty_done = sum([x.uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in s_line.invoice_lines if x.invoice_id.state in ('open', 'paid')])
|
||||
quantity = self.uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
|
||||
# Put moves in fixed order by date executed
|
||||
moves = s_line.move_ids
|
||||
moves.sorted(lambda x: x.date)
|
||||
moves = s_line.move_ids.sorted(lambda x: x.date)
|
||||
# Go through all the moves and do nothing until you get to qty_done
|
||||
# Beyond qty_done we need to calculate the average of the price_unit
|
||||
# on the moves we encounter.
|
||||
|
||||
@@ -40,10 +40,6 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
self.uom_kg = self.env['uom.uom'].search([('category_id', '=', self.categ_kgm.id), ('uom_type', '=', 'reference')], limit=1)
|
||||
self.uom_kg.write({
|
||||
'name': 'Test-KG',
|
||||
'category_id': self.categ_kgm.id,
|
||||
'factor_inv': 1,
|
||||
'factor': 1,
|
||||
'uom_type': 'reference',
|
||||
'rounding': 0.000001})
|
||||
self.uom_gm = self.UoM.create({
|
||||
'name': 'Test-G',
|
||||
@@ -54,9 +50,6 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
self.uom_unit = self.env['uom.uom'].search([('category_id', '=', self.categ_unit.id), ('uom_type', '=', 'reference')], limit=1)
|
||||
self.uom_unit.write({
|
||||
'name': 'Test-Unit',
|
||||
'category_id': self.categ_unit.id,
|
||||
'factor': 1,
|
||||
'uom_type': 'reference',
|
||||
'rounding': 1.0})
|
||||
self.uom_dozen = self.UoM.create({
|
||||
'name': 'Test-DozenA',
|
||||
|
||||
@@ -28,7 +28,7 @@ class SaleOrder(models.Model):
|
||||
for order in self:
|
||||
dates_list = []
|
||||
confirm_date = fields.Datetime.from_string(order.confirmation_date if order.state == 'sale' else fields.Datetime.now())
|
||||
for line in order.order_line.filtered(lambda x: x.state != 'cancel'):
|
||||
for line in order.order_line.filtered(lambda x: x.state != 'cancel' and not x._is_delivery()):
|
||||
dt = confirm_date + timedelta(days=line.customer_lead or 0.0)
|
||||
dates_list.append(dt)
|
||||
if dates_list:
|
||||
|
||||
@@ -17,7 +17,6 @@ class PaymentPortal(http.Controller):
|
||||
:return html: form containing all values related to the acquirer to
|
||||
redirect customers to the acquirer website """
|
||||
success_url = kwargs.get('success_url', '/my')
|
||||
callback_method = kwargs.get('callback_method', '')
|
||||
|
||||
order_sudo = request.env['sale.order'].sudo().browse(order_id)
|
||||
if not order_sudo:
|
||||
@@ -28,12 +27,12 @@ class PaymentPortal(http.Controller):
|
||||
except:
|
||||
return False
|
||||
|
||||
if request.env.user == request.env.ref('base.public_user'):
|
||||
save_token = False
|
||||
|
||||
# Create transaction
|
||||
vals = {
|
||||
'acquirer_id': acquirer_id,
|
||||
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', order_sudo._name)], limit=1).id,
|
||||
'callback_res_id': order_sudo.id,
|
||||
'callback_method': callback_method,
|
||||
}
|
||||
|
||||
if save_token:
|
||||
@@ -56,7 +55,6 @@ class PaymentPortal(http.Controller):
|
||||
""" Use a token to perform a s2s transaction """
|
||||
error_url = kwargs.get('error_url', '/my')
|
||||
success_url = kwargs.get('success_url', '/my')
|
||||
callback_method = kwargs.get('callback_method', '')
|
||||
access_token = kwargs.get('access_token')
|
||||
params = {}
|
||||
if access_token:
|
||||
@@ -71,23 +69,16 @@ class PaymentPortal(http.Controller):
|
||||
token = request.env['payment.token'].sudo().browse(int(pm_id))
|
||||
except (ValueError, TypeError):
|
||||
token = False
|
||||
if not token:
|
||||
|
||||
token_owner = order_sudo.partner_id if request.env.user == request.env.ref('base.public_user') else request.env.user.partner_id
|
||||
if not token or token.partner_id != token_owner:
|
||||
params['error'] = 'pay_sale_invalid_token'
|
||||
return request.redirect(_build_url_w_params(error_url, params))
|
||||
|
||||
try:
|
||||
pm_id = int(pm_id)
|
||||
except (ValueError, TypeError):
|
||||
params['error'] = 'pay_sale_invalid_token'
|
||||
return request.redirect(_build_url_w_params(error_url, params))
|
||||
|
||||
# Create transaction
|
||||
# Create transaction
|
||||
vals = {
|
||||
'payment_token_id': pm_id,
|
||||
'payment_token_id': token.id,
|
||||
'type': 'server2server',
|
||||
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', order_sudo._name)], limit=1).id,
|
||||
'callback_res_id': order_sudo.id,
|
||||
'callback_method': callback_method,
|
||||
}
|
||||
|
||||
order_sudo._create_payment_transaction(vals)
|
||||
|
||||
@@ -10,5 +10,16 @@ class CustomerPortal(CustomerPortal):
|
||||
def _order_get_page_view_values(self, order, access_token, **kwargs):
|
||||
values = super(CustomerPortal, self)._order_get_page_view_values(order, access_token, **kwargs)
|
||||
if values['portal_confirmation'] == 'pay':
|
||||
values.update(request.env['payment.acquirer']._get_available_payment_input(order.partner_id, order.company_id))
|
||||
payment_inputs = request.env['payment.acquirer']._get_available_payment_input(order.partner_id, order.company_id)
|
||||
# if not connected (using public user), the method _get_available_payment_input will return public user tokens
|
||||
is_public_user = request.env.ref('base.public_user') == request.env.user
|
||||
if is_public_user:
|
||||
# we should not display payment tokens owned by the public user
|
||||
payment_inputs.pop('pms', None)
|
||||
token_count = request.env['payment.token'].sudo().search_count([('acquirer_id.company_id', '=', order.company_id.id),
|
||||
('partner_id', '=', order.partner_id.id),
|
||||
])
|
||||
values['existing_token'] = token_count > 0
|
||||
values.update(payment_inputs)
|
||||
values['partner_id'] = order.partner_id if is_public_user else request.env.user.partner_id
|
||||
return values
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" t-if="existing_token">
|
||||
<div class="col-md-offset-3 col-md-6">
|
||||
<i class="fa fa-info"></i> You have credits card registered, you can log-in to be able to use them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-if='invoices']" position="before">
|
||||
|
||||
@@ -35,9 +35,7 @@ class AccountInvoiceLine(models.Model):
|
||||
qty_done = sum([x.uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in s_line.invoice_lines if x.invoice_id.state in ('open', 'paid')])
|
||||
quantity = self.uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
|
||||
# Put moves in fixed order by date executed
|
||||
moves = self.env['stock.move']
|
||||
moves |= s_line.move_ids
|
||||
moves.sorted(lambda x: x.date)
|
||||
moves = s_line.move_ids.sorted(lambda x: x.date)
|
||||
# Go through all the moves and do nothing until you get to qty_done
|
||||
# Beyond qty_done we need to calculate the average of the price_unit
|
||||
# on the moves we encounter.
|
||||
|
||||
@@ -95,8 +95,9 @@ class ProjectTask(models.Model):
|
||||
@api.model
|
||||
def create(self, values):
|
||||
# sub task has the same so line than their parent
|
||||
if 'parent_id' in values and values['parent_id']:
|
||||
values['sale_line_id'] = self.env['project.task'].browse(values['parent_id']).sudo().sale_line_id.id
|
||||
parent_id = values['parent_id'] if 'parent_id' in values else self.env.context.get('default_parent_id')
|
||||
if parent_id:
|
||||
values['sale_line_id'] = self.env['project.task'].browse(parent_id).sudo().sale_line_id.id
|
||||
return super(ProjectTask, self).create(values)
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -582,3 +582,27 @@ class ProductCategory(models.Model):
|
||||
category = category.parent_id
|
||||
routes |= category.route_ids
|
||||
self.total_route_ids = routes
|
||||
|
||||
|
||||
class UoM(models.Model):
|
||||
_inherit = 'uom.uom'
|
||||
|
||||
def write(self, values):
|
||||
# Users can not update the factor if open stock moves are based on it
|
||||
if 'factor' in values or 'factor_inv' in values or 'category_id' in values:
|
||||
changed = self.filtered(
|
||||
lambda u: any(u[f] != values[f] if f in values else False
|
||||
for f in {'factor', 'factor_inv', 'category_id'}))
|
||||
if changed:
|
||||
stock_move_lines = self.env['stock.move.line'].search_count([
|
||||
('product_uom_id.category_id', 'in', changed.mapped('category_id.id')),
|
||||
('state', '!=', 'cancel'),
|
||||
])
|
||||
|
||||
if stock_move_lines:
|
||||
raise UserError(_(
|
||||
"You cannot change the ratio of this unit of mesure as some"
|
||||
" products with this UoM have already been moved or are "
|
||||
"currently reserved."
|
||||
))
|
||||
return super(UoM, self).write(values)
|
||||
|
||||
@@ -81,6 +81,21 @@ class Location(models.Model):
|
||||
if 'usage' in values and values['usage'] == 'view':
|
||||
if self.mapped('quant_ids'):
|
||||
raise UserError(_("This location's usage cannot be changed to view as it contains products."))
|
||||
if 'usage' in values or 'scrap_location' in values:
|
||||
|
||||
modified_locations = self.filtered(
|
||||
lambda l: any(l[f] != values[f] if f in values else False
|
||||
for f in {'usage', 'scrap_location'}))
|
||||
reserved_quantities = self.env['stock.move.line'].search_count([
|
||||
('location_id', 'in', modified_locations.ids),
|
||||
('product_qty', '>', 0),
|
||||
])
|
||||
if reserved_quantities:
|
||||
raise UserError(_(
|
||||
"You cannot change the location type or its use as a scrap"
|
||||
" location as there are products reserved in this location."
|
||||
" Please unreserve the products first."
|
||||
))
|
||||
return super(Location, self).write(values)
|
||||
|
||||
def name_get(self):
|
||||
|
||||
@@ -796,7 +796,7 @@ class StockMove(models.Model):
|
||||
group_id = False
|
||||
return {
|
||||
'company_id': self.company_id,
|
||||
'date_planned': self.date,
|
||||
'date_planned': self.date_expected,
|
||||
'move_dest_ids': self,
|
||||
'group_id': group_id,
|
||||
'route_ids': self.route_ids,
|
||||
|
||||
@@ -627,6 +627,7 @@ class Picking(models.Model):
|
||||
all_in = True
|
||||
pack_move_lines = self.move_line_ids.filtered(lambda ml: ml.package_id == package)
|
||||
keys = ['product_id', 'lot_id']
|
||||
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
|
||||
grouped_quants = {}
|
||||
for k, g in groupby(sorted(package.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)):
|
||||
@@ -635,8 +636,8 @@ class Picking(models.Model):
|
||||
grouped_ops = {}
|
||||
for k, g in groupby(sorted(pack_move_lines, key=itemgetter(*keys)), key=itemgetter(*keys)):
|
||||
grouped_ops[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty'))
|
||||
if any(grouped_quants.get(key, 0) - grouped_ops.get(key, 0) != 0 for key in grouped_quants) \
|
||||
or any(grouped_ops.get(key, 0) - grouped_quants.get(key, 0) != 0 for key in grouped_ops):
|
||||
if any(not float_is_zero(grouped_quants.get(key, 0) - grouped_ops.get(key, 0), precision_digits=precision_digits) for key in grouped_quants) \
|
||||
or any(not float_is_zero(grouped_ops.get(key, 0) - grouped_quants.get(key, 0), precision_digits=precision_digits) for key in grouped_ops):
|
||||
all_in = False
|
||||
return all_in
|
||||
|
||||
|
||||
@@ -380,6 +380,12 @@ class QuantPackage(models.Model):
|
||||
|
||||
def unpack(self):
|
||||
for package in self:
|
||||
move_line_to_modify = self.env['stock.move.line'].search([
|
||||
('package_id', '=', package.id),
|
||||
('state', 'in', ('assigned', 'partially_available')),
|
||||
('product_qty', '!=', 0),
|
||||
])
|
||||
move_line_to_modify.write({'package_id': False})
|
||||
package.mapped('quant_ids').write({'package_id': False})
|
||||
|
||||
def action_view_picking(self):
|
||||
|
||||
@@ -397,7 +397,7 @@ class Warehouse(models.Model):
|
||||
|
||||
pull_rules_list = supplier_wh._get_supply_pull_rules_values(
|
||||
[self.Routing(output_location, transit_location, supplier_wh.out_type_id)],
|
||||
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': self.id})
|
||||
values={'route_id': inter_wh_route.id})
|
||||
pull_rules_list += self._get_supply_pull_rules_values(
|
||||
[self.Routing(transit_location, input_location, self.in_type_id)],
|
||||
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': supplier_wh.id})
|
||||
|
||||
@@ -9,6 +9,7 @@ from . import test_quant
|
||||
from . import test_inventory
|
||||
from . import test_move
|
||||
from . import test_move2
|
||||
from . import test_robustness
|
||||
from . import test_packing
|
||||
from . import test_packing_neg
|
||||
from . import test_proc_rule
|
||||
|
||||
@@ -48,10 +48,6 @@ class TestStockCommon(common.TransactionCase):
|
||||
self.uom_kg = self.env['uom.uom'].search([('category_id', '=', self.categ_kgm), ('uom_type', '=', 'reference')], limit=1)
|
||||
self.uom_kg.write({
|
||||
'name': 'Test-KG',
|
||||
'category_id': self.categ_kgm,
|
||||
'factor_inv': 1,
|
||||
'factor': 1,
|
||||
'uom_type': 'reference',
|
||||
'rounding': 0.000001})
|
||||
self.uom_tone = self.UomObj.create({
|
||||
'name': 'Test-Tone',
|
||||
@@ -75,9 +71,6 @@ class TestStockCommon(common.TransactionCase):
|
||||
self.uom_unit = self.env['uom.uom'].search([('category_id', '=', self.categ_unit), ('uom_type', '=', 'reference')], limit=1)
|
||||
self.uom_unit.write({
|
||||
'name': 'Test-Unit',
|
||||
'category_id': self.categ_unit,
|
||||
'factor': 1,
|
||||
'uom_type': 'reference',
|
||||
'rounding': 1.0})
|
||||
self.uom_dozen = self.UomObj.create({
|
||||
'name': 'Test-DozenA',
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestRobustness(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestRobustness, self).setUp()
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.customer_location = self.env.ref('stock.stock_location_customers')
|
||||
self.uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
self.uom_dozen = self.env.ref('uom.product_uom_dozen')
|
||||
self.product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
def test_uom_factor(self):
|
||||
""" Changing the factor of a unit of measure shouldn't be allowed while
|
||||
quantities are reserved, else the existing move lines won't be consistent
|
||||
with the `reserved_quantity` on quants.
|
||||
"""
|
||||
# make some stock
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
12,
|
||||
)
|
||||
|
||||
# reserve a dozen
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_uom_rounding',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
quant = self.env['stock.quant']._gather(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
)
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 12)
|
||||
self.assertEqual(move1.product_qty, 12)
|
||||
|
||||
# change the factor
|
||||
with self.assertRaises(UserError):
|
||||
with self.cr.savepoint():
|
||||
move1.product_uom.factor = 0.05
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 12)
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
self.assertEqual(move1.product_qty, 12)
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
|
||||
def test_location_usage(self):
|
||||
""" Changing the usage of a location shouldn't be allowed while
|
||||
quantities are reserved, else the existing move lines won't be
|
||||
consistent with the `reserved_quantity` on the quants.
|
||||
"""
|
||||
# change stock usage
|
||||
self.stock_location.scrap_location = True
|
||||
|
||||
# make some stock
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
1,
|
||||
)
|
||||
|
||||
# reserve a unit
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_location_archive',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
quant = self.env['stock.quant']._gather(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
)
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 0) # reservation is bypassed in scrap location
|
||||
self.assertEqual(move1.product_qty, 1)
|
||||
|
||||
# change the stock usage
|
||||
with self.assertRaises(UserError):
|
||||
with self.cr.savepoint():
|
||||
self.stock_location.scrap_location = False
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
|
||||
def test_package_unpack(self):
|
||||
""" Unpack a package that contains quants with a reservation
|
||||
should also remove the package on the reserved move lines.
|
||||
"""
|
||||
package = self.env['stock.quant.package'].create({
|
||||
'name': 'Shell Helix HX7 10W30',
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
10,
|
||||
package_id=package
|
||||
)
|
||||
|
||||
# reserve a dozen
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_uom_rounding',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 10,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
|
||||
move1.result_package_id = False
|
||||
|
||||
package.unpack()
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location, package_id=package)), 0)
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).reserved_quantity, 0)
|
||||
@@ -226,6 +226,215 @@ class TestWarehouse(TestStockCommon):
|
||||
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
|
||||
self.assertEqual(len(quant), 1)
|
||||
|
||||
def test_resupply_route(self):
|
||||
""" Simulate a resupply chain between warehouses.
|
||||
Stock -> transit -> Dist. -> transit -> Shop -> Customer
|
||||
Create the move from Shop to Customer and ensure that all the pull
|
||||
rules are triggered in order to complete the move chain to Stock.
|
||||
"""
|
||||
warehouse_stock = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock.',
|
||||
'code': 'STK',
|
||||
})
|
||||
|
||||
warehouse_distribution = self.env['stock.warehouse'].create({
|
||||
'name': 'Dist.',
|
||||
'code': 'DIST',
|
||||
'default_resupply_wh_id': warehouse_stock.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_stock.id])]
|
||||
})
|
||||
|
||||
warehouse_shop = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop',
|
||||
'code': 'SHOP',
|
||||
'default_resupply_wh_id': warehouse_distribution.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution.id])]
|
||||
})
|
||||
|
||||
route_stock_to_dist = warehouse_distribution.resupply_route_ids
|
||||
route_dist_to_shop = warehouse_shop.resupply_route_ids
|
||||
|
||||
# Change the procure_method on the pull rules between dist and shop
|
||||
# warehouses. Since mto and resupply routes are both on product it will
|
||||
# select one randomly between them and if it select the resupply it is
|
||||
# 'make to stock' and it will not create the picking between stock and
|
||||
# dist warehouses.
|
||||
route_dist_to_shop.pull_ids.write({'procure_method': 'make_to_order'})
|
||||
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Fakir',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, route_id) for route_id in [route_stock_to_dist.id, route_dist_to_shop.id, self.env.ref('stock.route_warehouse0_mto').id]],
|
||||
})
|
||||
|
||||
picking_out = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop.lot_stock_id.id,
|
||||
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out.id,
|
||||
'location_id': warehouse_shop.lot_stock_id.id,
|
||||
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||||
'warehouse_id': warehouse_shop.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out.action_confirm()
|
||||
|
||||
moves = self.env['stock.move'].search([('product_id', '=', product.id)])
|
||||
# Shop/Stock -> Customer
|
||||
# Transit -> Shop/Stock
|
||||
# Dist/Stock -> Transit
|
||||
# Transit -> Dist/Stock
|
||||
# Stock/Stock -> Transit
|
||||
self.assertEqual(len(moves), 5, 'Invalid moves number.')
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_stock.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_distribution.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_distribution.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_shop.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_shop.lot_stock_id.id)]))
|
||||
|
||||
def test_mutiple_resupply_warehouse(self):
|
||||
""" Simulate the following situation:
|
||||
- 2 shops with stock are resupply by 2 distinct warehouses
|
||||
- Shop Namur is resupply by the warehouse stock Namur
|
||||
- Shop Wavre is resupply by the warehouse stock Wavre
|
||||
- Simulate 2 moves for the same product but in different shop.
|
||||
This test ensure that the move are supplied by the correct distribution
|
||||
warehouse.
|
||||
"""
|
||||
customer_location = self.env.ref('stock.stock_location_customers')
|
||||
|
||||
warehouse_distribution_wavre = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock Wavre.',
|
||||
'code': 'WV',
|
||||
})
|
||||
|
||||
warehouse_shop_wavre = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop Wavre',
|
||||
'code': 'SHWV',
|
||||
'default_resupply_wh_id': warehouse_distribution_wavre.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution_wavre.id])]
|
||||
})
|
||||
|
||||
warehouse_distribution_namur = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock Namur.',
|
||||
'code': 'NM',
|
||||
})
|
||||
|
||||
warehouse_shop_namur = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop Namur',
|
||||
'code': 'SHNM',
|
||||
'default_resupply_wh_id': warehouse_distribution_namur.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution_namur.id])]
|
||||
})
|
||||
|
||||
route_shop_namur = warehouse_shop_namur.resupply_route_ids
|
||||
route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
|
||||
|
||||
# The product contains the 2 resupply routes.
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Fakir',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, route_id) for route_id in [route_shop_namur.id, route_shop_wavre.id, self.env.ref('stock.route_warehouse0_mto').id]],
|
||||
})
|
||||
|
||||
# Add 1 quant in each distribution warehouse.
|
||||
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_wavre.lot_stock_id, 1.0)
|
||||
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_namur.lot_stock_id, 1.0)
|
||||
|
||||
# Create the move for the shop Namur. Should create a resupply from
|
||||
# distribution warehouse Namur.
|
||||
picking_out_namur = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop_namur.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out_namur.id,
|
||||
'location_id': warehouse_shop_namur.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
'warehouse_id': warehouse_shop_namur.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out_namur.action_confirm()
|
||||
|
||||
# Validate the picking
|
||||
# Dist. warehouse Namur -> transit Location -> Shop Namur
|
||||
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_namur.lot_stock_id.id)])
|
||||
self.assertTrue(picking_stock_transit)
|
||||
picking_stock_transit.action_assign()
|
||||
picking_stock_transit.move_lines[0].quantity_done = 1.0
|
||||
picking_stock_transit.action_done()
|
||||
|
||||
picking_transit_shop_namur = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_namur.lot_stock_id.id)])
|
||||
self.assertTrue(picking_transit_shop_namur)
|
||||
picking_transit_shop_namur.action_assign()
|
||||
picking_transit_shop_namur.move_lines[0].quantity_done = 1.0
|
||||
picking_transit_shop_namur.action_done()
|
||||
|
||||
picking_out_namur.action_assign()
|
||||
picking_out_namur.move_lines[0].quantity_done = 1.0
|
||||
picking_out_namur.action_done()
|
||||
|
||||
# Check that the correct quantity has been provided to customer
|
||||
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 1)
|
||||
# Ensure there still no quants in distribution warehouse
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_namur.lot_stock_id)), 0)
|
||||
|
||||
# Create the move for the shop Wavre. Should create a resupply from
|
||||
# distribution warehouse Wavre.
|
||||
picking_out_wavre = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop_wavre.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out_wavre.id,
|
||||
'location_id': warehouse_shop_wavre.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
'warehouse_id': warehouse_shop_wavre.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out_wavre.action_confirm()
|
||||
|
||||
# Validate the picking
|
||||
# Dist. warehouse Wavre -> transit Location -> Shop Wavre
|
||||
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_wavre.lot_stock_id.id)])
|
||||
self.assertTrue(picking_stock_transit)
|
||||
picking_stock_transit.action_assign()
|
||||
picking_stock_transit.move_lines[0].quantity_done = 1.0
|
||||
picking_stock_transit.action_done()
|
||||
|
||||
picking_transit_shop_wavre = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_wavre.lot_stock_id.id)])
|
||||
self.assertTrue(picking_transit_shop_wavre)
|
||||
picking_transit_shop_wavre.action_assign()
|
||||
picking_transit_shop_wavre.move_lines[0].quantity_done = 1.0
|
||||
picking_transit_shop_wavre.action_done()
|
||||
|
||||
picking_out_wavre.action_assign()
|
||||
picking_out_wavre.move_lines[0].quantity_done = 1.0
|
||||
picking_out_wavre.action_done()
|
||||
|
||||
# Check that the correct quantity has been provided to customer
|
||||
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 2)
|
||||
# Ensure there still no quants in distribution warehouse
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id)), 0)
|
||||
|
||||
class TestResupply(TestStockCommon):
|
||||
def setUp(self):
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
<field name="model">stock.location.route</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Route">
|
||||
<field name="name"/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
|
||||
@@ -52,12 +52,12 @@ class AccountChartTemplate(models.Model):
|
||||
'fields_id': field.id,
|
||||
'value': value,
|
||||
}
|
||||
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)])
|
||||
if properties:
|
||||
# the property exist: modify it
|
||||
properties.write(vals)
|
||||
else:
|
||||
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)], limit=1)
|
||||
if not properties:
|
||||
# create the property
|
||||
PropertyObj.create(vals)
|
||||
elif not properties.value_reference:
|
||||
# update the property if False
|
||||
properties.write(vals)
|
||||
|
||||
return res
|
||||
|
||||
@@ -201,45 +201,39 @@ class ProductProduct(models.Model):
|
||||
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
|
||||
|
||||
for product in self:
|
||||
if to_date:
|
||||
price_used = product.get_history_price(
|
||||
self.env.user.company_id.id,
|
||||
date=to_date,
|
||||
) if product.cost_method in ['standard', 'average'] else 0.0
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
|
||||
moves = StockMove.search(domain)
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
if product.cost_method == 'fifo':
|
||||
if product.cost_method in ['standard', 'average']:
|
||||
qty_available = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
price_used = product.standard_price
|
||||
if to_date:
|
||||
price_used = product.get_history_price(
|
||||
self.env.user.company_id.id,
|
||||
date=to_date,
|
||||
)
|
||||
product.stock_value = price_used * qty_available
|
||||
product.qty_at_date = qty_available
|
||||
elif product.cost_method == 'fifo':
|
||||
if to_date:
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
|
||||
moves = StockMove.search(domain)
|
||||
product.stock_value = sum(moves.mapped('value'))
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
|
||||
elif product.cost_method in ['standard', 'average']:
|
||||
product.stock_value = product.qty_at_date * price_used
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
product.qty_at_date = quantity
|
||||
if product.cost_method == 'fifo':
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
product.stock_value = value
|
||||
product.qty_at_date = quantity
|
||||
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
||||
elif product.cost_method in ['standard', 'average']:
|
||||
product.stock_value = quantity * price_used
|
||||
else:
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
else:
|
||||
product.stock_value, moves = product._sum_remaining_values()
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
if product.cost_method == 'fifo':
|
||||
product.stock_value, product.stock_fifo_manual_move_ids = product._sum_remaining_values()
|
||||
elif product.cost_method in ['standard', 'average']:
|
||||
product.stock_value = product.qty_at_date * product.standard_price
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
product.qty_at_date = quantity
|
||||
if product.cost_method == 'fifo':
|
||||
product.stock_value = value
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
product.stock_fifo_manual_move_ids = moves
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
||||
elif product.cost_method in ['standard', 'average']:
|
||||
product.stock_value = quantity * product.standard_price
|
||||
|
||||
def action_valuation_at_date_details(self):
|
||||
""" Returns an action with either a list view of all the valued stock moves of `self` if the
|
||||
|
||||
@@ -12,13 +12,14 @@ class TestStockValuation(AccountingTestCase):
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.partner_id = self.env.ref('base.res_partner_1')
|
||||
self.product1 = self.env.ref('product.product_product_8')
|
||||
self.categ_id = self.product1.categ_id
|
||||
|
||||
self.acc_payable = self.env['account.account'].search([('name', '=', 'Account Payable')]).id
|
||||
self.acc_expense = self.env['account.account'].search([('name', '=', 'Expenses')]).id
|
||||
self.acc_receivable = self.env['account.account'].search([('name', '=', 'Account Receivable')]).id
|
||||
self.acc_sale = self.env['account.account'].search([('name', '=', 'Product Sales')]).id
|
||||
self.acc_stock_in = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Received)')]).id
|
||||
self.acc_stock_out = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Delivered)')]).id
|
||||
self.acc_payable = self.partner_id.property_account_payable_id.id
|
||||
self.acc_expense = self.categ_id.property_account_expense_categ_id.id
|
||||
self.acc_receivable = self.partner_id.property_account_receivable_id.id
|
||||
self.acc_sale = self.categ_id.property_account_income_categ_id.id
|
||||
self.acc_stock_in = self.categ_id.property_stock_account_input_categ_id.id
|
||||
self.acc_stock_out = self.categ_id.property_stock_account_output_categ_id.id
|
||||
|
||||
def _dropship_product1(self):
|
||||
# enable the dropship and MTO route on the product
|
||||
|
||||
@@ -17,9 +17,7 @@ class TestStockLandedCostsRounding(TestStockLandedCostsCommon):
|
||||
# Define undivisible units
|
||||
product_uom_unit_round_1 = self.env.ref('uom.product_uom_unit')
|
||||
product_uom_unit_round_1.write({
|
||||
'category_id': self.ref('uom.product_uom_categ_unit'),
|
||||
'name': 'Undivisible Unit(s)',
|
||||
'factor': 1.0,
|
||||
'rounding': 1.0,
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hashlib
|
||||
from collections import OrderedDict
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.tools import pycompat
|
||||
@@ -57,6 +58,24 @@ class Image(models.AbstractModel):
|
||||
elif options.get('zoom'):
|
||||
src_zoom = options['zoom']
|
||||
|
||||
img = '<img class="%s" src="%s" style="%s"%s%s/>' % \
|
||||
(classes, src, options.get('style', ''), ' alt="%s"' % alt if alt else '', ' data-zoom="1" data-zoom-image="%s"' % src_zoom if src_zoom else '')
|
||||
return pycompat.to_text(img)
|
||||
atts = OrderedDict()
|
||||
atts["src"] = src
|
||||
atts["class"] = classes
|
||||
atts["style"] = options.get('style')
|
||||
atts["alt"] = alt
|
||||
atts["data-zoom"] = src_zoom and u'1' or None
|
||||
atts["data-zoom-image"] = src_zoom
|
||||
|
||||
atts = self.env['ir.qweb']._post_processing_att('img', atts, options.get('template_options'))
|
||||
|
||||
img = ['<img']
|
||||
for name, value in atts.items():
|
||||
if value:
|
||||
img.append(' ')
|
||||
img.append(escape(pycompat.to_text(name)))
|
||||
img.append('="')
|
||||
img.append(escape(pycompat.to_text(value)))
|
||||
img.append('"')
|
||||
img.append('/>')
|
||||
|
||||
return u''.join(img)
|
||||
|
||||
@@ -391,10 +391,13 @@ var ActionManager = Widget.extend({
|
||||
}
|
||||
|
||||
return this._startController(controller).then(function (controller) {
|
||||
var prevDialogOnClose;
|
||||
if (self.currentDialogController) {
|
||||
prevDialogOnClose = self.currentDialogController.onClose;
|
||||
self._closeDialog(true);
|
||||
}
|
||||
|
||||
controller.onClose = prevDialogOnClose || options.on_close;
|
||||
var dialog = new Dialog(self, _.defaults({}, options, {
|
||||
buttons: [],
|
||||
dialogClass: controller.className,
|
||||
@@ -405,7 +408,7 @@ var ActionManager = Widget.extend({
|
||||
self._removeAction(action.jsID);
|
||||
self.currentDialogController = null;
|
||||
if (silent !== true) {
|
||||
options.on_close();
|
||||
controller.onClose();
|
||||
}
|
||||
});
|
||||
controller.dialog = dialog;
|
||||
@@ -488,8 +491,9 @@ var ActionManager = Widget.extend({
|
||||
* @returns {Deferred} resolved immediately
|
||||
*/
|
||||
_executeCloseAction: function (action, options) {
|
||||
var result;
|
||||
if (!this.currentDialogController) {
|
||||
options.on_close();
|
||||
result = options.on_close();
|
||||
}
|
||||
|
||||
this._closeDialog();
|
||||
@@ -499,7 +503,7 @@ var ActionManager = Widget.extend({
|
||||
this.trigger_up('show_effect', action.effect);
|
||||
}
|
||||
|
||||
return $.when();
|
||||
return $.when(result);
|
||||
},
|
||||
/**
|
||||
* Executes actions of type 'ir.actions.server'.
|
||||
|
||||
@@ -2515,6 +2515,21 @@ var AceEditor = DebouncedField.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Format value
|
||||
*
|
||||
* Note: We have to overwrite this method to always return a string.
|
||||
* AceEditor works with string and not boolean value.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {boolean|string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
_formatValue: function (value) {
|
||||
return this._super.apply(this, arguments) || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
@@ -2537,6 +2552,7 @@ var AceEditor = DebouncedField.extend({
|
||||
this.aceSession.setValue(newValue);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the ace library on the given DOM element. This initializes the
|
||||
* ace editor option according to the edit/readonly mode and binds ace
|
||||
|
||||
@@ -41,7 +41,8 @@ var CrashManager = core.Class.extend({
|
||||
core.bus.trigger('connection_lost');
|
||||
this.connection_lost = true;
|
||||
var timeinterval = setInterval(function() {
|
||||
ajax.jsonRpc('/web/webclient/version_info').then(function() {
|
||||
var options = {shadow: true};
|
||||
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, options).then(function () {
|
||||
clearInterval(timeinterval);
|
||||
core.bus.trigger('connection_restored');
|
||||
self.connection_lost = false;
|
||||
|
||||
@@ -584,13 +584,16 @@ var BasicModel = AbstractModel.extend({
|
||||
/**
|
||||
* Returns true if a record can be abandoned.
|
||||
*
|
||||
* A record can be abandonned if it is a new record, except if
|
||||
* this datapoint has been specifically tagged as "do not abandon".
|
||||
* Case for not abandoning the record:
|
||||
*
|
||||
* Example:
|
||||
* 1. flagged as 'no abandon' (i.e. during a `default_get`, including any
|
||||
* `onchange` from a `default_get`)
|
||||
* 2. registered in a list on addition
|
||||
* 2.1. registered as non-new addition
|
||||
* 2.2. registered as new additon on update
|
||||
* 3. record is not new
|
||||
*
|
||||
* - Discard record from "Add an item" => "New" record => abandon
|
||||
* - Discard record from `default_get`/`onchange` => do not abandon
|
||||
* Otherwise, the record can be abandoned.
|
||||
*
|
||||
* This is useful when discarding changes on this record, as it means that
|
||||
* we must keep the record even if some fields are invalids (e.g. required
|
||||
@@ -600,7 +603,29 @@ var BasicModel = AbstractModel.extend({
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canBeAbandoned: function (id) {
|
||||
return !this.localData[id]._noAbandon && this.isNew(id);
|
||||
// 1. no drop if flagged
|
||||
if (this.localData[id]._noAbandon) {
|
||||
return false;
|
||||
}
|
||||
// 2. no drop in a list on "ADD in some cases
|
||||
var record = this.localData[id];
|
||||
var parent = this.localData[record.parentID];
|
||||
if (parent) {
|
||||
var entry = _.findWhere(parent._savePoint, {operation: 'ADD', id: id});
|
||||
if (entry) {
|
||||
// 2.1. no drop on non-new addition in list
|
||||
if (!entry.isNew) {
|
||||
return false;
|
||||
}
|
||||
// 2.2. no drop on new addition on "UPDATE"
|
||||
var lastEntry = _.last(parent._savePoint);
|
||||
if (lastEntry.operation === 'UPDATE' && lastEntry.id === id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. drop new records
|
||||
return this.isNew(id);
|
||||
},
|
||||
/**
|
||||
* Returns true if a record is dirty. A record is considered dirty if it has
|
||||
@@ -2571,6 +2596,11 @@ var BasicModel = AbstractModel.extend({
|
||||
relationField: field.relation_field,
|
||||
viewType: view ? view.type : fieldInfo.viewType,
|
||||
});
|
||||
// set existing changes to the list
|
||||
if (record._changes && record._changes[fieldName]) {
|
||||
list._changes = self.localData[record._changes[fieldName]]._changes;
|
||||
record._changes[fieldName] = list.id;
|
||||
}
|
||||
record.data[fieldName] = list.id;
|
||||
if (!fieldInfo.__no_fetch) {
|
||||
var def = self._readUngroupedList(list).then(function () {
|
||||
|
||||
@@ -634,7 +634,11 @@ var FormController = BasicController.extend({
|
||||
*/
|
||||
_onSave: function (ev) {
|
||||
ev.stopPropagation(); // Prevent x2m lines to be auto-saved
|
||||
this.saveRecord();
|
||||
var self = this;
|
||||
this._disableButtons();
|
||||
this.saveRecord().always(function () {
|
||||
self._enableButtons();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* This method is called when someone tries to sort a column, most likely
|
||||
|
||||
@@ -98,6 +98,9 @@ QUnit.module('ActionManager', {
|
||||
name: 'A Client Action',
|
||||
tag: 'ClientAction',
|
||||
type: 'ir.actions.client',
|
||||
}, {
|
||||
id: 10,
|
||||
type: 'ir.actions.act_window_close',
|
||||
}];
|
||||
|
||||
this.archs = {
|
||||
@@ -2416,6 +2419,51 @@ QUnit.module('ActionManager', {
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('requests for execute_action of type object: disable buttons', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var self = this;
|
||||
var def;
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/call_button') {
|
||||
return $.when(false);
|
||||
} else if (args.method === 'read') {
|
||||
// Block the 'read' call
|
||||
var result = this._super.apply(this, arguments);
|
||||
return $.when(def).then(_.constant(result));
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
actionManager.doAction(3);
|
||||
|
||||
// open a record in form view
|
||||
actionManager.$('.o_list_view .o_data_row:first').click();
|
||||
|
||||
// click on 'Call method' button (should call an Object method)
|
||||
def = $.Deferred();
|
||||
actionManager.$('.o_form_view button:contains(Call method)').click();
|
||||
|
||||
// Buttons should be disabled
|
||||
assert.strictEqual(
|
||||
actionManager.$('.o_form_view button:contains(Call method)').attr('disabled'),
|
||||
'disabled', 'buttons should be disabled')
|
||||
|
||||
// Release the 'read' call
|
||||
def.resolve();
|
||||
|
||||
// Buttons should be enabled after the reload
|
||||
assert.strictEqual(
|
||||
actionManager.$('.o_form_view button:contains(Call method)').attr('disabled'),
|
||||
undefined, 'buttons should be disabled')
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('can open different records from a multi record view', function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
@@ -3015,6 +3063,31 @@ QUnit.module('ActionManager', {
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('chained action on_close', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
function on_close() {
|
||||
assert.step('Close Action');
|
||||
};
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
actionManager.doAction(5, {on_close: on_close});
|
||||
|
||||
// a target=new action shouldn't activate the on_close
|
||||
actionManager.doAction(5);
|
||||
assert.verifySteps([]);
|
||||
|
||||
// An act_window_close should trigger the on_close
|
||||
actionManager.doAction(10);
|
||||
assert.verifySteps(['Close Action']);
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('footer buttons are moved to the dialog footer', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user