diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 976f6106b2f..32e90d115eb 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -327,6 +327,15 @@ class AccountJournal(models.Model):
if 'bank_acc_number' in vals:
for journal in self.filtered(lambda r: r.type == 'bank' and not r.bank_account_id):
journal.set_bank_account(vals.get('bank_acc_number'), vals.get('bank_id'))
+ # create the relevant refund sequence
+ if vals.get('refund_sequence'):
+ for journal in self.filtered(lambda j: j.type in ('sale', 'purchase') and not j.refund_sequence_id):
+ journal_vals = {
+ 'name': journal.name,
+ 'company_id': journal.company_id.id,
+ 'code': journal.code
+ }
+ journal.refund_sequence_id = self.sudo()._create_sequence(journal_vals, refund=True).id
return result
@@ -342,7 +351,7 @@ class AccountJournal(models.Model):
""" Create new no_gap entry sequence for every new Journal"""
prefix = self._get_sequence_prefix(vals['code'], refund)
seq = {
- 'name': vals['name'],
+ 'name': refund and vals['name'] + _(': Refund') or vals['name'],
'implementation': 'no_gap',
'prefix': prefix,
'padding': 4,
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 2b3b9a83e60..0915e4c9f90 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -560,7 +560,7 @@ class AccountBankStatementLine(models.Model):
domain_reconciliation = ['&', '&', ('statement_line_id', '=', False), ('account_id', 'in', reconciliation_aml_accounts), ('payment_id','<>', False)]
# Black lines = unreconciled & (not linked to a payment or open balance created by statement
- domain_matching = ['&', ('reconciled', '=', False), '|', ('payment_id','=',False), ('statement_id', '<>', False)]
+ domain_matching = [('reconciled', '=', False)]
if self.partner_id.id or overlook_partner:
domain_matching = expression.AND([domain_matching, [('account_id.internal_type', 'in', ['payable', 'receivable'])]])
else:
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 4503e6ce96f..50a6102b0c8 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -312,7 +312,7 @@ class AccountInvoice(models.Model):
default=lambda self: self.env.user)
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position',
readonly=True, states={'draft': [('readonly', False)]})
- commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity',
+ commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', compute_sudo=True,
related='partner_id.commercial_partner_id', store=True, readonly=True,
help="The commercial entity that will be used on Journal Entries for this invoice")
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index 3498b71fe7f..d45f61fe50d 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -307,6 +307,8 @@ class account_journal(models.Model):
[action] = self.env.ref('account.%s' % action_name).read()
action['context'] = ctx
action['domain'] = self._context.get('use_domain', [])
+ action['views'] = False
+ action['view_id'] = False
return action
@api.multi
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index 6d49b37728e..a87e906c074 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, api, _
+from odoo.exceptions import ValidationError
from datetime import timedelta
@@ -79,6 +80,14 @@ Best Regards,''')
@api.multi
def write(self, values):
+ #restrict the closing of FY if there are still unposted entries
+ if values.get('fiscalyear_lock_date'):
+ nb_draft_entries = self.env['account.move'].search([
+ ('company_id', 'in', [c.id for c in self]),
+ ('state', '=', 'draft'),
+ ('date', '<=', values['fiscalyear_lock_date'])])
+ if nb_draft_entries:
+ raise ValidationError(_('There are still unposted entries in the period you want to lock. You should either post or delete them.'))
# Reflect the change on accounts
for company in self:
digits = values.get('accounts_code_digits') or company.accounts_code_digits
diff --git a/addons/account/tests/test_account_move_closed_period.py b/addons/account/tests/test_account_move_closed_period.py
index 120b7cbb556..f0b700247f8 100644
--- a/addons/account/tests/test_account_move_closed_period.py
+++ b/addons/account/tests/test_account_move_closed_period.py
@@ -10,10 +10,15 @@ class TestPeriodState(AccountingTestCase):
def setUp(self):
super(TestPeriodState, self).setUp()
+ self.user_id = self.env.user
self.day_before_yesterday = datetime.now() - timedelta(2)
self.yesterday = datetime.now() - timedelta(1)
- self.user_id = self.env.user
- self.user_id.company_id.write({'fiscalyear_lock_date': self.yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT)})
+ self.yesterday_str = self.yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT)
+ #make sure there is no unposted entry
+ draft_entries = self.env['account.move'].search([('date', '<=', self.yesterday_str), ('state', '=', 'draft')])
+ if draft_entries:
+ draft_entries.post()
+ self.user_id.company_id.write({'fiscalyear_lock_date': self.yesterday_str})
self.sale_journal_id = self.env['account.journal'].search([('type', '=', 'sale')])[0]
self.account_id = self.env['account.account'].search([('internal_type', '=', 'receivable')])[0]
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 8fd2f020003..ab4a3c75587 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -261,7 +261,7 @@
-
+
diff --git a/addons/account/views/res_config_view.xml b/addons/account/views/res_config_view.xml
index fa2074c39a8..b7ee84bbdda 100644
--- a/addons/account/views/res_config_view.xml
+++ b/addons/account/views/res_config_view.xml
@@ -21,7 +21,6 @@
-
Fiscal Localization
@@ -33,13 +32,15 @@
Taxes, fiscal positions, chart of accounts & legal statements for your country
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -48,7 +49,6 @@
-
Taxes
diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py
index d6fb00358c9..4d6e3590037 100644
--- a/addons/account_asset/models/account_invoice.py
+++ b/addons/account_asset/models/account_invoice.py
@@ -80,8 +80,6 @@ class AccountInvoiceLine(models.Model):
@api.onchange('asset_category_id')
def onchange_asset_category_id(self):
- if not self.asset_category_id:
- self.account_id = self.get_invoice_line_account(self.invoice_id.type, self.product_id, self.invoice_id.fiscal_position_id, self.invoice_id.company_id)
if self.invoice_id.type == 'out_invoice' and self.asset_category_id:
self.account_id = self.asset_category_id.account_asset_id.id
elif self.invoice_id.type == 'in_invoice' and self.asset_category_id:
diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py
index 8ee9a40a11f..13b9e39cb22 100644
--- a/addons/account_check_printing/models/account_payment.py
+++ b/addons/account_check_printing/models/account_payment.py
@@ -123,7 +123,6 @@ class AccountPayment(models.Model):
}
else:
self.filtered(lambda r: r.state == 'draft').post()
- self.write({'state': 'sent'})
return self.do_print_checks()
@api.multi
diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index 804642ac8c2..0ddca4cc626 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -65,7 +65,9 @@ class AuthSignupHome(Home):
except Exception, e:
qcontext['error'] = e.message or e.name
- return request.render('auth_signup.reset_password', qcontext)
+ response = request.render('auth_signup.reset_password', qcontext)
+ response.headers['X-Frame-Options'] = 'DENY'
+ return response
def get_auth_signup_config(self):
"""retrieve the module config (which features are enabled) for the login page"""
diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js
index 3c5cb289232..0adff5f8e1b 100644
--- a/addons/barcodes/static/src/js/barcode_events.js
+++ b/addons/barcodes/static/src/js/barcode_events.js
@@ -113,6 +113,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
if (e.key === "ArrowLeft" || e.key === "ArrowRight" ||
e.key === "ArrowUp" || e.key === "ArrowDown" ||
e.key === "Escape" || e.key === "Tab" ||
+ e.key === "Backspace" || e.key === "Delete" ||
/F\d\d?/.test(e.key)) {
return true;
} else {
diff --git a/addons/barcodes/static/src/js/field_float_scannable.js b/addons/barcodes/static/src/js/field_float_scannable.js
index 79022362251..801cf32b5e2 100644
--- a/addons/barcodes/static/src/js/field_float_scannable.js
+++ b/addons/barcodes/static/src/js/field_float_scannable.js
@@ -23,6 +23,10 @@ var FieldFloatScannable = form_widgets.FieldFloat.extend({
},
simulateKeypress: function (e) {
+ /* only simulate a keypress if it has been previously prevented */
+ if (e.originalEvent.dispatched_by_barcode_reader !== true) {
+ return;
+ }
var character = String.fromCharCode(e.which);
var current_str = e.target.value;
var str_before_carret = current_str.substring(0, e.target.selectionStart);
diff --git a/addons/base_automation/data/base_automation_demo.xml b/addons/base_automation/data/base_automation_demo.xml
index d8bedc07020..246320ecf4a 100644
--- a/addons/base_automation/data/base_automation_demo.xml
+++ b/addons/base_automation/data/base_automation_demo.xml
@@ -35,6 +35,19 @@
[('customer', '=', True)]
+
+ Base Automation: test recursive rule
+
+ on_write
+
+ code
+
+record = model.browse(env.context['active_id'])
+if 'partner_id' in env.context['old_values'][record.id]:
+ record.write({'state': 'draft'})
+
+
+
Base Automation: test rule on secondary model
@@ -44,4 +57,18 @@
+
+
+ Base Automation: test rule on write check context
+
+ on_write
+
+ code
+
+record = model.browse(env.context['active_id'])
+if 'user_id' in env.context['old_values'][record.id]:
+ record.write({'is_assigned_to_admin': (record.user_id.id == 1)})
+
+
+
diff --git a/addons/base_automation/models/base_automation.py b/addons/base_automation/models/base_automation.py
index 24267f72821..af8363b92fa 100644
--- a/addons/base_automation/models/base_automation.py
+++ b/addons/base_automation/models/base_automation.py
@@ -156,13 +156,18 @@ class BaseAutomation(models.Model):
def _process(self, records):
""" Process action ``self`` on the ``records`` that have not been done yet. """
- # filter out the records on which self has already been done, then mark
- # remaining records as done (to avoid recursive processing)
+ # filter out the records on which self has already been done
action_done = self._context['__action_done']
- records -= action_done.setdefault(self, records.browse())
+ records_done = action_done.get(self, records.browse())
+ records -= records_done
if not records:
return
- action_done[self] |= records
+
+ # mark the remaining records as done (to avoid recursive processing)
+ action_done = dict(action_done)
+ action_done[self] = records_done + records
+ self = self.with_context(__action_done=action_done)
+ records = records.with_context(__action_done=action_done)
# modify records
values = {}
diff --git a/addons/base_automation/tests/test_base_automation.py b/addons/base_automation/tests/test_base_automation.py
index 9fddfad983d..2bea32ec57c 100644
--- a/addons/base_automation/tests/test_base_automation.py
+++ b/addons/base_automation/tests/test_base_automation.py
@@ -92,6 +92,30 @@ class base_automation_test(common.TransactionCase):
self.assertTrue(lead.customer, "Customer field should updated to True")
self.assertEqual(lead.user_id, self.user_demo, "Responsible should be change on write of Lead when Customer becomes True.")
+ def test_11_recomputed_field(self):
+ """
+ Check that a rule is executed whenever a field is recomputed and the
+ context contains the target field
+ """
+ partner = self.env.ref('base.res_partner_1')
+ lead = self.create_lead(state='draft', partner_id=partner.id)
+ self.assertFalse(lead.deadline, 'There should not be a deadline defined')
+ # change priority and user; this triggers deadline recomputation, and
+ # the server action should set the boolean field to True
+ lead.write({'priority': True, 'user_id': self.user_admin.id})
+ self.assertTrue(lead.deadline, 'Deadline should be defined')
+ self.assertTrue(lead.is_assigned_to_admin, 'Lead should be assigned to admin')
+
+ def test_12_recursive(self):
+ """ Check that a rule is executed recursively by a secondary change. """
+ lead = self.create_lead(state='open')
+ self.assertEqual(lead.state, 'open')
+ self.assertEqual(lead.user_id, self.user_admin)
+ # change partner; this should trigger the rule that modifies the state
+ partner = self.env.ref('base.res_partner_1')
+ lead.write({'partner_id': partner.id})
+ self.assertEqual(lead.state, 'draft')
+
def test_20_direct_line(self):
"""
Check that a rule is executed after creating a line record.
diff --git a/addons/base_automation/tests/test_models.py b/addons/base_automation/tests/test_models.py
index 36893e37447..fd2255dff6f 100644
--- a/addons/base_automation/tests/test_models.py
+++ b/addons/base_automation/tests/test_models.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import fields, models
+from dateutil import relativedelta
+from odoo import fields, models, api
class LeadTest(models.Model):
@@ -19,6 +20,17 @@ class LeadTest(models.Model):
customer = fields.Boolean(related='partner_id.customer', readonly=True, store=True)
line_ids = fields.One2many('base.automation.line.test', 'lead_id')
+ priority = fields.Boolean()
+ deadline = fields.Boolean(compute='_compute_deadline', store=True)
+ is_assigned_to_admin = fields.Boolean(string='Assigned to admin user')
+
+ @api.depends('priority')
+ def _compute_deadline(self):
+ for record in self:
+ if not record.priority:
+ record.deadline = False
+ else:
+ record.deadline = fields.Datetime.from_string(record.create_date) + relativedelta.relativedelta(days=3)
class LineTest(models.Model):
_name = "base.automation.line.test"
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 2c3fa12fa5e..c543f5ea830 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -1066,7 +1066,7 @@ class Meeting(models.Model):
if [True for item in new_pile if not item]:
continue
- result_data.append(meeting.get_search_fields(order_fields, r_date=r_date))
+ result_data.append(meeting.get_search_fields(order_fields, r_date=r_start_date))
if order_fields:
uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
diff --git a/addons/calendar/tests/test_calendar_recurrent_event_case2.py b/addons/calendar/tests/test_calendar_recurrent_event_case2.py
index e05428593ae..892c0ffe2aa 100644
--- a/addons/calendar/tests/test_calendar_recurrent_event_case2.py
+++ b/addons/calendar/tests/test_calendar_recurrent_event_case2.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
+from odoo.addons.calendar.models.calendar import calendar_id2real_id
class TestRecurrentEvent(common.TransactionCase):
@@ -145,3 +146,26 @@ class TestRecurrentEvent(common.TransactionCase):
('start', '>=', '2017-06-30 08:00:00'), ('name', '=', 'Review code with programmer')
])
self.assertEqual(meetings_count, 1, "Last recurrent weekly meetings are not found without stop filter !")
+
+ def test_recurrent_meeting5(self):
+ # I create a recurrent event and I check if the virtual_id are correct
+ self.CalendarEvent.create({
+ 'count': 5,
+ 'start': '2012-04-13 11:00:00',
+ 'stop': '2012-04-13 12:00:00',
+ 'duration': 1.0,
+ 'name': 'Test Meeting',
+ 'recurrency': True,
+ 'rrule_type': 'daily'
+ })
+ # I search for the first recurrent meeting
+ meeting = self.CalendarEvent.with_context({'virtual_id': True}).search([
+ ('start', '=', '2012-04-13 11:00:00'), ('stop', '=', '2012-04-13 12:00:00')
+ ])
+ virutal_dates = calendar_id2real_id(meeting.id, with_date=True)
+
+ # virtual_dates are used by the calendar view and I check if the start date for the first virtual event is correct.
+ self.assertEqual(virutal_dates[1], '2012-04-13 11:00:00', "The virtual event doesn't have the correct start date !")
+
+ # virtual_dates are used by the calendar view and I check if the stop date for the first virtual event is correct.
+ self.assertEqual(virutal_dates[2], '2012-04-13 12:00:00', "The virtual event doesn't have the correct stop date !")
diff --git a/addons/crm/data/crm_data.xml b/addons/crm/data/crm_data.xml
index 1f27dcb6256..ddfb2fe40ae 100644
--- a/addons/crm/data/crm_data.xml
+++ b/addons/crm/data/crm_data.xml
@@ -10,7 +10,7 @@
- info
+
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 50b20b94fd8..df134ff6c4b 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -1044,6 +1044,15 @@ class Lead(models.Model):
view_id = super(Lead, self).get_formview_id()
return view_id
+ @api.multi
+ def message_get_default_recipients(self):
+ return {
+ r.id : {'partner_ids': [],
+ 'email_to': r.email_from,
+ 'email_cc': False}
+ for r in self.sudo()
+ }
+
@api.multi
def message_get_suggested_recipients(self):
recipients = super(Lead, self).message_get_suggested_recipients()
diff --git a/addons/crm/models/res_config.py b/addons/crm/models/res_config.py
index a786f1e2c0b..b3bed2ba043 100644
--- a/addons/crm/models/res_config.py
+++ b/addons/crm/models/res_config.py
@@ -11,7 +11,8 @@ class CRMSettings(models.TransientModel):
help="Odoo will generate an email alias based on the sales channel name")
alias_prefix = fields.Char('Default Alias Name for Leads')
alias_domain = fields.Char('Alias Domain', default=lambda self: self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain"))
- default_generate_lead_from_alias = fields.Boolean()
+ # TDE TODO: as it is not linked to ir.values anymore, it should be renamed, but not in stable
+ default_generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails')
group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead')
module_crm_voip = fields.Boolean("Asterisk (VoIP)")
@@ -40,6 +41,26 @@ class CRMSettings(models.TransientModel):
IrValues = IrValues.sudo()
IrValues.set_default('sale.config.settings', 'generate_sales_team_alias', self.generate_sales_team_alias)
+ @api.model
+ def get_default_default_generate_lead_from_alias(self, fields):
+ return {
+ 'default_generate_lead_from_alias': self.env['ir.config_parameter'].sudo().get_param('sale_config_settings.default_generate_lead_from_alias')
+ }
+
+ @api.multi
+ def set_default_default_generate_lead_from_alias(self):
+ self.env['ir.config_parameter'].sudo().set_param('sale_config_settings.default_generate_lead_from_alias', self.default_generate_lead_from_alias)
+
+ @api.onchange('group_use_lead')
+ def _onchange_group_use_lead(self):
+ """ Reset alias / leads configuration if leads are not used """
+ if not self.group_use_lead:
+ self.default_generate_lead_from_alias = False
+
+ @api.onchange('default_generate_lead_from_alias')
+ def _onchange_default_generate_lead_from_alias(self):
+ self.alias_prefix = 'info' if self.default_generate_lead_from_alias else False
+
@api.model
def get_default_alias_prefix(self, fields):
alias = self._find_default_lead_alias_id()
diff --git a/addons/crm/views/crm_team_views.xml b/addons/crm/views/crm_team_views.xml
index 7b007b8a531..23698ecc36c 100644
--- a/addons/crm/views/crm_team_views.xml
+++ b/addons/crm/views/crm_team_views.xml
@@ -242,4 +242,8 @@
+
+ ['|', ('res_model_id', '=', False), ('res_model_id.model', 'in', ['crm.lead', 'res.partner'])]
+
+
diff --git a/addons/crm/views/res_config_views.xml b/addons/crm/views/res_config_views.xml
index 69df0c5a712..745b5e2b5ed 100644
--- a/addons/crm/views/res_config_views.xml
+++ b/addons/crm/views/res_config_views.xml
@@ -20,19 +20,24 @@
-
+
-
+
- Generate leads from incoming emails
+ Generate unassigned leads from incoming emails. Leads coming from
+ that email will be assigned manually.
-
+ @
diff --git a/addons/hr_attendance/models/hr_attendance.py b/addons/hr_attendance/models/hr_attendance.py
index 9ad4ef499e7..c8728ec0e31 100644
--- a/addons/hr_attendance/models/hr_attendance.py
+++ b/addons/hr_attendance/models/hr_attendance.py
@@ -26,15 +26,15 @@ class HrAttendance(models.Model):
result = []
for attendance in self:
if not attendance.check_out:
- result.append((self.id, _("%(empl_name)s from %(check_in)s") % {
- 'empl_name': self.employee_id.name,
- 'check_in': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_in))),
+ result.append((attendance.id, _("%(empl_name)s from %(check_in)s") % {
+ 'empl_name': attendance.employee_id.name,
+ 'check_in': fields.Datetime.to_string(fields.Datetime.context_timestamp(attendance, fields.Datetime.from_string(attendance.check_in))),
}))
else:
- result.append((self.id, _("%(empl_name)s from %(check_in)s to %(check_out)s") % {
- 'empl_name': self.employee_id.name,
- 'check_in': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_in))),
- 'check_out': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_out))),
+ result.append((attendance.id, _("%(empl_name)s from %(check_in)s to %(check_out)s") % {
+ 'empl_name': attendance.employee_id.name,
+ 'check_in': fields.Datetime.to_string(fields.Datetime.context_timestamp(attendance, fields.Datetime.from_string(attendance.check_in))),
+ 'check_out': fields.Datetime.to_string(fields.Datetime.context_timestamp(attendance, fields.Datetime.from_string(attendance.check_out))),
}))
return result
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index cb36a790ecf..b6e25c34f51 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -172,19 +172,24 @@ class HrExpense(models.Model):
'''
main function that is called when trying to create the accounting entries related to an expense
'''
+ move_group_by_sheet = {}
for expense in self:
journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
#create the move that will contain the accounting entries
acc_date = expense.sheet_id.accounting_date or expense.date
- move = self.env['account.move'].create({
- 'journal_id': journal.id,
- 'company_id': self.env.user.company_id.id,
- 'date': acc_date,
- 'ref': expense.sheet_id.name,
- # force the name to the default value, to avoid an eventual 'default_name' in the context
- # to set it to '' which cause no number to be given to the account.move when posted.
- 'name': '/',
- })
+ if not expense.sheet_id.id in move_group_by_sheet:
+ move = self.env['account.move'].create({
+ 'journal_id': journal.id,
+ 'company_id': self.env.user.company_id.id,
+ 'date': acc_date,
+ 'ref': expense.sheet_id.name,
+ # force the name to the default value, to avoid an eventual 'default_name' in the context
+ # to set it to '' which cause no number to be given to the account.move when posted.
+ 'name': '/',
+ })
+ move_group_by_sheet[expense.sheet_id.id] = move
+ else:
+ move = move_group_by_sheet[expense.sheet_id.id]
company_currency = expense.company_id.currency_id
diff_currency_p = expense.currency_id != company_currency
#one account.move.line per expense (+taxes..)
@@ -235,9 +240,10 @@ class HrExpense(models.Model):
lines = map(lambda x: (0, 0, expense._prepare_move_line(x)), move_lines)
move.with_context(dont_create_taxes=True).write({'line_ids': lines})
expense.sheet_id.write({'account_move_id': move.id})
- move.post()
if expense.payment_mode == 'company_account':
expense.sheet_id.paid_expense_sheets()
+ for move in move_group_by_sheet.values():
+ move.post()
return True
@api.multi
@@ -272,7 +278,7 @@ class HrExpense(models.Model):
# Calculate tax lines and adjust base line
taxes = expense.tax_ids.compute_all(expense.unit_amount, expense.currency_id, expense.quantity, expense.product_id)
account_move[-1]['price'] = taxes['total_excluded']
- account_move[-1]['tax_ids'] = expense.tax_ids.ids
+ account_move[-1]['tax_ids'] = [(6, 0, expense.tax_ids.ids)]
for tax in taxes['taxes']:
account_move.append({
'type': 'tax',
@@ -511,14 +517,14 @@ class HrExpenseSheet(models.Model):
if any(not sheet.journal_id for sheet in self):
raise UserError(_("Expenses must have an expense journal specified to generate accounting entries."))
- res = self.mapped('expense_line_ids')\
- .filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.user.company_id.currency_id).rounding))\
- .action_move_create()
+ expense_line_ids = self.mapped('expense_line_ids')\
+ .filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.user.company_id.currency_id).rounding))
+ res = expense_line_ids.action_move_create()
if not self.accounting_date:
self.accounting_date = self.account_move_id.date
- if self.payment_mode=='own_account':
+ if self.payment_mode == 'own_account' and expense_line_ids:
self.write({'state': 'post'})
else:
self.write({'state': 'done'})
diff --git a/addons/hr_holidays/models/hr_holidays.py b/addons/hr_holidays/models/hr_holidays.py
index 1569a91a73a..61af9bd1dbb 100644
--- a/addons/hr_holidays/models/hr_holidays.py
+++ b/addons/hr_holidays/models/hr_holidays.py
@@ -243,6 +243,7 @@ class Holidays(models.Model):
('date_to', '>=', holiday.date_from),
('employee_id', '=', holiday.employee_id.id),
('id', '!=', holiday.id),
+ ('type', '=', holiday.type),
('state', 'not in', ['cancel', 'refuse']),
]
nholidays = self.search_count(domain)
diff --git a/addons/hr_maintenance/views/maintenance_views.xml b/addons/hr_maintenance/views/maintenance_views.xml
index 58dec50bc2f..2c56a9f2538 100644
--- a/addons/hr_maintenance/views/maintenance_views.xml
+++ b/addons/hr_maintenance/views/maintenance_views.xml
@@ -79,6 +79,12 @@
+
+
+
+
+
+
diff --git a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml
index ba48dceb8f2..a307bd47ddd 100644
--- a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml
+++ b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml
@@ -29,6 +29,8 @@
+
+
diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py
index 09ba008d430..f43e585fcba 100644
--- a/addons/hw_posbox_homepage/controllers/main.py
+++ b/addons/hw_posbox_homepage/controllers/main.py
@@ -56,7 +56,10 @@ index_template = """
If you need to grant remote debugging access to a developer, you can do it here.
- The PosBox software installed on this posbox is version 15,
+ If you need to display the current customer basket on another device, you can do it here.
+
+
+ The PosBox software installed on this posbox is version 16,
the posbox version number is independent from Odoo. You can upgrade
the software on the upgrade page.
diff --git a/addons/hw_screen/__init__.py b/addons/hw_screen/__init__.py
new file mode 100644
index 00000000000..bf7fb7b45c1
--- /dev/null
+++ b/addons/hw_screen/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import controllers
diff --git a/addons/hw_screen/__openerp__.py b/addons/hw_screen/__openerp__.py
new file mode 100644
index 00000000000..8ed5711b836
--- /dev/null
+++ b/addons/hw_screen/__openerp__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': 'Screen Driver',
+ 'version': '1.0',
+ 'category': 'Hardware Drivers',
+ 'sequence': 6,
+ 'summary': 'Provides support for customer facing displays',
+ 'website': 'https://www.odoo.com/page/point-of-sale',
+ 'description': """
+Screen Driver
+=============
+
+This module allows the POS client to send rendered HTML to a remotely
+installed screen. This module then displays this HTML using a web
+browser.
+""",
+ 'depends': ['hw_proxy'],
+ 'installable': False,
+ 'auto_install': False,
+}
diff --git a/addons/hw_screen/controllers/__init__.py b/addons/hw_screen/controllers/__init__.py
new file mode 100644
index 00000000000..f48716d3a69
--- /dev/null
+++ b/addons/hw_screen/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import main
diff --git a/addons/hw_screen/controllers/main.py b/addons/hw_screen/controllers/main.py
new file mode 100644
index 00000000000..f6101e7c8ca
--- /dev/null
+++ b/addons/hw_screen/controllers/main.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import http
+from odoo.tools import config
+from odoo.addons.web.controllers import main as web
+
+import logging
+import netifaces as ni
+import os
+from subprocess import call
+import time
+import threading
+
+self_port = str(config['xmlrpc_port'] or 8069)
+
+_logger = logging.getLogger(__name__)
+
+
+class HardwareScreen(web.Home):
+
+ event_data = threading.Event()
+ pos_client_data = {'rendered_html': False,
+ 'ip_from': False}
+ display_in_use = ''
+ failure_count = {}
+
+ def _call_xdotools(self, keystroke):
+ os.environ['DISPLAY'] = ":0.0"
+ os.environ['XAUTHORITY'] = "/run/lightdm/pi/xauthority"
+ try:
+ call(['xdotool', 'key', keystroke])
+ return "xdotool succeeded in stroking " + keystroke
+ except:
+ return "xdotool threw an error, maybe it is not installed on the posbox"
+
+ @http.route('/hw_proxy/display_refresh', type='json', auth='none', cors='*')
+ def display_refresh(self):
+ return self._call_xdotools('F5')
+
+ # POS CASHIER'S ROUTES
+ @http.route('/hw_proxy/customer_facing_display', type='json', auth='none', cors='*')
+ def update_user_facing_display(self, html=None):
+ request_ip = http.request.httprequest.remote_addr
+ if request_ip == HardwareScreen.pos_client_data.get('ip_from', ''):
+ HardwareScreen.pos_client_data['rendered_html'] = html
+ HardwareScreen.event_data.set()
+
+ return {'status': 'updated'}
+ else:
+ return {'status': 'failed'}
+
+ @http.route('/hw_proxy/take_control', type='json', auth='none', cors='*')
+ def take_control(self, html=None):
+ # ALLOW A CASHIER TO TAKE CONTROL OVER THE POSBOX, IN CASE OF MULTIPLE CASHIER PER POSBOX
+ HardwareScreen.pos_client_data['rendered_html'] = html
+ HardwareScreen.pos_client_data['ip_from'] = http.request.httprequest.remote_addr
+ HardwareScreen.event_data.set()
+
+ return {'status': 'success',
+ 'message': 'You now have access to the display'}
+
+ @http.route('/hw_proxy/test_ownership', type='json', auth='none', cors='*')
+ def test_ownership(self):
+ if HardwareScreen.pos_client_data.get('ip_from') == http.request.httprequest.remote_addr:
+ return {'status': 'OWNER'}
+ else:
+ return {'status': 'NOWNER'}
+
+ # POSBOX ROUTES (SELF)
+ @http.route('/point_of_sale/display', type='http', auth='none')
+ def render_main_display(self):
+ return self._get_html()
+
+ @http.route('/point_of_sale/get_serialized_order', type='json', auth='none')
+ def get_serialized_order(self):
+ request_addr = http.request.httprequest.remote_addr
+ result = HardwareScreen.pos_client_data
+ if HardwareScreen.display_in_use and request_addr != HardwareScreen.display_in_use:
+ if not HardwareScreen.failure_count.get(request_addr):
+ HardwareScreen.failure_count[request_addr] = 0
+ if HardwareScreen.failure_count[request_addr] > 0:
+ time.sleep(10)
+ HardwareScreen.failure_count[request_addr] += 1
+ return {'rendered_html': """
Not Authorized. Another browser is in use to display for the client. Please refresh.
""",
+ 'stop_longpolling': True,
+ 'ip_from': request_addr}
+
+ # IMPLEMENTATION OF LONGPOLLING
+ # Times out 2 seconds before the JS request does
+ if HardwareScreen.event_data.wait(28):
+ HardwareScreen.event_data.clear()
+ HardwareScreen.failure_count[request_addr] = 0
+ return result
+ return {'rendered_html': False,
+ 'ip_from': HardwareScreen.pos_client_data['ip_from']}
+
+ def _get_html(self):
+ cust_js = None
+ interfaces = ni.interfaces()
+ my_ip = '127.0.0.1'
+ HardwareScreen.display_in_use = http.request.httprequest.remote_addr
+
+ with open(os.path.join(os.path.dirname(__file__), "../static/src/js/worker.js")) as js:
+ cust_js = js.read()
+
+ with open(os.path.join(os.path.dirname(__file__), "../static/src/css/cust_css.css")) as css:
+ cust_css = css.read()
+
+ display_ifaces = ""
+ for iface_id in interfaces:
+ iface_obj = ni.ifaddresses(iface_id)
+ ifconfigs = iface_obj.get(ni.AF_INET, [])
+ for conf in ifconfigs:
+ if conf.get('addr'):
+ display_ifaces += "
" + iface_id + "
"
+ display_ifaces += "
" + conf.get('addr') + "
"
+ display_ifaces += "
" + conf.get('netmask') + "
"
+ # What is my external IP ?
+ if iface_id != 'lo':
+ my_ip = conf.get('addr')
+
+ my_ip_port = my_ip + ":" + self_port
+
+ html = """
+
+
+
+ Odoo -- Point of Sale
+
+
+
+
+
+
+
+
+
+
+
+
+
Odoo Point of Sale
+
POSBox Client display
+
My IPs
+
+
+
Interface
+
IP
+
Netmask
+
+ """ + display_ifaces + """
+
+
The customer cart will be displayed here once a Point of Sale session is started.
+
Odoo version 11 or above is required.
+
+
+
+
+
+ """
+ return html
diff --git a/addons/hw_screen/static/src/css/cust_css.css b/addons/hw_screen/static/src/css/cust_css.css
new file mode 100644
index 00000000000..69fddb25457
--- /dev/null
+++ b/addons/hw_screen/static/src/css/cust_css.css
@@ -0,0 +1,21 @@
+html {
+ width: 100%;
+ height: 100%;
+ font-size: 11px;
+}
+
+body {
+ width: 100%;
+ height: 100%;
+ font-size: 14px;
+ margin: 0;
+}
+
+.original_body {
+ background-color: #797083;
+ color: white;
+}
+
+.ajax_got_body {
+ color: black;
+}
\ No newline at end of file
diff --git a/addons/hw_screen/static/src/js/worker.js b/addons/hw_screen/static/src/js/worker.js
new file mode 100644
index 00000000000..55e389e78ab
--- /dev/null
+++ b/addons/hw_screen/static/src/js/worker.js
@@ -0,0 +1,60 @@
+ $(function() {
+ "use strict";
+ // mergedHead will be turned to true the first time we receive something from a new host
+ // It allows to transform the only once
+ var mergedHead = false;
+ var current_client_url = "";
+ var stop_longpolling = false;
+
+ function longpolling() {
+ $.ajax({
+ type: 'POST',
+ url: 'http://'+window.location.host+'/point_of_sale/get_serialized_order',
+ dataType: 'json',
+ beforeSend: function(xhr){xhr.setRequestHeader('Content-Type', 'application/json');},
+ data: JSON.stringify({jsonrpc: '2.0'}),
+
+ success: function(data) {
+ if (typeof data.result.stop_longpolling !== 'undefined') {
+ stop_longpolling = data.result.stop_longpolling;
+ }
+ if (data.result.ip_from && data.result.rendered_html) {
+ var trimmed = $.trim(data.result.rendered_html);
+ var $parsedHTML = $('