[MERGE] forward port branch 12.0 up to f854e01a98
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">…</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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&value=%s&width=%s&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&value=%s&width=%s&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&value=%s&width=%s&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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
+5
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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']:
|
||||
|
||||
Reference in New Issue
Block a user