[MERGE] forward port branch 12.0 up to f854e01a98

This commit is contained in:
Christophe Simonis
2019-01-18 14:26:33 +01:00
100 changed files with 1640 additions and 698 deletions
+4 -4
View File
@@ -772,12 +772,12 @@ class AccountInvoice(models.Model):
msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
if type in ('out_invoice', 'out_refund'):
account_id = rec_account.id
payment_term_id = p.property_payment_term_id.id
else:
if type in ('in_invoice', 'in_refund'):
account_id = pay_account.id
payment_term_id = p.property_supplier_payment_term_id.id
else:
account_id = rec_account.id
payment_term_id = p.property_payment_term_id.id
delivery_partner_id = self.get_delivery_partner_id()
fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=delivery_partner_id)
@@ -28,7 +28,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
ctx = self._context
periods = {}
date_from = fields.Date.from_string(date_from)
start = date_from - relativedelta(days=1)
start = date_from
for i in range(5)[::-1]:
stop = start - relativedelta(days=period_length)
period_name = str((5-(i+1)) * period_length + 1) + '-' + str((5-i) * period_length)
@@ -292,6 +292,10 @@
overflow: auto;
border: 1px solid #ddd;
border-radius: 0 0 5px 5px;
> div.quick_add > .o_reconcile_models {
max-height: 54px;
overflow: auto;
}
}
.quick_add {
margin-bottom: 7px;
@@ -360,4 +364,4 @@
}
}
}
}
}
@@ -83,7 +83,6 @@
view_type="form"
context="{
'default_template_id': ref('account.email_template_edi_invoice'),
'default_is_print': False,
'mark_invoice_as_sent': True,
}"
groups="account.group_account_invoice"/>
@@ -77,11 +77,11 @@ class PaymentPortal(http.Controller):
vals = {
'payment_token_id': token.id,
'type': 'server2server',
'return_url': success_url,
'return_url': _build_url_w_params(success_url, params),
}
tx = invoice_sudo._create_payment_transaction(vals)
PaymentProcessing.add_payment_transaction(tx)
params['success'] = 'pay_invoice'
return request.redirect(_build_url_w_params(success_url, params))
return request.redirect('/payment/process')
+4
View File
@@ -1205,6 +1205,10 @@ class Lead(models.Model):
if msg_dict.get('priority') in dict(crm_stage.AVAILABLE_PRIORITIES):
defaults['priority'] = msg_dict.get('priority')
defaults.update(custom_values)
# assign right company
if 'company_id' not in defaults and 'team_id' in defaults:
defaults['company_id'] = self.env['crm.team'].browse(defaults['team_id']).company_id.id
return super(Lead, self).message_new(msg_dict, custom_values=defaults)
def _message_post_after_hook(self, message, *args, **kwargs):
@@ -41,3 +41,95 @@ class NewLeadNotification(TestCrmCases):
# The user should have a new unread message
lead_user = lead.sudo(self.crm_salesman)
self.assertTrue(lead_user.message_needaction)
def test_new_lead_from_email_multicompany(self):
company0 = self.env.user.company_id
company1 = self.env['res.company'].create({'name': 'new_company'})
self.env.user.write({
'company_ids': [(4, company0.id, False), (4, company1.id, False)],
})
crm_team_model = self.env['ir.model'].search([('model', '=', 'crm.team')])
crm_lead_model = self.env['ir.model'].search([('model', '=', 'crm.lead')])
self.env["ir.config_parameter"].sudo().set_param("mail.catchall.domain", 'aqualung.com')
crm_team0 = self.env['crm.team'].create({
'name': 'crm team 0',
'company_id': company0.id,
})
crm_team1 = self.env['crm.team'].create({
'name': 'crm team 1',
'company_id': company1.id,
})
mail_alias0 = self.env['mail.alias'].create({
'alias_name': 'sale_team_0',
'alias_model_id': crm_lead_model.id,
'alias_parent_model_id': crm_team_model.id,
'alias_parent_thread_id': crm_team0.id,
'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team0.id,
})
mail_alias1 = self.env['mail.alias'].create({
'alias_name': 'sale_team_1',
'alias_model_id': crm_lead_model.id,
'alias_parent_model_id': crm_team_model.id,
'alias_parent_thread_id': crm_team1.id,
'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team1.id,
})
crm_team0.write({'alias_id': mail_alias0.id})
crm_team1.write({'alias_id': mail_alias1.id})
new_message0 = """MIME-Version: 1.0
Date: Thu, 27 Dec 2018 16:27:45 +0100
Message-ID: blablabla0
Subject: sale team 0 in company 0
From: A client <client_a@someprovider.com>
To: sale_team_0@aqualung.com
Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
--000000000000a47519057e029630
Content-Type: text/plain; charset="UTF-8"
--000000000000a47519057e029630
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<div>A good message</div>
--000000000000a47519057e029630--
"""
new_message1 = """MIME-Version: 1.0
Date: Thu, 27 Dec 2018 16:27:45 +0100
Message-ID: blablabla1
Subject: sale team 1 in company 1
From: B client <client_b@someprovider.com>
To: sale_team_1@aqualung.com
Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
--000000000000a47519057e029630
Content-Type: text/plain; charset="UTF-8"
--000000000000a47519057e029630
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<div>A good message bis</div>
--000000000000a47519057e029630--
"""
crm_lead0_id = self.env['mail.thread'].message_process('crm.lead', new_message0)
crm_lead1_id = self.env['mail.thread'].message_process('crm.lead', new_message1)
crm_lead0 = self.env['crm.lead'].browse(crm_lead0_id)
crm_lead1 = self.env['crm.lead'].browse(crm_lead1_id)
self.assertEqual(crm_lead0.team_id, crm_team0)
self.assertEqual(crm_lead1.team_id, crm_team1)
self.assertEqual(crm_lead0.company_id, company0)
self.assertEqual(crm_lead1.company_id, company1)
+9 -1
View File
@@ -12,8 +12,16 @@ class SaleOrder(models.Model):
for so in self:
# confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
so.order_line._update_registrations(confirm=so.amount_total == 0, cancel_to_draft=False)
return res
@api.multi
def action_confirm(self):
res = super(SaleOrder, self).action_confirm()
for so in self:
if any(so.order_line.filtered(lambda line: line.event_id)):
return self.env['ir.actions.act_window'].with_context(default_sale_order_id=so.id).for_xml_id('event_sale', 'action_sale_order_event_registration')
return self.env['ir.actions.act_window'] \
.with_context(default_sale_order_id=so.id) \
.for_xml_id('event_sale', 'action_sale_order_event_registration')
return res
@@ -12,7 +12,7 @@
<tree string="Registration" editable="top" create="false" delete="false">
<field name="event_id" readonly='1' force_save="1"/>
<field name="registration_id" readonly='1' force_save="1"/>
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1'/>
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1' force_save="1"/>
<field name="name"/>
<field name="email"/>
<field name="phone"/>
+1 -1
View File
@@ -427,7 +427,7 @@ class HolidaysRequest(models.Model):
employee = self.env['hr.employee'].browse(employee_id)
return employee.get_work_days_data(date_from, date_to)['days']
return self.env.user.company_id.resource_calendar_id.get_work_hours_count() / HOURS_PER_DAY
return self.env.user.company_id.resource_calendar_id.get_work_hours_count(date_from, date_to) / HOURS_PER_DAY
####################################################
# ORM Overrides methods
+20 -5
View File
@@ -8,6 +8,7 @@ import logging
from odoo import api, fields, models
from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.tools.translate import _
from odoo.tools.float_utils import float_round
@@ -207,9 +208,18 @@ class HolidaysType(models.Model):
@api.multi
def _compute_group_days_allocation(self):
domain = [
('holiday_status_id', 'in', self.ids),
('holiday_type', '!=', 'employee'),
('state', '=', 'validate'),
]
domain2 = [
'|',
('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))),
('date_from', '=', False),
]
grouped_res = self.env['hr.leave.allocation'].read_group(
[('holiday_status_id', 'in', self.ids), ('holiday_type', '!=', 'employee'), ('state', '=', 'validate'),
('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)))],
expression.AND([domain, domain2]),
['holiday_status_id', 'number_of_days'],
['holiday_status_id'],
)
@@ -270,11 +280,16 @@ class HolidaysType(models.Model):
def action_see_days_allocated(self):
self.ensure_one()
action = self.env.ref('hr_holidays.hr_leave_allocation_action_all').read()[0]
action['domain'] = [
domain = [
('holiday_status_id', 'in', self.ids),
('holiday_type', '!=', 'employee'),
('holiday_status_id', '=', self.ids[0]),
('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)))
]
domain2 = [
'|',
('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))),
('date_from', '=', False),
]
action['domain'] = expression.AND([domain, domain2])
action['context'] = {
'default_holiday_type': 'department',
'default_holiday_status_id': self.ids[0],
@@ -104,6 +104,7 @@
<div class="o_media_left">
<a href="#"
t-att-data-employee-id="self.id"
t-att-data-employee-name="self.name"
class="o_org_chart_show_more text-center o_employee_sub_redirect">&#8230;</a>
</div>
</div>
+45 -1
View File
@@ -81,7 +81,7 @@
You can not log timesheets on this project since is linked to an inactive analytic account. Please change it, or reactivate the current one to timesheet on the project.
</div>
</group>
<field name="timesheet_ids" attrs="{'invisible': [('analytic_account_active', '=', False)]}" context="{'default_project_id': project_id, 'default_name':''}">
<field name="timesheet_ids" mode="tree,kanban" attrs="{'invisible': [('analytic_account_active', '=', False)]}" context="{'default_project_id': project_id, 'default_name':''}">
<tree editable="bottom" string="Timesheet Activities" default_order="date">
<field name="date"/>
<field name="user_id" invisible="1"/>
@@ -90,6 +90,50 @@
<field name="unit_amount" widget="timesheet_uom"/>
<field name="project_id" invisible="1"/>
</tree>
<kanban class="o_kanban_mobile">
<field name="date"/>
<field name="user_id"/>
<field name="employee_id"/>
<field name="name"/>
<field name="unit_amount"/>
<field name="project_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="row">
<div class="col-xs-6">
<strong><span><t t-esc="record.employee_id.value"/></span></strong>
</div>
<div class="col-xs-6 pull-right text-right">
<strong><t t-esc="record.date.value"/></strong>
</div>
</div>
<div class="row">
<div class="col-xs-6 text-muted">
<span><t t-esc="record.name.value"/></span>
</div>
<div class="col-xs-6">
<span class="pull-right text-right">
<field name="unit_amount" widget="float_time"/>
</span>
</div>
</div>
</div>
</t>
</templates>
</kanban>
<form string="Timesheet Activities">
<sheet>
<group>
<field name="date"/>
<field name="user_id" invisible="1"/>
<field name="employee_id" required="1"/>
<field name="name"/>
<field name="unit_amount" string="Duration" widget="float_time"/>
<field name="project_id" invisible="1"/>
</group>
</sheet>
</form>
</field>
<group attrs="{'invisible': [('analytic_account_active', '=', False)]}">
<group class="oe_subtotal_footer oe_right" name="project_hours">
+22 -12
View File
@@ -344,15 +344,19 @@ def send_iot_box_device(send_printer):
printerList = {}
if send_printer:
printers = subprocess.check_output("sudo lpinfo -lv", shell=True).decode('utf-8').split('Device')
printers_installed = ''
try:
printers_installed = subprocess.check_output("sudo lpstat -a", shell=True).decode('utf-8')
except:
pass
for printer in printers:
printerTab = printer.split('\n')
if printer and printerTab[4].split('=')[1] != ' ':
if printer and printerTab[4].split('=')[1] != ' ' or 'lpd://' in printerTab[0]:
device_connection = printerTab[1].split('= ')[1]
model = ''
for device_id in printerTab[4].split('= ')[1].split(';'):
if any(x in device_id for x in ['MDL','MODEL']):
model = device_id.split(':')[1]
name = printerTab[2].split('= ')[1]
serial = re.sub('[^a-zA-Z0-9 ]+', '', model).replace(' ','_')
identifier = ''
if device_connection == 'direct':
@@ -361,6 +365,9 @@ def send_iot_box_device(send_printer):
socketIP = printerTab[0].split('://')[1]
macprinter = subprocess.check_output("arp -a " + socketIP + " |awk NR==1'{print $4}'", shell=True).decode('utf-8').split('\n')[0]
identifier = macprinter # macPRINTER
elif device_connection == 'network' and 'lpd' in printerTab[0]:
identifier = re.sub('[^a-zA-Z0-9 ]+', '', printerTab[0].split('://')[1])
model = printerTab[2].split('= ')[1]
elif device_connection == 'network' and 'dnssd' in printerTab[0]:
hostname_printer = subprocess.check_output("ippfind -n \"" + model + "\" | awk \'{split($0,a,\"/\"); print a[3]}\' | awk \'{split($0,b,\":\"); print b[1]}\'", shell=True).decode('utf-8').split('\n')[0]
if hostname_printer:
@@ -375,15 +382,16 @@ def send_iot_box_device(send_printer):
'type': 'printer'
}
# install these printers
try:
ppd = subprocess.check_output("sudo lpinfo -m |grep '" + model + "'", shell=True).decode('utf-8').split('\n')
if len(ppd) > 2:
if identifier not in printers_installed:
# install these printers
try:
ppd = subprocess.check_output("sudo lpinfo -m |grep '" + model + "'", shell=True).decode('utf-8').split('\n')
if len(ppd) > 2:
subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "'", shell=True)
else:
subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "' -m '" + ppd[0].split(' ')[0] + "'", shell=True)
except:
subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "'", shell=True)
else:
subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "' -m '" + ppd[0].split(' ')[0] + "'", shell=True)
except:
subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "'", shell=True)
subprocess.call('> /tmp/printers', shell=True)
for printer in printerList:
subprocess.call('echo "' + printerList[printer]['name'] + '" >> /tmp/printers', shell=True)
@@ -409,14 +417,16 @@ def send_iot_box_device(send_printer):
data['devices'] = devicesList
data_json = json.dumps(data).encode('utf8')
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
http = urllib3.PoolManager()
urllib3.disable_warnings()
http = urllib3.PoolManager(cert_reqs='CERT_NONE')
req = False
try:
req = http.request('POST',
url,
body=data_json,
headers=headers)
except:
except Exception as e:
_logger.warning('Could not reach configured server')
_logger.error('A error encountered : %s ' % e)
+10 -4
View File
@@ -119,12 +119,15 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home):
f = open('/tmp/devices', 'r')
for line in f:
url = 'http://' + self.get_ip_iotbox() + ':8069/hw_drivers/driverdetails/' + line.split('|')[0]
http = urllib3.PoolManager()
urllib3.disable_warnings()
http = urllib3.PoolManager(cert_reqs='CERT_NONE')
value = ''
try:
req = http.request('GET', url)
except:
except Exception as e:
req = ''
_logger.warning('Could not reach configured server')
_logger.error('A error encountered : %s ' % e)
if req:
value = req.data.decode('utf-8')
@@ -193,15 +196,18 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home):
#response = requests.get(url, auth=(username, db_uuid.split('\n')[0]), stream=True)
server = self.get_server_status()
if server:
pm = urllib3.PoolManager()
urllib3.disable_warnings()
pm = urllib3.PoolManager(cert_reqs='CERT_NONE')
resp = False
server = server + '/iot/get_drivers'
try:
resp = pm.request('POST',
server,
fields={'mac': mac})
except:
except Exception as e:
req = ''
_logger.warning('Could not reach configured server')
_logger.error('A error encountered : %s ' % e)
if resp and resp.data:
zip_file = zipfile.ZipFile(io.BytesIO(resp.data))
zip_file.extractall("/home/pi/odoo/addons/hw_drivers/drivers")
@@ -90,7 +90,7 @@
</tr>
<tr>
<td class="heading">Version</td>
<td>V18.10 <a class="btn btn-sm float-right" href='/hw_proxy/upgrade/'>update</a></td>
<td>V18.12 <a class="btn btn-sm float-right" href='/hw_proxy/upgrade/'>update</a></td>
</tr>
<tr>
<td class="heading">IP Address</td>
@@ -136,5 +136,6 @@
<div style="margin: 20px auto 10px auto;" class="text-center">
<a class="btn" href='/point_of_sale/display'>POS Display</a>
<a class="btn" style="margin-left: 10px;" href='/remote_connect'>Remote Debug</a>
<a target="_blank" class="btn" style="margin-left: 10px;" href="http://{{ ip }}:631">Printers server</a>
</div>
{% endblock %}
@@ -41,6 +41,13 @@ class MailChannel(models.Model):
_sql_constraints = [('livechat_operator_id', "CHECK((channel_type = 'livechat' and livechat_operator_id is not null) or (channel_type != 'livechat'))",
'Livechat Operator ID is required for a channel of type livechat.')]
@api.multi
def _compute_is_chat(self):
super(MailChannel, self)._compute_is_chat()
for record in self:
if record.channel_type == 'livechat':
record.is_chat = True
@api.multi
def _channel_message_notifications(self, message):
""" When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when
File diff suppressed because it is too large Load Diff
+58 -19
View File
@@ -4,9 +4,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0\n"
"Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-03-06 12:52+0000\n"
"POT-Creation-Date: 2019-01-17 08:59+0000\n"
"PO-Revision-Date: 2017-03-15 14:20+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -790,16 +790,6 @@ msgstr "Andere ingehouden voorheffingen"
msgid "Avances et prêts au personnel"
msgstr "Voorschotten en leningen aan het personeel"
#. module: l10n_be
#: model:account.chart.template,name:l10n_be.l10nbe_chart_template
msgid "Belgian PCMN"
msgstr "Belgische PCMN"
#. module: l10n_be
#: model:ir.ui.menu,name:l10n_be.account_reports_be_statements_menu
msgid "Belgium Statements"
msgstr ""
#. module: l10n_be
#: model:account.account.tag,name:l10n_be.tax_tag_81_not_deductible
msgid "Belgium VAT Form: Tax not deductible (grid 81)"
@@ -1285,6 +1275,11 @@ msgstr "Verschuldigde of gestorte belastingen en voorheffingen"
msgid "Impôts étrangers"
msgstr "Buitenlandse belastingen"
#. module: l10n_be
#: model:account.account.template,name:l10n_be.l10nbe_chart_template_liquidity_transfer
msgid "Liquidity Transfer"
msgstr ""
#. module: l10n_be
#: model:account.account.template,name:l10n_be.a6100
msgid "Loyers divers"
@@ -1303,7 +1298,8 @@ msgstr "Grondstoffen"
#. module: l10n_be
#: model:account.account.template,name:l10n_be.a24011
msgid "Matériel de bureau de bâtiments administratifs et commerciaux"
msgstr "Kantooruitrusting voor gebouwen met adminestratieve en commerciële functie"
msgstr ""
"Kantooruitrusting voor gebouwen met adminestratieve en commerciële functie"
#. module: l10n_be
#: model:account.account.template,name:l10n_be.a24010
@@ -1583,7 +1579,8 @@ msgstr "Bezoldigingen: Bestuurders of zaakvoerders"
#. module: l10n_be
#: model:account.account.template,name:l10n_be.a6204
msgid "Rémunérations: Autres membres du personnel"
msgstr "Bezoldigingen: Andere personeelsleden, verbonden met een arbeidsovereenkomst"
msgstr ""
"Bezoldigingen: Andere personeelsleden, verbonden met een arbeidsovereenkomst"
#. module: l10n_be
#: model:account.account.template,name:l10n_be.a6202
@@ -1666,6 +1663,7 @@ msgid "T.V.A. à payer - Intra-communautaire"
msgstr "Te betalen BTW - Intracommunautair"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_0
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-00
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-00-G
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-00-S
@@ -1694,6 +1692,11 @@ msgstr "BTW 0% Medecont."
msgid "TVA 0% EU"
msgstr "BTW 0% EU"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_0_eu
msgid "TVA 0% EU."
msgstr ""
#. module: l10n_be
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-00-ROW-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-00-ROW-CC
@@ -1703,6 +1706,7 @@ msgid "TVA 0% Non EU"
msgstr "BTW 0% Non EU"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_12
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-12
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-12-G
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-12-S
@@ -1713,6 +1717,7 @@ msgid "TVA 12%"
msgstr "BTW 12%"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_12_cocont
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-12-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-12-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V83-12-CC
@@ -1762,6 +1767,11 @@ msgstr "BTW 12% EU Verhaalbaar"
msgid "TVA 12% EU Récupérable - Biens divers"
msgstr "BTW 12% EU Verhaalbaar - Diverse goederen"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_12_eu
msgid "TVA 12% EU."
msgstr ""
#. module: l10n_be
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-12-ROW-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-12-ROW-CC
@@ -1784,6 +1794,12 @@ msgid "TVA 12% Non EU Récupérable"
msgstr "BTW 12% Niet EU Verhaalbaar"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_12_non_eu
msgid "TVA 12% Non EU."
msgstr ""
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_21
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-21
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-21-G
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-21-S
@@ -1794,6 +1810,7 @@ msgid "TVA 21%"
msgstr "BTW 21%"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_21_cocont
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-21-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-21-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V83-21-CC
@@ -1838,6 +1855,11 @@ msgstr "BTW 21% EU Aftrekbaar"
msgid "TVA 21% EU Récupérable"
msgstr "BTW 21% EU Verhaalbaar"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_21_eu
msgid "TVA 21% EU."
msgstr ""
#. module: l10n_be
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-21-ROW-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-21-ROW-CC
@@ -1859,11 +1881,21 @@ msgstr "BTW 21% Niet EU Aftrekbaar"
msgid "TVA 21% Non EU Récupérable"
msgstr "BTW 21% Niet EU Verhaalbaar"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_21_non_eu
msgid "TVA 21% Non EU."
msgstr ""
#. module: l10n_be
#: model:account.tax.template,description:l10n_be.attn_TVA-21-inclus-dans-prix
msgid "TVA 21% TTC"
msgstr "VAT 21% (inclusief BTW)"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_21_50_deductible
msgid "TVA 21%, 50% Deductible."
msgstr ""
#. module: l10n_be
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-CAR-EXC-C2
msgid "TVA 50% Déductible - Frais de voiture (Prix Excl.)"
@@ -1875,6 +1907,7 @@ msgid "TVA 50% Non Déductible - Frais de voiture (Prix Excl.)"
msgstr "BTW 50% Niet Aftrekbaar - Auto kosten (Prijs Excl.)"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_6
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-06
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-06-G
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-06-S
@@ -1885,6 +1918,7 @@ msgid "TVA 6%"
msgstr "BTW 6%"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_6_cocont
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-06-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-06-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V83-06-CC
@@ -1929,6 +1963,11 @@ msgstr "BTW 6% EU Aftrekbaar"
msgid "TVA 6% EU Récupérable"
msgstr "BTW 6% EU Verhaalbaar"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_6_eu
msgid "TVA 6% EU."
msgstr ""
#. module: l10n_be
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V81-06-ROW-CC
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-06-ROW-CC
@@ -1950,6 +1989,11 @@ msgstr "BTW 6% Niet EU Aftrekbaar"
msgid "TVA 6% Non EU Récupérable"
msgstr "BTW 6% Niet EU Verhaalbaar"
#. module: l10n_be
#: model:account.tax.group,name:l10n_be.tax_group_tva_6_non_eu
msgid "TVA 6% Non EU."
msgstr ""
#. module: l10n_be
#: model:account.account.template,name:l10n_be.a4118
msgid "Taxe d'égalisation due"
@@ -1965,11 +2009,6 @@ msgstr "Verkeersbelasting bedrijfswagens"
msgid "Terrains"
msgstr "Terreinen"
#. module: l10n_be
#: model:account.account.template,name:l10n_be.trans
msgid "Transferts"
msgstr "Transferten"
#. module: l10n_be
#: model:account.tax.template,description:l10n_be.attn_VAT-IN-V82-CAR-EXC-C1
msgid "VAT 50% Non Déductible - Frais de voiture"
@@ -25,7 +25,7 @@
This invoice is already paid.
% else:
Please remit payment at your earliest convenience.
% if object.reference_type == 'bba' and object.reference:
% if object.invoice_reference_type=='structured' and object.reference:
Please use the following communication for your payment: <strong>${object.reference}</strong>.
% endif
<br /><br />
+8 -1
View File
@@ -78,6 +78,7 @@ class Channel(models.Model):
('chat', 'Chat Discussion'),
('channel', 'Channel')],
'Channel Type', default='channel')
is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat', default=False)
description = fields.Text('Description')
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False)
email_send = fields.Boolean('Send messages by email', default=False)
@@ -178,6 +179,12 @@ class Channel(models.Model):
for record in self:
record.is_member = record in membership_ids
@api.multi
def _compute_is_chat(self):
for record in self:
if record.channel_type == 'chat':
record.is_chat = True
@api.onchange('public')
def _onchange_public(self):
if self.public == 'public':
@@ -384,7 +391,7 @@ class Channel(models.Model):
if moderation_status == 'rejected':
return self.env['mail.message']
self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True})
self.filtered(lambda channel: channel.is_chat).mapped('channel_last_seen_partner_ids').write({'is_pinned': True})
message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(message_type=message_type, moderation_status=moderation_status, **kwargs)
+1 -1
View File
@@ -22,7 +22,7 @@ function _parseAndTransform(nodes, transformFunction) {
// Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
// Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match.
var urlRegexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._+~#=]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_+.~#?&'$//=]*)/gi;
var urlRegexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._+~#=]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_+.~#?&'$//=;]*)/gi;
function linkify(text, attrs) {
attrs = attrs || {};
if (attrs.target === undefined) {
@@ -18,3 +18,9 @@
.o_white_body {
background-color: white;
}
.o_mass_mailing_unsubscribed {
margin-left: 20px;
color: #005326;
font-size: 90%;
}
@@ -32,6 +32,10 @@
<input type="checkbox" class="mail_list_checkbox" name="contact_ids"
t-att-value="list_id['id']" t-att-checked="None if list_id['id'] in opt_out_list_ids else 'checked'"/>
<t t-esc="list_id.name"/>
<span t-if="list_id['id'] in opt_out_list_ids"
class="o_mass_mailing_unsubscribed">
Unsubscribed
</span>
</li>
</t>
</t>
@@ -57,7 +57,7 @@
${object.company_id.email}
</a>
% endif
${object.company_id.website}
% if object.company_id.website
|
<a href="'%s' % object.company_id.website" style="text-decoration:none; color: #999999;">
${object.company_id.website}
+4 -1
View File
@@ -540,7 +540,10 @@ class MrpProduction(models.Model):
if move:
old_qty = move[0].product_uom_qty
if quantity > 0:
move[0].write({'product_uom_qty': quantity})
move[0]._decrease_reserved_quanity(quantity)
move[0].with_context(do_not_unreserve=True).write({'product_uom_qty': quantity})
move[0]._recompute_state()
move[0]._action_assign()
move.unit_factor = quantity / move.raw_material_production_id.product_qty
elif quantity < 0: # Do not remove 0 lines
if move[0].quantity_done > 0:
+18
View File
@@ -188,6 +188,24 @@ class StockMove(models.Model):
self.sudo().unlink()
return processed_moves
def _decrease_reserved_quanity(self, quantity):
""" Decrease the reservation on move lines but keeps the
all other data.
"""
move_line_to_unlink = self.env['stock.move.line']
for move in self:
reserved_quantity = quantity
for move_line in self.move_line_ids:
if move_line.product_uom_qty > reserved_quantity:
move_line.product_uom_qty = reserved_quantity
else:
move_line.product_uom_qty = 0
reserved_quantity -= move_line.product_uom_qty
if not move_line.product_uom_qty and not move_line.qty_done:
move_line_to_unlink |= move_line
move_line_to_unlink.unlink()
return True
def _prepare_phantom_move_values(self, bom_line, product_qty, quantity_done):
return {
'picking_id': self.picking_id.id if self.picking_id else False,
+56 -1
View File
@@ -7,9 +7,14 @@
<div class="page">
<div class="oe_structure"/>
<div class="row">
<div class="col-12">
<div class="col-7">
<h2><span t-field="o.name"/></h2>
</div>
<div class="col-5">
<span class="text-right">
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', o.name, 600, 100)" style="width:350px;height:60px"/>
</span>
</div>
</div>
<div class="row mt32 mb32">
<div class="col-3" t-if="o.origin">
@@ -142,6 +147,56 @@
</t>
</template>
<template id="label_production_view_pdf">
<t t-call="web.basic_layout">
<t t-foreach="docs" t-as="o">
<div class="page">
<table class="table table-condensed" style="border-bottom: 0px solid white !important;width: 3in;" t-if="o.move_finished_ids">
<t t-set="lines" t-value="o.move_finished_ids"/>
<t t-foreach="lines" t-as="line">
<t t-set="move_lines" t-value="line.move_line_ids.filtered(lambda x: x.state != 'done' and x.product_qty)"/>
<t t-if="o.state == 'done'">
<t t-set="move_lines" t-value="line.move_line_ids.filtered(lambda x: x.state == 'done' and x.qty_done)"/>
</t>
<t t-foreach="move_lines" t-as="ml">
<tr>
<th style="text-align: left;">
<span t-field="line.product_id"/>
</th>
</tr>
<tr>
<td>
Quantity:
<span t-esc="ml.product_uom_qty" t-if="ml.state !='done'"/>
<span t-esc="ml.qty_done" t-if="ml.state =='done'"/>
<span t-field="ml.product_uom_id" groups="uom.group_uom"/>
</td>
</tr>
<tr>
<td style="text-align: center; vertical-align: middle;" class="col-5">
<t t-if="ml.lot_id">
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', ml.lot_id.name, 600, 100)" style="width:100%;height:35px" alt="Barcode"/>
<span t-field="ml.lot_id.name"/>
</t>
</td>
</tr>
<tr>
<td style="text-align: center; vertical-align: middle;" class="col-5">
<t t-if="line.product_id.barcode and not ml.lot_id">
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', line.product_id.barcode, 600, 100)" style="width:100%;height:20%;" alt="Barcode"/>
<span t-field="line.product_id.barcode"/>
</t>
</td>
</tr>
</t>
</t>
</table>
<div class="oe_structure"/>
</div>
</t>
</t>
</template>
<template id="production_message">
<t t-if="move.move_id.raw_material_production_id">
<t t-set="message">Consumed</t>
@@ -19,5 +19,14 @@
report_type="qweb-pdf"
print_report_name="'Bom Structure - %s' % object.display_name"
/>
<report
string="Finished Product Label (PDF)"
id="action_report_finished_product"
model="mrp.production"
name="mrp.label_production_view_pdf"
file="mrp.label_production_view_pdf"
report_type="qweb-pdf"
print_report_name="'Finished products - %s' % object.name"
/>
</data>
</odoo>
+47
View File
@@ -629,6 +629,53 @@ class TestMrpOrder(TestMrpCommon):
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
def test_product_produce_5(self):
""" Build 5 final products with different consumed lots,
then edit the finished quantity and update the Manufacturing
order quantity. Then check if the produced quantity do not
change and it is possible to close the MO.
"""
self.stock_location = self.env.ref('stock.stock_location_stock')
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot')
self.assertEqual(len(mo), 1, 'MO should have been created')
lot_1 = self.env['stock.production.lot'].create({
'name': 'lot1',
'product_id': p1.id,
})
lot_2 = self.env['stock.production.lot'].create({
'name': 'lot2',
'product_id': p1.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 10, lot_id=lot_1)
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 10, lot_id=lot_2)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_wizard = self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}).create({
'product_qty': 5.0,
})
produce_wizard._onchange_product_qty()
for produce_line in produce_wizard.produce_line_ids:
produce_line.qty_done = produce_line.qty_to_consume
produce_wizard.do_produce()
mo.move_finished_ids.move_line_ids.qty_done -= 1
update_quantity_wizard = self.env['change.production.qty'].create({
'mo_id': mo.id,
'product_qty': 4,
})
update_quantity_wizard.change_prod_qty()
self.assertEqual(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).quantity_done, 20, 'Update the produce quantity should not impact already produced quantity.')
mo.button_mark_done()
def test_product_produce_uom(self):
plastic_laminate = self.env.ref('mrp.product_product_plastic_laminate')
bom = self.env.ref('mrp.mrp_bom_plastic_laminate')
@@ -53,7 +53,6 @@ class ChangeProductionQty(models.TransientModel):
factor = production.product_uom_id._compute_quantity(production.product_qty - qty_produced, production.bom_id.product_uom_id) / production.bom_id.product_qty
boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
documents = {}
done_quantities = {move: move.quantity_done for move in production.move_raw_ids}
for line, line_data in lines:
move, old_qty, new_qty = production._update_raw_move(line, line_data)
iterate_key = production._get_document_iterate_key(move)
@@ -73,9 +72,6 @@ class ChangeProductionQty(models.TransientModel):
production._log_downside_manufactured_quantity(finished_moves_modification)
moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
moves._action_assign()
for move in production.move_raw_ids:
if move.quantity_done != done_quantities[move]:
move._set_quantity_done(done_quantities[move])
for wo in production.workorder_ids:
operation = wo.operation_id
if operation_bom_qty.get(operation.id):
+1 -1
View File
@@ -324,7 +324,7 @@ class PaymentAcquirer(models.Model):
'acquirers': acquirers,
'pms': self.env['payment.token'].search([
('partner_id', '=', partner.id),
('acquirer_id', 'in', acquirers.filtered(lambda acq: acq.payment_flow == 's2s').ids)]),
('acquirer_id', 'in', acquirers.ids)]),
}
@api.multi
@@ -114,12 +114,14 @@ class PaymentWizard(models.TransientModel):
'paypal_seller_account': self.paypal_seller_account,
'paypal_pdt_token': self.paypal_pdt_token,
'website_published': True,
'environment': 'prod',
})
if self.payment_method == 'stripe':
new_env.ref('payment.payment_acquirer_stripe').write({
'stripe_secret_key': self.stripe_secret_key,
'stripe_publishable_key': self.stripe_publishable_key,
'website_published': True,
'environment': 'prod',
})
if self.payment_method == 'manual':
manual_acquirer = self._get_manual_payment_acquirer(new_env)
@@ -131,6 +133,7 @@ class PaymentWizard(models.TransientModel):
manual_acquirer.name = self.manual_name
manual_acquirer.post_msg = self.manual_post_msg
manual_acquirer.website_published = True
manual_acquirer.environment = 'prod'
journal = manual_acquirer.journal_id
if journal:
+12 -4
View File
@@ -572,14 +572,22 @@ class PosOrder(models.Model):
states={'draft': [('readonly', False)]},
)
@api.onchange('statement_ids', 'lines', 'lines')
@api.onchange('statement_ids', 'lines')
def _onchange_amount_all(self):
amounts = {order_id: {'paid': 0, 'return': 0, 'untaxed': 0} for order_id in self.ids}
for order in self.env['account.bank.statement.line'].read_group([('pos_statement_id', 'in', self.ids)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
amounts[order['pos_statement_id'][0]]['paid'] = order['amount']
for order in self.env['account.bank.statement.line'].read_group(['&', ('pos_statement_id', 'in', self.ids), ('amount', '<', 0)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
amounts[order['pos_statement_id'][0]]['return'] = order['amount']
for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal'], ['order_id']):
amounts[order['order_id'][0]]['untaxed'] = order['price_subtotal']
for order in self:
currency = order.pricelist_id.currency_id
order.amount_paid = sum(payment.amount for payment in order.statement_ids)
order.amount_return = sum(payment.amount < 0 and payment.amount or 0 for payment in order.statement_ids)
order.amount_paid = amounts[order.id]['paid']
order.amount_return = amounts[order.id]['return']
order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines))
amount_untaxed = currency.round(sum(line.price_subtotal for line in order.lines))
amount_untaxed = currency.round(amounts[order.id]['untaxed'])
order.amount_total = order.amount_tax + amount_untaxed
@api.onchange('partner_id')
+1 -1
View File
@@ -723,7 +723,7 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
}else if(err.message === 'TLSError'){
title = 'Https connection to IoT Box failed';
body = 'Make sure you are using IoT Box v18.10 or higher.\n\n Navigate to ' + err.url + ' to accept the certificate of your IoT Box.';
body = 'Make sure you are using IoT Box v18.12 or higher.\n\n Navigate to ' + err.url + ' to accept the certificate of your IoT Box.';
}else if(err.code === 200){
title = err.data.message;
body = err.data.debug;
@@ -1,6 +1,11 @@
#!/usr/bin/env bash
sudo mount -o remount,rw /
sudo git --work-tree=/home/pi/odoo/ --git-dir=/home/pi/odoo/.git pull
cd /home/pi/odoo
localbranch=$(git symbolic-ref -q --short HEAD)
localremote=$(git config branch.$localbranch.remote)
git fetch "${localremote}" "${localbranch}" --depth=1
git reset "${localremote}"/"${localbranch}" --hard
sudo mount -o remount,ro /
(sleep 5 && sudo reboot) &
@@ -22,3 +22,7 @@ create_ramdisk "/tmp" "64M"
# bind mount / so that we can get to the real /var and /etc
mount --bind / /root_bypass_ramdisks
# allow to cups server to save configuration file of printers
mount --bind /root_bypass_ramdisks/etc/cups /root_bypass_ramdisks/etc/cups
mount -o remount,rw /root_bypass_ramdisks/etc/cups /root_bypass_ramdisks/etc/cups
@@ -0,0 +1,93 @@
#
# File/directory/user/group configuration file for the CUPS scheduler.
# See "man cups-files.conf" for a complete description of this file.
#
# List of events that are considered fatal errors for the scheduler...
#FatalErrors config
# Do we call fsync() after writing configuration or status files?
#SyncOnClose Yes
# Default user and group for filters/backends/helper programs; this cannot be
# any user or group that resolves to ID 0 for security reasons...
#User lp
#Group lp
# Administrator user group, used to match @SYSTEM in cupsd.conf policy rules...
# This cannot contain the Group value for security reasons...
SystemGroup lpadmin pi
# User that is substituted for unauthenticated (remote) root accesses...
#RemoteRoot remroot
# Do we allow file: device URIs other than to /dev/null?
#FileDevice No
# Permissions for configuration and log files...
#ConfigFilePerm 0640
#LogFilePerm 00640
# Location of the file logging all access to the scheduler; may be the name
# "syslog". If not an absolute path, the value of ServerRoot is used as the
# root directory. Also see the "AccessLogLevel" directive in cupsd.conf.
AccessLog /var/log/cups/access_log
# Location of cache files used by the scheduler...
#CacheDir /var/cache/cups
# Location of data files used by the scheduler...
#DataDir /usr/share/cups
# Location of the static web content served by the scheduler...
#DocumentRoot /usr/share/cups/doc-root
# Location of the file logging all messages produced by the scheduler and any
# helper programs; may be the name "syslog". If not an absolute path, the value
# of ServerRoot is used as the root directory. Also see the "LogLevel"
# directive in cupsd.conf.
ErrorLog /var/log/cups/error_log
# Location of fonts used by older print filters...
#FontPath /usr/share/cups/fonts
# Location of LPD configuration
#LPDConfigFile
# Location of the file logging all pages printed by the scheduler and any
# helper programs; may be the name "syslog". If not an absolute path, the value
# of ServerRoot is used as the root directory. Also see the "PageLogFormat"
# directive in cupsd.conf.
PageLog /var/log/cups/page_log
# Location of the file listing all of the local printers...
#Printcap /run/cups/printcap
# Format of the Printcap file...
#PrintcapFormat bsd
#PrintcapFormat plist
#PrintcapFormat solaris
# Location of all spool files...
#RequestRoot /var/spool/cups
# Location of helper programs...
#ServerBin /usr/lib/cups
# SSL/TLS keychain for the scheduler...
#ServerKeychain ssl
# Location of other configuration files...
ServerRoot /root_bypass_ramdisks/etc/cups
# Location of Samba configuration file...
#SMBConfigFile
# Location of scheduler state files...
#StateDir /run/cups
# Location of scheduler/helper temporary files. This directory is emptied on
# scheduler startup and cannot be one of the standard (public) temporary
# directory locations for security reasons...
#TempDir /var/spool/cups/tmp
@@ -0,0 +1,188 @@
#
# Configuration file for the CUPS scheduler. See "man cupsd.conf" for a
# complete description of this file.
#
# Log general information in error_log - change "warn" to "debug"
# for troubleshooting...
LogLevel warn
PageLogFormat
# Deactivate CUPS' internal logrotating, as we provide a better one, especially
# LogLevel debug2 gets usable now
MaxLogSize 0
# Only listen for connections from the local machine.
Listen 0.0.0.0:631
Listen /run/cups/cups.sock
# Show shared printers on the local network.
Browsing On
BrowseLocalProtocols dnssd
# Default authentication type, when authentication is required...
DefaultAuthType Basic
# Web interface setting...
WebInterface Yes
# Restrict access to the server...
<Location />
Order allow,deny
Allow all
</Location>
# Restrict access to the admin pages...
<Location /admin>
Order allow,deny
Allow all
</Location>
# Restrict access to configuration files...
<Location /admin/conf>
AuthType Default
Require user @SYSTEM
Order allow
</Location>
# Restrict access to log files...
<Location /admin/log>
AuthType Default
Require user @SYSTEM
Order allow
</Location>
# Set the default printer/job policies...
<Policy default>
# Job/subscription privacy...
JobPrivateAccess default
JobPrivateValues default
SubscriptionPrivateAccess default
SubscriptionPrivateValues default
# Job-related operations must be done by the owner or an administrator...
<Limit Create-Job Print-Job Print-URI Validate-Job>
Order deny,allow
</Limit>
<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
# All administration operations require an administrator to authenticate...
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default CUPS-Get-Devices>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# All printer operations require a printer operator to authenticate...
<Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After Cancel-Jobs CUPS-Accept-Jobs CUPS-Reject-Jobs>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# Only the owner or an administrator can cancel or authenticate a job...
<Limit Cancel-Job CUPS-Authenticate-Job>
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
<Limit All>
Order deny,allow
</Limit>
</Policy>
# Set the authenticated printer/job policies...
<Policy authenticated>
# Job/subscription privacy...
JobPrivateAccess default
JobPrivateValues default
SubscriptionPrivateAccess default
SubscriptionPrivateValues default
# Job-related operations must be done by the owner or an administrator...
<Limit Create-Job Print-Job Print-URI Validate-Job>
AuthType Default
Order deny,allow
</Limit>
<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
AuthType Default
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
# All administration operations require an administrator to authenticate...
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# All printer operations require a printer operator to authenticate...
<Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After Cancel-Jobs CUPS-Accept-Jobs CUPS-Reject-Jobs>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# Only the owner or an administrator can cancel or authenticate a job...
<Limit Cancel-Job CUPS-Authenticate-Job>
AuthType Default
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
<Limit All>
Order deny,allow
</Limit>
</Policy>
# Set the kerberized printer/job policies...
<Policy kerberos>
# Job/subscription privacy...
JobPrivateAccess default
JobPrivateValues default
SubscriptionPrivateAccess default
SubscriptionPrivateValues default
# Job-related operations must be done by the owner or an administrator...
<Limit Create-Job Print-Job Print-URI Validate-Job>
AuthType Negotiate
Order deny,allow
</Limit>
<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
AuthType Negotiate
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
# All administration operations require an administrator to authenticate...
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# All printer operations require a printer operator to authenticate...
<Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After Cancel-Jobs CUPS-Accept-Jobs CUPS-Reject-Jobs>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# Only the owner or an administrator can cancel or authenticate a job...
<Limit Cancel-Job CUPS-Authenticate-Job>
AuthType Negotiate
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
<Limit All>
Order deny,allow
</Limit>
</Policy>
@@ -76,16 +76,16 @@ server {
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
# listen 80;
server {
listen 80;
# listen [::]:80;
#
# server_name example.com;
#
# root /var/www/example.com;
# index index.html;
root /var/www/;
index index.html;
#
# location / {
# try_files $uri $uri/ =404;
# }
#}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -21,7 +21,70 @@ apt-get update && apt-get -y upgrade
# Do not be too fast to upgrade to more recent firmware and kernel than 4.38
# Firmware 4.44 seems to prevent the LED mechanism from working
PKGS_TO_INSTALL="fswebcam python3-urllib3 cups printer-driver-all cups-ipp-utils adduser postgresql python3 python3-dateutil python3-decorator python3-docutils python3-feedparser python3-pil python3-jinja2 python3-ldap3 python3-lxml python3-mako python3-mock python3-openid python3-psutil python3-psycopg2 python3-babel python3-pydot python3-pyparsing python3-pypdf2 python3-reportlab python3-requests python3-simplejson python3-tz python3-vatnumber python3-werkzeug python3-serial python3-pip python3-dev localepurge vim mc mg screen iw hostapd git rsync console-data lightdm xserver-xorg-video-fbdev xserver-xorg-input-evdev iceweasel xdotool unclutter x11-utils openbox python3-netifaces rpi-update python3-passlib python3-libsass python3-qrcode python3-html2text python3-unittest2 python3-simplejson nginx-full dnsmasq"
PKGS_TO_INSTALL="
fswebcam \
nginx-full \
dnsmasq \
cups \
printer-driver-all \
cups-ipp-utils \
localepurge \
vim \
mc \
mg \
screen \
iw \
hostapd \
git \
rsync \
console-data \
lightdm \
xserver-xorg-video-fbdev \
xserver-xorg-input-evdev \
iceweasel \
xdotool \
unclutter \
x11-utils \
openbox \
rpi-update \
adduser \
postgresql \
python3 \
python3-urllib3 \
python3-dateutil \
python3-decorator \
python3-docutils \
python3-feedparser \
python3-pil \
python3-jinja2 \
python3-ldap3 \
python3-lxml \
python3-mako \
python3-mock \
python3-openid \
python3-psutil \
python3-psycopg2 \
python3-babel \
python3-pydot \
python3-pyparsing \
python3-pypdf2 \
python3-reportlab \
python3-requests \
python3-simplejson \
python3-tz \
python3-vatnumber \
python3-werkzeug \
python3-serial \
python3-pip \
python3-dev \
python3-netifaces \
python3-passlib \
python3-libsass \
python3-qrcode \
python3-html2text \
python3-unittest2 \
python3-simplejson"
echo "Acquire::Retries "16";" > /etc/apt/apt.conf.d/99acquire-retries
# KEEP OWN CONFIG FILES DURING PACKAGE CONFIGURATION
# http://serverfault.com/questions/259226/automatically-keep-current-version-of-config-files-when-apt-get-install
@@ -84,6 +147,7 @@ fi
if [ $SYSTEMD -eq 1 ]; then
systemctl set-default graphical.target
ln -fs /etc/systemd/system/autologin@.service /etc/systemd/system/getty.target.wants/getty@tty1.service
rm /etc/systemd/system/sysinit.target.wants/systemd-timesyncd.service
else
update-rc.d lightdm enable 2
fi
@@ -29,7 +29,7 @@ __base="$(basename ${__file} .sh)"
MOUNT_POINT="${__dir}/root_mount"
OVERWRITE_FILES_BEFORE_INIT_DIR="${__dir}/overwrite_before_init"
OVERWRITE_FILES_AFTER_INIT_DIR="${__dir}/overwrite_after_init"
VERSION=saas-11.5
VERSION=12.0
REPO=https://github.com/odoo/odoo.git
if [ ! -f kernel-qemu ] || ! file_exists *raspbian*.img ; then
+2 -2
View File
@@ -414,7 +414,7 @@ class ProductProduct(models.Model):
# Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
# Use `load=False` to not call `name_get` for the `product_tmpl_id`
self.sudo().read(['name', 'default_code', 'product_tmpl_id', 'attribute_value_ids'], load=False)
self.sudo().read(['name', 'default_code', 'product_tmpl_id', 'attribute_value_ids', 'attribute_line_ids'], load=False)
product_template_ids = self.sudo().mapped('product_tmpl_id').ids
@@ -431,7 +431,7 @@ class ProductProduct(models.Model):
supplier_info_by_template.setdefault(r.product_tmpl_id, []).append(r)
for product in self.sudo():
# display only the attributes with multiple possible values on the template
variable_attributes = product.attribute_value_ids.filtered(lambda v: len(v.attribute_id.value_ids) > 1).mapped('attribute_id')
variable_attributes = product.attribute_line_ids.filtered(lambda l: len(l.value_ids) > 1).mapped('attribute_id')
variant = product.attribute_value_ids._variant_name(variable_attributes)
name = variant and "%s (%s)" % (product.name, variant) or product.name
+1 -1
View File
@@ -310,7 +310,7 @@
<label for="weight"/>
<div class="o_row">
<field name="weight"/>
<span>kg</span>
<span><field name="weight_uom_name"/></span>
</div>
</group>
<group name="packaging" string="Packaging" groups="product.group_stock_packaging">
+29 -6
View File
@@ -11,15 +11,38 @@ class res_partner(models.Model):
@api.multi
def _compute_purchase_order_count(self):
PurchaseOrder = self.env['purchase.order']
for partner in self:
partner.purchase_order_count = PurchaseOrder.search_count([('partner_id', 'child_of', partner.id)])
# retrieve all children partners and prefetch 'parent_id' on them
all_partners = self.search([('id', 'child_of', self.ids)])
all_partners.read(['parent_id'])
purchase_order_groups = self.env['purchase.order'].read_group(
domain=[('partner_id', 'in', all_partners.ids)],
fields=['partner_id'], groupby=['partner_id']
)
for group in purchase_order_groups:
partner = self.browse(group['partner_id'][0])
while partner:
if partner in self:
partner.purchase_order_count += group['partner_id_count']
partner = partner.parent_id
@api.multi
def _compute_supplier_invoice_count(self):
Invoice = self.env['account.invoice']
for partner in self:
partner.supplier_invoice_count = Invoice.search_count([('partner_id', 'child_of', partner.id), ('type', 'in', ('in_invoice', 'in_refund'))])
# retrieve all children partners and prefetch 'parent_id' on them
all_partners = self.search([('id', 'child_of', self.ids), ('type', 'in', ('in_invoice', 'in_refund')])
all_partners.read(['parent_id'])
supplier_invoice_groups = self.env['account.invoice'].read_group(
domain=[('partner_id', 'in', all_partners.ids),
('type', '=', 'in_invoice')],
fields=['partner_id'], groupby=['partner_id']
)
for group in supplier_invoice_groups:
partner = self.browse(group['partner_id'][0])
while partner:
if partner in self:
partner.supplier_invoice_count += group['partner_id_count']
partner = partner.parent_id
@api.model
def _commercial_fields(self):
+1 -1
View File
@@ -8,7 +8,7 @@ class PurchaseBillUnion(models.Model):
_name = 'purchase.bill.union'
_auto = False
_description = 'Purchases & Bills Union'
_order = "purchase_order_id desc, vendor_bill_id desc"
_order = "date desc, name desc"
name = fields.Char(string='Reference', readonly=True)
reference = fields.Char(string='Source', readonly=True)
+1
View File
@@ -220,6 +220,7 @@
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use', '=', 'purchase')]" options="{'no_create': True}"/>
<field name="date_planned" widget="date"/>
<field name="account_analytic_id" colspan="2" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</group>
</group>
-15
View File
@@ -863,21 +863,6 @@ class SaleOrder(models.Model):
self.ensure_one()
return '%s %s' % (self.type_name, self.name)
@api.multi
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the online quote if it exists. """
self.ensure_one()
user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
if not self.sale_order_template_id or (not user.share and not self.env.context.get('force_website')):
return super(SaleOrder, self).get_access_action(access_uid)
return {
'type': 'ir.actions.act_url',
'url': self.get_portal_url(),
'target': 'self',
'res_id': self.id,
}
def _get_share_url(self, redirect=False, signup_partner=False, pid=None):
self.ensure_one()
if self.state not in ['sale', 'done']:
+2
View File
@@ -313,6 +313,7 @@
<field name="qty_to_invoice" invisible="1"/>
<field name="qty_delivered_manual" invisible="1"/>
<field name="qty_delivered_method" invisible="1"/>
<field name="price_total" invisible="1"/>
<field name="price_subtotal" invisible="1"/>
<label for="product_uom_qty" string="Ordered Quantity"/>
<div>
@@ -479,6 +480,7 @@
<field name="product_uom_qty"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="price_subtotal"/>
<field name="price_total" invisible="1"/>
<field name="price_unit"/>
<field name="display_type"/>
<templates>
@@ -114,6 +114,21 @@ class SaleOrder(models.Model):
self.sale_order_template_id.mail_template_id.send_mail(order.id)
return res
@api.multi
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the online quote if it exists. """
self.ensure_one()
user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
if not self.sale_order_template_id or (not user.share and not self.env.context.get('force_website')):
return super(SaleOrder, self).get_access_action(access_uid)
return {
'type': 'ir.actions.act_url',
'url': self.get_portal_url(),
'target': 'self',
'res_id': self.id,
}
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
@@ -14,6 +14,9 @@
padding-left: 10mm;
}
}
.o_background_footer {
padding-left: 8mm;
}
.article>.page {
> h1, > h2, > h3 {
width: 50%;
+10 -1
View File
@@ -50,6 +50,7 @@ class ProductTemplate(models.Model):
class ProductProduct(models.Model):
_inherit = 'product.product'
stock_value_currency_id = fields.Many2one('res.currency', compute='_compute_stock_value_currency')
stock_value = fields.Float(
'Value', compute='_compute_stock_value')
qty_at_date = fields.Float(
@@ -125,6 +126,12 @@ class ProductProduct(models.Model):
moves = StockMove.search(domain)
return sum(moves.mapped('remaining_value')), moves
@api.multi
def _compute_stock_value_currency(self):
currency_id = self.env.user.company_id.currency_id
for product in self:
product.stock_value_currency_id = currency_id
@api.multi
@api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'stock_move_ids.remaining_value', 'product_tmpl_id.cost_method', 'product_tmpl_id.standard_price', 'product_tmpl_id.categ_id.property_valuation')
def _compute_stock_value(self):
@@ -191,12 +198,14 @@ class ProductProduct(models.Model):
"""
self.ensure_one()
to_date = self.env.context.get('to_date')
ctx = self.env.context.copy()
ctx.pop('group_by', None)
action = {
'name': _('Valuation at date'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'context': self.env.context,
'context': ctx,
}
if self.valuation == 'real_time':
action['res_model'] = 'account.move.line'
@@ -77,7 +77,8 @@
<field name="qty_at_date"/>
<field name="uom_id" groups="uom.group_uom"/>
<field name="currency_id" invisible="1"/>
<field name="stock_value" sum="Stock Valuation" widget="monetary"/>
<field name="stock_value_currency_id" invisible="1"/>
<field name="stock_value" sum="Stock Valuation" widget="monetary" options="{'currency_field': 'stock_value_currency_id'}"/>
<field name="cost_method" invisible="1"/>
<button name="action_valuation_at_date_details" type="object" icon="fa-info-circle" aria-label="Valuation" title="Valuation" attrs="{'invisible': [('cost_method', '!=', 'fifo')]}" />
</tree>
+4
View File
@@ -254,6 +254,10 @@ class Survey(http.Controller):
def print_survey(self, survey, token=None, **post):
'''Display an survey in printable view; if <token> is set, it will
grab the answers of the user_input_id that has <token>.'''
if survey.auth_required and request.env.user == request.website.user_id:
return request.render("survey.auth_required", {'survey': survey, 'token': token})
return request.render('survey.survey_print',
{'survey': survey,
'token': token,
@@ -205,6 +205,10 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
});
}
} else {
// ignore Chrome video internal error: https://crbug.com/809574
if (!error && message === 'ResizeObserver loop limit exceeded') {
return;
}
var traceback = error ? error.stack : '';
crash_manager.show_error({
type: _t("Odoo Client Error"),
@@ -41,6 +41,9 @@ var AppsMenu = Widget.extend({
* Open the first app in the list of apps
*/
openFirstApp: function () {
if (!this._apps.length) {
return
}
var firstApp = this._apps[0];
this._openApp(firstApp);
},
@@ -122,7 +122,8 @@ var FieldTimezoneMismatch = FieldSelection.extend({
});
var FieldReportLayout = relational_fields.FieldMany2One.extend({
supportedFieldTypes: ['many2one', 'selection'],
// this widget is not generic, so we disable its studio use
// supportedFieldTypes: ['many2one', 'selection'],
events: _.extend({}, relational_fields.FieldMany2One.prototype.events, {
'click img': '_onImgClicked',
}),
@@ -22,6 +22,7 @@ var map_title ={
var CrashManager = core.Class.extend({
init: function() {
this.active = true;
this.isConnected = true;
},
enable: function () {
this.active = true;
@@ -29,8 +30,29 @@ var CrashManager = core.Class.extend({
disable: function () {
this.active = false;
},
rpc_error: function(error) {
handleLostConnection: function () {
var self = this;
if (!this.isConnected) {
// already handled, nothing to do. This can happen when several
// rpcs are done in parallel and fail because of a lost connection.
return;
}
this.isConnected = false;
var delay = 2000;
core.bus.trigger('connection_lost');
setTimeout(function checkConnection() {
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
core.bus.trigger('connection_restored');
self.isConnected = true;
}).fail(function () {
// exponential backoff, with some jitter
delay = (delay * 1.5) + 500*Math.random();
setTimeout(checkConnection, delay);
});
}, delay);
},
rpc_error: function(error) {
if (!this.active) {
return;
}
@@ -38,16 +60,7 @@ var CrashManager = core.Class.extend({
return;
}
if (error.code === -32098) {
core.bus.trigger('connection_lost');
this.connection_lost = true;
var timeinterval = setInterval(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;
});
}, 2000);
this.handleLostConnection();
return;
}
var handler = core.crash_registry.get(error.data.name, true);
@@ -38,6 +38,10 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
FieldManagerMixin.init.call(this, this.model);
this.mode = params.mode || 'readonly';
this.handle = this.initialState.id;
// savingDef is used to ensure that we always wait for pending save
// operations to complete before checking if there are changes to
// discard when discardChanges is called
this.savingDef = $.when();
},
/**
* @override
@@ -102,14 +106,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return true;
},
/**
* Waits for the mutex to be unlocked and then calls _.discardChanges.
* Waits for the mutex to be unlocked and for changes to be saved, then
* calls _.discardChanges.
* This ensures that the confirm dialog isn't displayed directly if there is
* a pending 'write' rpc.
*
* @see _.discardChanges
*/
discardChanges: function (recordID, options) {
return this.mutex.exec(function () {})
return $.when(this.mutex.getUnlockedDef(), this.savingDef)
.then(this._discardChanges.bind(this, recordID || this.handle, options));
},
/**
@@ -183,11 +188,12 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// mutex-protected as commitChanges function of x2m has to be aware of
// all final changes made to a row.
var self = this;
return this.mutex.getUnlockedDef().then(function () {
this.savingDef = this.mutex.getUnlockedDef().then(function () {
return self.renderer.commitChanges(recordID || self.handle).then(function () {
return self.mutex.exec(self._saveRecord.bind(self, recordID, options));
});
});
return this.savingDef;
},
/**
* @override
@@ -130,8 +130,10 @@ var FieldManagerMixin = {
if ('offset' in data) {
params.offset = data.offset;
}
this.model.reload(data.id, params).then(function (db_id) {
data.on_success(self.model.get(db_id));
this.mutex.exec(function () {
return self.model.reload(data.id, params).then(function (db_id) {
data.on_success(self.model.get(db_id));
});
});
},
/**
@@ -210,15 +210,17 @@ var KanbanController = BasicController.extend({
*/
_onAddColumn: function (ev) {
var self = this;
this.model.createGroup(ev.data.value, this.handle).then(function () {
var state = self.model.get(self.handle, {raw: true});
var ids = _.pluck(state.data, 'res_id').filter(_.isNumber);
return self._resequenceColumns(ids);
}).then(function () {
return self.update({}, {reload: false});
}).then(function () {
self._updateButtons();
self.renderer.quickCreateToggleFold();
this.mutex.exec(function () {
return self.model.createGroup(ev.data.value, this.handle).then(function () {
var state = self.model.get(self.handle, {raw: true});
var ids = _.pluck(state.data, 'res_id').filter(_.isNumber);
return self._resequenceColumns(ids);
}).then(function () {
return self.update({}, {reload: false});
}).then(function () {
self._updateButtons();
self.renderer.quickCreateToggleFold();
});
});
},
/**
@@ -271,11 +273,16 @@ var KanbanController = BasicController.extend({
* @private
*/
_onButtonNew: function () {
var self = this;
var state = this.model.get(this.handle, {raw: true});
var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state);
if (this.on_create === 'quick_create' && quickCreateEnabled && state.data.length) {
// Activate the quick create in the first column
this.renderer.addQuickCreate();
// activate the quick create in the first column when the mutex is
// unlocked, to ensure that there is no pending re-rendering that
// would remove it (e.g. if we are currently adding a new column)
this.mutex.getUnlockedDef().then(function () {
self.renderer.addQuickCreate();
});
} else if (this.on_create && this.on_create !== 'quick_create') {
// Execute the given action
this.do_action(this.on_create, {
@@ -321,7 +321,7 @@ var PivotModel = AbstractModel.extend({
};
}
return {
colGroupBys: this.data.main_col.groupbys,
colGroupBys: this.data.main_col.root.groupbys,
context: this.data.context,
domain: this.data.domain,
compare: this.data.compare,
@@ -331,7 +331,7 @@ var PivotModel = AbstractModel.extend({
mainColWidth: this.data.main_col.width,
measures: this.data.measures,
rows: !isRaw && this._computeRows(),
rowGroupBys: this.data.main_row.groupbys,
rowGroupBys: this.data.main_row.root.groupbys,
sortedColumn: this.data.sorted_column,
};
},
@@ -432,22 +432,19 @@ var PivotModel = AbstractModel.extend({
return this._loadData();
}
var old_row_root = this.data.main_row.root;
var old_col_root = this.data.main_col.root;
var old_row = this.data.main_row;
var old_col = this.data.main_col;
return this._loadData().then(function () {
var new_groupby_length;
if (!('groupBy' in params) && !('pivot_row_groupby' in (params.context || {}))) {
// we only update the row groupbys according to the old groupbys
// if we don't have the key 'groupBy' in params. In that case,
// we want to have the full open state for the groupbys.
self._updateTree(old_row_root, self.data.main_row.root);
new_groupby_length = self._getHeaderDepth(self.data.main_row.root) - 1;
self.data.main_row.groupbys = old_row_root.groupbys.slice(0, new_groupby_length);
self._updateTree(old_row.root, self.data.main_row.root);
self._updateMainGroupBys(old_row, self.data.main_row);
}
self._updateTree(old_col_root, self.data.main_col.root);
new_groupby_length = self._getHeaderDepth(self.data.main_col.root) - 1;
self.data.main_row.groupbys = old_row_root.groupbys.slice(0, new_groupby_length);
self._updateTree(old_col.root, self.data.main_col.root);
self._updateMainGroupBys(old_col, self.data.main_col);
});
},
/**
@@ -1052,9 +1049,6 @@ var PivotModel = AbstractModel.extend({
}
}
this.data.main_row.groupbys = rowGroupBys;
this.data.main_col.groupbys = colGroupBys;
main_row_header.other_root = main_col_header;
main_col_header.other_root = main_row_header;
@@ -1123,6 +1117,19 @@ var PivotModel = AbstractModel.extend({
this._traverseTree(root.children[i], f, arg1, arg2, arg3);
}
},
/**
* Updates the groupBys that the main[row | col] holds
*
* @private
* @param {Object} old: The main abstract header before reload
* @param {Object} main: The main abstract header after reload
*
*/
_updateMainGroupBys: function (old, main) {
var new_groupby_length = this._getHeaderDepth(main.root) - 1;
var new_groupby_list = old.root.groupbys.slice(0, new_groupby_length);
main.root.groupbys = new_groupby_list;
},
/**
* @param {Object} old_tree
* @param {Object} new_tree
@@ -150,6 +150,61 @@ QUnit.module('relational_fields', {
}
}, function () {
QUnit.test('search more pager is reset when doing a new search', function (assert) {
assert.expect(6);
for(var i = 10; i < 180; i++) {
this.data.partner.records.push({
id: i,
display_name: "Partner " + i,
});
}
function stringToEvent ($element, string) {
for (var i = 0; i < string.length; i++) {
var keyAscii = string.charCodeAt(i);
$element.val($element.val()+string[i]);
$element.trigger($.Event('keyup', {which: keyAscii, keyCode:keyAscii}));
}
$element.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER, keyCode:$.ui.keyCode.ENTER}));
}
this.data.partner.fields.datetime.searchable = true;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="trululu"/>' +
'</group>' +
'</sheet>' +
'</form>',
archs: {
'partner,false,list': '<tree><field name="display_name"/></tree>',
'partner,false,search': '<search><field name="datetime"/><field name="display_name"/></search>',
},
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
form.$('.o_field_many2one input').click();
$dropdown.find('.o_m2o_dropdown_option:contains(Search)').mouseenter().click();
$('.modal .o_pager_next').click();
assert.strictEqual($('.o_pager_limit').text(), "1173", "there should be 173 records");
assert.strictEqual($('.o_pager_value').text(), "181-160", "should display the second page");
assert.strictEqual($('tr.o_data_row').length, 80, "should display 80 record");
stringToEvent($('.modal .o_searchview_input'), 'first');
assert.strictEqual($('.o_pager_limit').text(), "11", "there should be 1 record");
assert.strictEqual($('.o_pager_value').text(), "11-1", "should display the first page");
assert.strictEqual($('tr.o_data_row').length, 1, "should display 1 record");
form.destroy();
});
QUnit.test('x2many default_order multiple fields', function (assert) {
assert.expect(7);
@@ -446,6 +501,48 @@ QUnit.module('relational_fields', {
form.destroy();
});
form.destroy();
});
QUnit.test('quickly switch between pages in one2many list', function (assert) {
assert.expect(2);
this.data.partner.records[0].turtles = [1, 2, 3];
var readDefs = [$.when(), $.Deferred(), $.Deferred()];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree limit="1">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'read') {
var recordID = args.args[0][0];
return $.when(readDefs[recordID - 1]).then(_.constant(result));
}
return result;
},
res_id: 1,
});
form.$('.o_field_widget[name=turtles] .o_pager_next').click();
form.$('.o_field_widget[name=turtles] .o_pager_next').click();
readDefs[1].resolve();
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_data_cell').text(), 'donatello');
readDefs[2].resolve();
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_data_cell').text(), 'raphael');
QUnit.test('many2many read, field context is properly sent', function (assert) {
assert.expect(4);
@@ -7189,6 +7189,69 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('call canBeRemoved while saving', function (assert) {
assert.expect(10);
this.data.partner.onchanges = {
foo: function (obj) {
obj.display_name = obj.foo === 'trigger onchange' ? 'changed' : 'default';
},
};
var onchangeDef;
var createDef = $.Deferred();
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="display_name"/><field name="foo"/></form>',
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'onchange') {
return $.when(onchangeDef).then(_.constant(result));
}
if (args.method === 'create') {
return $.when(createDef).then(_.constant(result));
}
return result;
},
});
// edit foo to trigger a delayed onchange
onchangeDef = $.Deferred();
testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default');
// save (will wait for the onchange to return), and will be delayed as well
testUtils.dom.click(form.$buttons.find('.o_form_button_save'));
assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default');
// simulate a click on the breadcrumbs to leave the form view
form.canBeRemoved();
assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default');
// unlock the onchange
onchangeDef.resolve();
assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'changed');
// unlock the create
createDef.resolve();
assert.hasClass(form.$('.o_form_view'), 'o_form_readonly');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').text(), 'changed');
assert.containsNone(document.body, '.modal',
"should not display the 'Changes will be discarded' dialog");
form.destroy();
});
QUnit.module('FormViewTABMainButtons');
QUnit.test('using tab in an empty required string field should not move to the next field',function(assert) {
@@ -2110,6 +2110,67 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('quick create record while adding a new column', function (assert) {
assert.expect(10);
var def = $.Deferred();
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create">' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'name_create' && args.model === 'product') {
return def.then(_.constant(result));
}
return result;
},
});
assert.containsN(kanban, '.o_kanban_group', 2);
assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2);
// add a new column
assert.containsOnce(kanban, '.o_column_quick_create');
assert.isNotVisible(kanban.$('.o_column_quick_create input'));
testUtils.dom.click(kanban.$('.o_quick_create_folded'));
assert.isVisible(kanban.$('.o_column_quick_create input'));
testUtils.fields.editInput(kanban.$('.o_column_quick_create input'), 'new column');
testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add'));
assert.containsN(kanban, '.o_kanban_group', 2);
// click to add a new record
testUtils.dom.click(kanban.$buttons.find('.o-kanban-button-new'));
// should wait for the column to be created (and view to be re-rendered
// before opening the quick create
assert.containsNone(kanban, '.o_kanban_quick_create');
// unlock column creation
def.resolve();
assert.containsN(kanban, '.o_kanban_group', 3);
assert.containsOnce(kanban, '.o_kanban_quick_create');
// quick create record in first column
testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'new record');
testUtils.dom.click(kanban.$('.o_kanban_quick_create .o_kanban_add'));
assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 3);
kanban.destroy();
});
QUnit.test('many2many_tags in kanban views', function (assert) {
assert.expect(12);
@@ -923,6 +923,80 @@ QUnit.module('Views', {
pivot.destroy();
});
QUnit.test('correctly remove pivot_ keys from the context', function (assert) {
assert.expect(5);
this.data.partner.fields.amount = {string: "Amount", type: "float"};
// Equivalent to loading with default filter
var pivot = createView({
View: PivotView,
model: "partner",
data: this.data,
arch: '<pivot>' +
'<field name="date" interval="day" type="col"/>' +
'<field name="amount" type="measure"/>' +
'</pivot>',
viewOptions: {
context: {
pivot_measures: ['foo'],
pivot_column_groupby: ['customer'],
pivot_row_groupby: ['product_id'],
},
},
});
// Equivalent to unload the filter
var reloadParams = {
context: {},
};
pivot.reload(reloadParams);
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: ['customer'],
pivot_measures: ['foo'],
pivot_row_groupby: ['product_id'],
}, "context should be correct");
// Let's get rid of the rows groupBy
pivot.$('tbody .o_pivot_header_cell_opened').click();
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: ['customer'],
pivot_measures: ['foo'],
pivot_row_groupby: [],
}, "context should be correct");
// And now, get rid of the col groupby
pivot.$('thead .o_pivot_header_cell_opened').click();
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: [],
pivot_measures: ['foo'],
pivot_row_groupby: [],
}, "context should be correct");
pivot.$('tbody .o_pivot_header_cell_closed').click();
pivot.$('.o_pivot_field_menu li[data-field=product_id] a').click();
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: [],
pivot_measures: ['foo'],
pivot_row_groupby: ['product_id'],
}, "context should be correct");
pivot.$('thead .o_pivot_header_cell_closed').click();
pivot.$('.o_pivot_field_menu li[data-field=customer] a').click();
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: ['customer'],
pivot_measures: ['foo'],
pivot_row_groupby: ['product_id'],
}, "context should be correct");
pivot.destroy();
});
QUnit.test('correctly uses pivot_ keys from the context', function (assert) {
assert.expect(7);
+11 -1
View File
@@ -416,7 +416,17 @@ class Web_Editor(http.Controller):
# Create a view to extend the template which adds the original file to link the new modified version instead
IrUiView = request.env["ir.ui.view"]
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(lambda v: v.arch.find(url) >= 0)
def views_linking_url(view):
"""
Returns whether the view arch has some html link tag linked to the url.
(note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.)
"""
return bool(etree.XML(view.arch).xpath("link[@href='{}']".format(url)))
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url)
new_view = {
'name': custom_url,
'key': 'web_editor.scss_%s' % str(uuid.uuid4())[:6],
+1 -1
View File
@@ -21,7 +21,7 @@ Odoo Web tours.
'data/web_tour_demo.xml',
],
'qweb': [
"static/src/xml/*.xml",
"static/src/xml/debug_manager.xml",
],
'auto_install': True
}
+1
View File
@@ -8,6 +8,7 @@ var _t = core._t;
var Tip = Widget.extend({
template: "Tip",
xmlDependencies: ['/web_tour/static/src/xml/tip.xml'],
events: {
click: '_onTipClicked',
mouseenter: "_to_info_mode",
+21 -6
View File
@@ -32,7 +32,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
this.tours = {};
this.consumed_tours = consumed_tours || [];
this.running_tour = local_storage.getItem(get_running_key());
this.running_step_delay = parseInt(local_storage.getItem(get_running_delay_key()), 10) || 10;
this.running_step_delay = parseInt(local_storage.getItem(get_running_delay_key()), 10) || 0;
this.edition = (_.last(session.server_version_info) === 'e') ? 'enterprise' : 'community';
this._log = [];
console.log('Tour Manager is ready. running_tour=' + this.running_tour);
@@ -171,7 +171,10 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
if (this.running_tour && this.running_tour_timeout === undefined) {
this._set_running_tour_timeout(this.running_tour, this.active_tooltips[this.running_tour]);
}
this._check_for_tooltip(this.active_tooltips[tour_name], tour_name);
var self = this;
setTimeout(function () {
self._check_for_tooltip(self.active_tooltips[tour_name], tour_name);
});
} else {
for (var tourName in this.active_tooltips) {
var tip = this.active_tooltips[tourName];
@@ -371,11 +374,23 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
},
_to_next_running_step: function (tip, tour_name) {
if (this.running_tour !== tour_name) return;
var self = this;
this._stop_running_tour_timeout();
if (this.running_step_delay) {
// warning: due to the delay, it may happen that the $anchor isn't
// in the DOM anymore when exec is called, either because:
// - it has been removed from the DOM meanwhile and the tip's
// selector doesn't match anything anymore
// - it has been re-rendered and thus the selector still has a match
// in the DOM, but executing the step with that $anchor won't work
_.delay(exec, this.running_step_delay);
} else {
exec();
}
var action_helper = new RunningTourActionHelper(tip.widget);
_.delay((function () {
do_before_unload(this._consume_tip.bind(this, tip, tour_name));
function exec() {
var action_helper = new RunningTourActionHelper(tip.widget);
do_before_unload(self._consume_tip.bind(self, tip, tour_name));
if (typeof tip.run === "function") {
tip.run.call(tip.widget, action_helper);
@@ -385,7 +400,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
} else {
action_helper.auto();
}
}).bind(this), this.running_step_delay);
}
},
/**
+10 -16
View File
@@ -61,12 +61,8 @@ return session.is_bound.then(function () {
}, 500);
var observer = new MutationObserver(check_tooltip);
var start_service = (function () {
var load_def;
return function (observe) {
if (load_def === undefined && observe && session.is_frontend) {
load_def = ajax.loadXML('/web_tour/static/src/xml/tip.xml', QWeb);
}
var def = $.Deferred();
$(function () {
@@ -74,18 +70,16 @@ return session.is_bound.then(function () {
* Once the DOM is ready, we still have to wait all the modules are loaded before completing the tours
* registration and starting listening for DOM mutations.
*/
$.when(load_def).then(function () {
_.defer(function () {
tour_manager._register_all(observe);
if (observe) {
observer.observe(document.body, {
attributes: true,
childList: true,
subtree: true,
});
}
def.resolve();
});
_.defer(function () {
tour_manager._register_all(observe);
if (observe) {
observer.observe(document.body, {
attributes: true,
childList: true,
subtree: true,
});
}
def.resolve();
});
});
return def;
+50 -1
View File
@@ -74,7 +74,7 @@ class View(models.Model):
view._create_website_specific_pages_for_view(website_specific_view,
view.env['website'].browse(current_website_id))
for inherit_child in view.inherit_children_ids.filter_duplicate():
for inherit_child in view.inherit_children_ids.filter_duplicate().sorted(key=lambda v: (v.priority, v.id)):
if inherit_child.website_id.id == current_website_id:
# In the case the child was already specific to the current
# website, we cannot just reattach it to the new specific
@@ -92,6 +92,55 @@ class View(models.Model):
return True
@api.multi
def _get_specific_views(self):
""" Given a view, return a record set containing all the specific views
for that view's key.
If the given view is already specific, it will also return itself.
"""
self.ensure_one()
domain = [('key', '=', self.key), ('website_id', '!=', False)]
return self.with_context(active_test=False).search(domain)
def _load_records_write(self, values):
""" During module update, when updating a generic view, we should also
update its specific views (COW'd).
Note that we will only update unmodified fields. That will mimic the
noupdate behavior on views having an ir.model.data.
"""
if self.type == 'qweb' and not self.website_id:
# Update also specific views
for cow_view in self._get_specific_views():
authorized_vals = {}
for key in values:
if cow_view[key] == self[key]:
authorized_vals[key] = values[key]
cow_view.write(authorized_vals)
super(View, self)._load_records_write(values)
def _load_records_create(self, values):
""" During module install, when creating a generic child view, we should
also create that view under specific view trees (COW'd).
Top level view (no inherit_id) do not need that behavior as they
will be shared between websites since there is no specific yet.
"""
records = super(View, self)._load_records_create(values)
for record in records:
if record.type == 'qweb' and record.inherit_id and not record.website_id and not record.inherit_id.website_id:
specific_parent_views = record.with_context(active_test=False).search([
('key', '=', record.inherit_id.key),
('website_id', '!=', None),
])
for specific_parent_view in specific_parent_views:
record.copy({
# Set key to avoid copy() to generate an unique key as
# we want the specific view to have the same key
'key': record.key,
'inherit_id': specific_parent_view.id,
'website_id': specific_parent_view.website_id.id,
})
return records
@api.multi
def unlink(self):
'''This implements COU (copy-on-unlink). When deleting a generic page
+1 -1
View File
@@ -1253,7 +1253,7 @@ class WebsiteRedirect(models.Model):
_order = "sequence, id"
_rec_name = 'url_from'
redirect_type = fields.Selection([('301', 'Moved permanently (301)'), ('302', 'Moved temporarily (302)')], string='Redirection Type', required=True, oldname='type')
redirect_type = fields.Selection([('301', 'Moved permanently (301)'), ('302', 'Moved temporarily (302)')], string='Redirection Type', required=True, default='301)
url_from = fields.Char('Redirect From', required=True)
url_to = fields.Char('Redirect To', required=True)
website_id = fields.Many2one('website', 'Website')
@@ -81,7 +81,7 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
var $group = this.$dialog.find('div.form-group');
$group.removeClass('mb0');
var $add = $('<div/>', {'class': 'form-group mb0'})
var $add = $('<div/>', {'class': 'form-group mb0 row'})
.append($('<span/>', {'class': 'offset-md-3 col-md-9 text-left'})
.append(qweb.render('web_editor.components.switch', {id: 'switch_addTo_menu', label: _t("Add to menu")})));
$add.find('input').prop('checked', true);
@@ -176,8 +176,6 @@
font-family: Roboto, $font-family-sans-serif;
.o_tooltip_container {
display: inline-flex;
align-items: center;
transition: opacity 1s linear;
> .o_tooltip {
@@ -3,7 +3,7 @@
<div t-name="website.homepage_editor_welcome_message" class="container text-center o_homepage_editor_welcome_message">
<h2 class="mt0">Welcome to your <b>Homepage</b>!</h2>
<p class="lead d-none d-md-block">Let's start designing.</p>
<div class="o_tooltip_container d-none d-md-block fade">Follow all the <div class="o_tooltip bottom"/> signs to get your website ready in no time.</div>
<div class="o_tooltip_container d-none d-md-inline-flex fade">Follow all the <div class="o_tooltip bottom"/> signs to get your website ready in no time.</div>
</div>
<div t-name="website.leaving_current_page_edition">
<p>What do you want to do?</p>
+98
View File
@@ -622,6 +622,104 @@ class TestCowViewSaving(common.TransactionCase):
views = View.with_context(website_id=1).get_related_views('B')
self.assertEqual(views.mapped('key'), ['B', 'I', 'II'], "Should only return the specific tree")
def test_cow_inherit_children_order(self):
""" COW method should loop on inherit_children_ids in correct order
when copying them on the new specific tree.
Correct order is the same as the one when applying view arch:
PRIORITY, ID
And not the default one from ir.ui.view (NAME, PRIORIRTY, ID).
"""
self.inherit_view.copy({
'name': 'alphabetically before "Extension"',
'key': '_test.alphabetically_first',
'arch': '<div position="replace"><p>COMPARE</p></div>',
})
# Next line should not crash, COW loop on inherit_children_ids should be sorted correctly
self.base_view.with_context(website_id=1).write({'name': 'Product (W1)'})
def test_module_new_inherit_view_on_parent_already_forked(self):
""" If a generic parent view is copied (COW) and that another module
creates a child view for that generic parent, all the COW views
should also get a copy of that new child view.
Typically, a parent view (website_sale.product) is copied (COW)
and then wishlist module is installed.
Wishlist views inhering from website_sale.product are added to the
generic `website_sale.product`. But it should also be added to the
COW `website_sale.product` to activate the module views for that
website.
"""
Website = self.env['website']
View = self.env['ir.ui.view']
# Simulate website_sale product view
self.base_view.write({'name': 'Product', 'key': '_website_sale.product'})
# Trigger cow on website_sale hierarchy for website 1
self.base_view.with_context(website_id=1).write({'name': 'Product (W1)'})
# Simulate website_sale_comparison install
View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
'name': 'Add to comparison in product page',
'mode': 'extension',
'inherit_id': self.base_view.id,
'arch': '<div position="replace"><p>COMPARE</p></div>',
'key': '_website_sale_comparison.product_add_to_compare',
})])
specific_view = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale.product')
specific_view_arch = specific_view.read_combined(['arch'])['arch']
self.assertEqual(specific_view.website_id.id, 1, "Ensure we got specific view to perform the checks against")
self.assertEqual(specific_view_arch, '<p>COMPARE</p>', "When a module creates an inherited view (on a generic tree), it should also create that view in the specific COW'd tree.")
# Simulate website_sale_comparison update
View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
'arch': '<div position="replace"><p>COMPARE EDITED</p></div>',
})])
specific_view_arch = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale.product').read_combined(['arch'])['arch']
self.assertEqual(specific_view_arch, '<p>COMPARE EDITED</p>', "When a module updates an inherited view (on a generic tree), it should also update the copies of that view (COW).")
# Test fields that should not be COW'd
random_view_id = View.search([], limit=1).id
View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
'website_id': None,
'inherit_id': random_view_id,
})])
w1_specific_child_view = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale_comparison.product_add_to_compare')
generic_child_view = Website.with_context(load_all_views=True).viewref('_website_sale_comparison.product_add_to_compare')
self.assertEqual(w1_specific_child_view.website_id.id, 1, "website_id is a prohibited field when COWing views during _load_records")
self.assertEqual(w1_specific_child_view.inherit_id.id != random_view_id, True, "inherit_id is a prohibited field when COWing views during _load_records")
self.assertEqual(generic_child_view.inherit_id.id, random_view_id, "prohibited fields only concerned write on COW'd view. Generic should still considere these fields")
# Set back the generic view as parent for the rest of the test
generic_child_view.inherit_id = self.base_view
# Don't update fields from COW'd view if these fields have been modified from original view
new_website = Website.create({'name': 'New Website'})
self.base_view.with_context(website_id=new_website.id).write({'name': 'Product (new_website)'})
new_website_specific_child_view = Website.with_context(load_all_views=True, website_id=new_website.id).viewref('_website_sale_comparison.product_add_to_compare')
new_website_specific_child_view.priority = 6
View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
'priority': 3,
})])
self.assertEqual(generic_child_view.priority, 3, "XML update should be written on the Generic View")
self.assertEqual(w1_specific_child_view.priority, 3, "XML update should be written on the specific view if the fields have not been modified on that specific view")
self.assertEqual(new_website_specific_child_view.priority, 6, "XML update should NOT be written on the specific view if the fields have been modified on that specific view")
# Simulate website_sale update on top level view
self.env['ir.model.data'].create({
'module': '_website_sale',
'name': 'product',
'model': self.base_view._name,
'res_id': self.base_view.id,
})
View._load_records([dict(xml_id='_website_sale.product', values={
'website_meta_title': 'A bug got fixed by updating this field',
})])
all_title_updated = specific_view.website_meta_title == self.base_view.website_meta_title == "A bug got fixed by updating this field"
self.assertEqual(all_title_updated, True, "Update on top level generic views should also be applied on specific views")
class Crawler(HttpCase):
def setUp(self):
+13
View File
@@ -8,6 +8,19 @@
<menuitem name="Forum" id="menu_website_forum_global"
parent="website.menu_website_global_configuration" sequence="170" groups="website.group_website_designer"/>
<record id="badge_form_view_forum" model="ir.ui.view">
<field name="name">forum.badge_form_view</field>
<field name="model">gamification.badge</field>
<field name="inherit_id" ref="gamification.badge_form_view"/>
<field name="arch" type="xml">
<field name="challenge_ids" position="after">
<group>
<field name="level" />
</group>
</field>
</field>
</record>
<!-- FORUM VIEWS -->
<record id="view_forum_forum_list" model="ir.ui.view">
<field name="name">forum.forum.list</field>
+1 -1
View File
@@ -568,7 +568,7 @@ class WebsiteSale(ProductConfiguratorController):
except ValidationError:
error["vat"] = 'error'
if [err for err in error.items() if err == 'missing']:
if [err for err in error.values() if err == 'missing']:
error_message.append(_('Some required fields are empty.'))
return error, error_message
@@ -21,7 +21,7 @@ tour.register('shop_buy_product', {
},
{
content: "select conference chair",
trigger: '.oe_product_cart a:contains("Conference Chair")',
trigger: '.oe_product_cart:first a:contains("Conference Chair")',
},
{
content: "select Conference Chair Aluminium",
@@ -3,6 +3,7 @@ from odoo import http, _
from odoo.http import request
from odoo.addons.website_sale.controllers.main import WebsiteSale
import json
from collections import OrderedDict
class WebsiteSaleProductComparison(WebsiteSale):
@@ -24,12 +25,12 @@ class WebsiteSaleProductComparison(WebsiteSale):
att_name = var.attribute_id.name
if not product.attribute_value_ids: # create_variant = False
continue
res.setdefault(cat_name, {}).setdefault(att_name, [' - '] * len(products))
res.setdefault(cat_name, OrderedDict()).setdefault(att_name, [' - '] * len(products))
val = product.attribute_value_ids.filtered(lambda x: x.attribute_id == var.attribute_id)
if val:
res[cat_name][att_name][num] = val[0].name
values['specs'] = res
values['compute_currency'] = self._get_compute_currency_and_context(products[:1])[0]
values['compute_currency'] = self._get_compute_currency_and_context(products[:1].product_tmpl_id)[0]
return request.render("website_sale_comparison.product_compare", values)
@http.route(['/shop/get_product_data'], type='json', auth="public", website=True)
@@ -37,7 +38,7 @@ class WebsiteSaleProductComparison(WebsiteSale):
ret = {}
pricelist_context, pricelist = self._get_pricelist_context()
prods = request.env['product.product'].with_context(pricelist_context, display_default_code=False).search([('id', 'in', product_ids)])
compute_currency = self._get_compute_currency(pricelist, prods[:1])
compute_currency = self._get_compute_currency(pricelist, prods[:1].product_tmpl_id)
if cookies is not None:
ret['cookies'] = json.dumps(request.env['product.product'].search([('id', 'in', list(set(product_ids + cookies)))]).ids)
@@ -47,14 +47,9 @@ tour.register('shop_wishlist', {
},
},
{
content: "check that logged in and search for Customizable Desk",
extra_trigger: "li span:contains('Mitchell Admin')",
trigger: 'form input[name="search"]',
run: "text Customizable Desk",
},
{
content: "submit search",
trigger: 'form:has(input[name="search"]) .oe_search_button',
content: "check that logged in",
trigger: "li span:contains('Mitchell Admin')",
run: function () {},
},
{
content: "click on Customizable Desk",
@@ -113,7 +113,7 @@
</a>
</div>
<div class="col-auto text-right ml-auto mt-2 mb-2">
<a t-if="slide.download_security and not is_embedded" target="_top" t-attf-href="/slides/slide/#{slug(slide)}/download" title="Download" id="download">
<a t-if="slide.download_security != 'none' and not is_embedded" target="_top" t-attf-href="/slides/slide/#{slug(slide)}/download" title="Download" id="download">
<i class="fa fa-download fa-2x" role="img" aria-label="Download"></i>
</a>
<span id="fullscreen" title="View fullscreen" role="img" aria-label="Fullscreen">
+1
View File
@@ -21,3 +21,4 @@ Medardo Rodríguez med@merchise.org https://github.com/med-merchise
Abel Firvida firvida@merchise.org
Abel Firvida abel@merchise.org
Mónica Díaz Pena monicadp@merchise.org https://github.com/mdpena
Larisa González larisa@merchise.org
+9
View File
@@ -0,0 +1,9 @@
India, 2019-01-11
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this declaration.
Signed,
Dipak Shah dipssah@gmail.com https://github.com/dipsshah42
+11
View File
@@ -0,0 +1,11 @@
Germany, 10.01.2019
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Jörg Ricardo Schumacher eHanseJoerg@users.noreply.github.com https://github.com/eHanseJoerg
+9
View File
@@ -0,0 +1,9 @@
Ukraine, 2019-01-09
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this declaration.
Signed,
Vyshnevska Iryna i.vyshnevska@mobilunity.com https://github.com/i-vyshnevska/
+11
View File
@@ -0,0 +1,11 @@
Bosnia and Herzegovina, 2019-01-12
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Sasa Stanisljevic mcse.sasa@gmail.com https://github.com/stanisljevic
+1 -1
View File
@@ -397,7 +397,7 @@ class IrActionsServer(models.Model):
# Create
crud_model_id = fields.Many2one('ir.model', string='Create/Write Target Model',
oldname='srcmodel_id', help="Model for record creation / update. Set this field only to specify a different model than the base model.")
crud_model_name = fields.Char(related='crud_model_id.name', readonly=True)
crud_model_name = fields.Char(related='crud_model_id.model', string='Target Model', readonly=True)
link_field_id = fields.Many2one('ir.model.fields', string='Link using field',
help="Provide the field used to link the newly created record "
"on the record on used by the server action.")
+2 -1
View File
@@ -457,7 +457,8 @@ class Module(models.Model):
modules_to_remove = self.mapped('name')
self.env['ir.model.data']._module_data_uninstall(modules_to_remove)
self._remove_copied_views()
self.write({'state': 'uninstalled', 'latest_version': False})
# we deactivate prefetching to not try to read a column that has been deleted
self.with_context(prefetch_fields=False).write({'state': 'uninstalled', 'latest_version': False})
return True
@api.multi
+2 -2
View File
@@ -257,10 +257,10 @@ class Partner(models.Model):
for partner in self:
partner.tz_offset = datetime.datetime.now(pytz.timezone(partner.tz or 'GMT')).strftime('%z')
@api.depends('user_ids.share')
@api.depends('user_ids.share', 'user_ids.active')
def _compute_partner_share(self):
for partner in self:
partner.partner_share = not partner.user_ids or any(user.share for user in partner.user_ids)
partner.partner_share = not partner.user_ids or not any(not user.share for user in partner.user_ids)
@api.depends(lambda self: self._display_address_depends())
def _compute_contact_address(self):
@@ -129,14 +129,14 @@ class MergePartnerAutomatic(models.TransientModel):
# unique key treated
query = """
UPDATE "%(table)s" as ___tu
SET %(column)s = %%s
SET "%(column)s" = %%s
WHERE
%(column)s = %%s AND
"%(column)s" = %%s AND
NOT EXISTS (
SELECT 1
FROM "%(table)s" as ___tw
WHERE
%(column)s = %%s AND
"%(column)s" = %%s AND
___tu.%(value)s = ___tw.%(value)s
)""" % query_dic
for partner in src_partners:
@@ -144,7 +144,7 @@ class MergePartnerAutomatic(models.TransientModel):
else:
try:
with mute_logger('odoo.sql_db'), self._cr.savepoint():
query = 'UPDATE "%(table)s" SET %(column)s = %%s WHERE %(column)s IN %%s' % query_dic
query = 'UPDATE "%(table)s" SET "%(column)s" = %%s WHERE "%(column)s" IN %%s' % query_dic
self._cr.execute(query, (dst_partner.id, tuple(src_partners.ids),))
# handle the recursivity with parent relation
@@ -211,7 +211,6 @@ tour.register('main_flow_tour', {
position: 'bottom',
}, {
trigger: ".breadcrumb-item:first",
extra_trigger: ".o_form_readonly", // FIXME: this is required due to an issue in tour_manager (see [*])
content: _t("Use the breadcrumbs to <b>go back to products</b>."),
position: "bottom"
}, {
@@ -542,12 +541,10 @@ tour.register('main_flow_tour', {
trigger: ".o_menu_sections a[data-menu-xmlid='mrp.menu_mrp_manufacturing']",
content: _t('Click on Operations menuitem'),
position: 'bottom',
edition: 'enterprise',
}, {
trigger: ".o_menu_sections a[data-menu-xmlid='mrp.menu_mrp_production_action']",
content: _t('Open manufacturing orders'),
position: 'bottom',
edition: 'enterprise',
}, {
trigger: '.o_data_row:has(.o_data_cell:contains("the_flow.product")):first',
content: _t('Select the generated manufacturing order'),
@@ -755,7 +752,6 @@ tour.register('main_flow_tour', {
}, {
edition: "enterprise",
trigger: ".o_selected_row .o_field_widget[name=name]",
extra_trigger: ".o_selected_row .o_field_widget[name=partner_id] .o_external_button", // FIXME: this is required due to an issue in tour_manager (see [*])
content: _t('Let\'s enter a name.'),
position: "bottom",
run: "text the_flow.statement.line",
@@ -781,16 +777,3 @@ tour.register('main_flow_tour', {
position: "bottom",
}]);
});
/*
* [*] FIXME: issue in tour_manager:
* The JQuery element of a step is registered as soon as its trigger and
* extra_trigger elements are visible in the DOM, but it's action is delayed
* to handle the 'running_step_delay' option (0 by default, but even with 0,
* the action is executed after finishing the current execution block, which
* may produce a re-rendering of the part of the DOM containing the element).
* When this happens, the action is executed on an jquery element not present
* in the DOM anymore.
* To avoid this, we add an 'extra_trigger' to wait for the updated jquery
* element before activating the step (and thus registering the JQuery element).
*/
@@ -26,3 +26,4 @@ access_test_new_api_monetary_base,access_test_new_api_monetary_base,model_test_n
access_test_new_api_monetary_related,access_test_new_api_monetary_related,model_test_new_api_monetary_related,,1,1,1,1
access_test_new_api_monetary_custom,access_test_new_api_monetary_custom,model_test_new_api_monetary_custom,,1,1,1,1
access_test_new_api_monetary_inherits,access_test_new_api_monetary_inherits,model_test_new_api_monetary_inherits,,1,1,1,1
access_test_new_api_field_with_caps,access_test_new_api_field_with_caps,model_test_new_api_field_with_caps,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
26 access_test_new_api_monetary_related access_test_new_api_monetary_related model_test_new_api_monetary_related 1 1 1 1
27 access_test_new_api_monetary_custom access_test_new_api_monetary_custom model_test_new_api_monetary_custom 1 1 1 1
28 access_test_new_api_monetary_inherits access_test_new_api_monetary_inherits model_test_new_api_monetary_inherits 1 1 1 1
29 access_test_new_api_field_with_caps access_test_new_api_field_with_caps model_test_new_api_field_with_caps 1 1 1 1
+7
View File
@@ -504,3 +504,10 @@ class MonetaryInherits(models.Model):
monetary_id = fields.Many2one('test_new_api.monetary_base', required=True, ondelete='cascade')
currency_id = fields.Many2one('res.currency')
class FieldWithCaps(models.Model):
_name = 'test_new_api.field_with_caps'
_description = 'Model with field defined with capital letters'
pArTneR_321_id = fields.Many2one('res.partner')
@@ -130,3 +130,22 @@ class One2manyCase(TransactionCase):
res_movies_not_of_edition_name = self.Movie.search([('editions', '!=', one_movie_edition.name)])
self.assertItemsEqual(t(res_movies_not_of_edition_name), t(movies.filtered(lambda r: one_movie_edition not in r.editions)))
def test_merge_partner(self):
model = self.env['test_new_api.field_with_caps']
partner = self.env['res.partner']
p1 = partner.create({'name': 'test1'})
p2 = partner.create({'name': 'test2'})
model1 = model.create({'pArTneR_321_id': p1.id})
model2 = model.create({'pArTneR_321_id': p2.id})
self.env['base.partner.merge.automatic.wizard']._merge((p1 + p2).ids, p1)
self.assertFalse(p2.exists())
self.assertTrue(p1.exists())
self.assertEqual(model1.pArTneR_321_id, p1)
self.assertTrue(model2.exists())
self.assertEqual(model2.pArTneR_321_id, p1)
+12 -4
View File
@@ -2778,14 +2778,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# retrieve results from records; this takes values from the cache and
# computes remaining fields
self = self.with_prefetch(self._prefetch.copy())
data = {record: {'id': record.id} for record in self}
missing = set()
use_name_get = (load == '_classic_read')
for name in (stored + inherited + computed):
convert = self._fields[name].convert_to_read
# read every field with prefetching limited to self; this avoids
# restrict the prefetching of self's model to self; this avoids
# computing fields on a larger recordset than self
for record in self.with_prefetch():
self._prefetch[self._name] = set(self._ids)
for record in self:
try:
data[record][name] = convert(record[name], record, use_name_get)
except MissingError:
@@ -3738,6 +3740,12 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
modified_ids = {row[0] for row in cr.fetchall()}
self.browse(modified_ids).modified(['parent_path'])
def _load_records_write(self, values):
self.write(values)
def _load_records_create(self, values):
return self.create(values)
def _load_records(self, data_list, update=False):
""" Create or update records of this model, and assign XMLIDs.
@@ -3795,7 +3803,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# update existing records
for data in to_update:
data['record'].write(data['values'])
data['record']._load_records_write(data['values'])
# determine existing parents for new records
for parent_model, parent_field in self._inherits.items():
@@ -3822,7 +3830,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
_logger.warning("Creating record %s in module %s.", data['xml_id'], module)
# create records
records = self.create([data['values'] for data in to_create])
records = self._load_records_create([data['values'] for data in to_create])
for data, record in zip(to_create, records):
data['record'] = record
+3 -3
View File
@@ -128,17 +128,17 @@ def initialize_sys_path():
global ad_paths
global hooked
dd = tools.config.addons_data_dir
dd = os.path.normcase(tools.config.addons_data_dir)
if os.access(dd, os.R_OK) and dd not in ad_paths:
ad_paths.append(dd)
for ad in tools.config['addons_path'].split(','):
ad = os.path.abspath(tools.ustr(ad.strip()))
ad = os.path.normcase(os.path.abspath(tools.ustr(ad.strip())))
if ad not in ad_paths:
ad_paths.append(ad)
# add base module path
base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))
base_path = os.path.normcase(os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons')))
if base_path not in ad_paths and os.path.isdir(base_path):
ad_paths.append(base_path)
+7 -3
View File
@@ -529,6 +529,7 @@ class ChromeBrowser():
'--user-data-dir': self.user_data_dir,
'--disable-translate': '',
'--window-size': self.window_size,
'--remote-debugging-address': HOST,
'--remote-debugging-port': str(self.devtools_port),
'--no-sandbox': '',
}
@@ -571,7 +572,7 @@ class ChromeBrowser():
command = os.path.join('json', command).strip('/')
while timeout > 0:
try:
url = werkzeug.urls.url_join('http://127.0.0.1:%s/' % self.devtools_port, command)
url = werkzeug.urls.url_join('http://%s:%s/' % (HOST, self.devtools_port), command)
self._logger.info('Url : %s', url)
r = requests.get(url, timeout=3)
if r.ok:
@@ -881,7 +882,7 @@ class HttpCase(TransactionCase):
odoo.http.root.session_store.save(session)
if self.browser:
self._logger.info('Setting session cookie in browser')
self.browser.set_cookie('session_id', self.session_id, '/', '127.0.0.1')
self.browser.set_cookie('session_id', self.session_id, '/', HOST)
def browser_js(self, url_path, code, ready='', login=None, timeout=60, **kw):
""" Test js code running in the browser
@@ -905,7 +906,10 @@ class HttpCase(TransactionCase):
try:
self.authenticate(login, login)
url = "http://%s:%s%s" % (HOST, PORT, url_path or '/')
base_url = "http://%s:%s" % (HOST, PORT)
ICP = self.env['ir.config_parameter']
ICP.set_param('web.base.url', base_url)
url = "%s%s" % (base_url, url_path or '/')
self._logger.info('Open "%s" in browser', url)
if odoo.tools.config['logfile']: