[MERGE] forward port branch 10.0 up to 13dc704
This commit is contained in:
@@ -70,6 +70,13 @@ class AccountMove(models.Model):
|
||||
if self.env.context.get('default_journal_type'):
|
||||
return self.env['account.journal'].search([('type', '=', self.env.context['default_journal_type'])], limit=1).id
|
||||
|
||||
@api.multi
|
||||
@api.depends('line_ids.partner_id')
|
||||
def _compute_partner_id(self):
|
||||
for move in self:
|
||||
partner = move.line_ids.mapped('partner_id')
|
||||
move.partner_id = partner.id if len(partner) == 1 else False
|
||||
|
||||
name = fields.Char(string='Number', required=True, copy=False, default='/')
|
||||
ref = fields.Char(string='Reference', copy=False)
|
||||
date = fields.Date(required=True, states={'posted': [('readonly', True)]}, index=True, default=fields.Date.context_today)
|
||||
@@ -84,7 +91,7 @@ class AccountMove(models.Model):
|
||||
'in \'Posted\' status.')
|
||||
line_ids = fields.One2many('account.move.line', 'move_id', string='Journal Items',
|
||||
states={'posted': [('readonly', True)]}, copy=True)
|
||||
partner_id = fields.Many2one('res.partner', related='line_ids.partner_id', string="Partner", store=True, readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', compute='_compute_partner_id', string="Partner", store=True, readonly=True)
|
||||
amount = fields.Monetary(compute='_amount_compute', store=True)
|
||||
narration = fields.Text(string='Internal Note')
|
||||
company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True,
|
||||
@@ -150,6 +157,7 @@ class AccountMove(models.Model):
|
||||
if not move.journal_id.update_posted:
|
||||
raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.'))
|
||||
if self.ids:
|
||||
self._check_lock_date()
|
||||
self._cr.execute('UPDATE account_move '\
|
||||
'SET state=%s '\
|
||||
'WHERE id IN %s', ('draft', tuple(self.ids),))
|
||||
|
||||
@@ -134,12 +134,15 @@ class account_register_payments(models.TransientModel):
|
||||
raise UserError(_("In order to pay multiple invoices at once, they must use the same currency."))
|
||||
|
||||
total_amount = sum(inv.residual * MAP_INVOICE_TYPE_PAYMENT_SIGN[inv.type] for inv in invoices)
|
||||
communication = ' '.join([ref for ref in invoices.mapped('reference') if ref])
|
||||
|
||||
rec.update({
|
||||
'amount': abs(total_amount),
|
||||
'currency_id': invoices[0].currency_id.id,
|
||||
'payment_type': total_amount > 0 and 'inbound' or 'outbound',
|
||||
'partner_id': invoices[0].commercial_partner_id.id,
|
||||
'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
|
||||
'communication': communication,
|
||||
})
|
||||
return rec
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
<field name="code"/>
|
||||
<field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}" groups="base.group_no_one"/>
|
||||
<field name="sequence_id" required="0" attrs="{'readonly': 1}" groups="base.group_no_one"/>
|
||||
<field name="refund_sequence_id" attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)], 'required': [('type', 'in', ['sale', 'purchase']), ('refund_sequence', '=', True)]}" groups="base.group_no_one"/>
|
||||
<field name="refund_sequence_id" attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)], 'required': [('type', 'in', ['sale', 'purchase']), ('refund_sequence', '=', True)]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="default_debit_account_id" domain="[('deprecated', '=', False)]" />
|
||||
|
||||
@@ -66,7 +66,7 @@ class AccountInvoiceLine(models.Model):
|
||||
'partner_id': self.invoice_id.partner_id.id,
|
||||
'company_id': self.invoice_id.company_id.id,
|
||||
'currency_id': self.invoice_id.company_currency_id.id,
|
||||
'date': self.asset_start_date or self.invoice_id.date_invoice,
|
||||
'date': self.invoice_id.date_invoice,
|
||||
'invoice_id': self.invoice_id.id,
|
||||
}
|
||||
changed_vals = self.env['account.asset.asset'].onchange_category_id_values(vals['category_id'])
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
<field name="account_asset_id" nolabel="1" attrs="{'invisible': [('type','=', False)]}"/>
|
||||
<div>
|
||||
<label for="account_depreciation_id" attrs="{'invisible': [('type','!=','purchase')]}"/>
|
||||
<label for="account_depreciation_id" string="Recognition Account" attrs="{'invisible': [('type','!=','sale')]}"/>
|
||||
<label for="account_depreciation_id" string="Recognition Income Account" attrs="{'invisible': [('type','!=','sale')]}"/>
|
||||
</div>
|
||||
<field name="account_depreciation_id" nolabel="1"/>
|
||||
<div>
|
||||
<label for="account_depreciation_expense_id" attrs="{'invisible': [('type','!=','purchase')]}"/>
|
||||
<label for="account_depreciation_expense_id" string="Recognition Income Account" attrs="{'invisible': [('type','!=','sale')]}"/>
|
||||
<label for="account_depreciation_expense_id" string="Recognition Account" attrs="{'invisible': [('type','!=','sale')]}"/>
|
||||
</div>
|
||||
<field name="account_depreciation_expense_id" nolabel="1"/>
|
||||
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
|
||||
|
||||
@@ -213,7 +213,11 @@ class AccountBankStatementImport(models.TransientModel):
|
||||
if 'unique_import_id' not in line_vals \
|
||||
or not line_vals['unique_import_id'] \
|
||||
or not bool(BankStatementLine.sudo().search([('unique_import_id', '=', line_vals['unique_import_id'])], limit=1)):
|
||||
filtered_st_lines.append(line_vals)
|
||||
if line_vals['amount'] != 0:
|
||||
# Some banks, like ING, create a line for free charges.
|
||||
# We just skip those lines as there's a 'non-zero' constraint
|
||||
# on the amount of account.bank.statement.line
|
||||
filtered_st_lines.append(line_vals)
|
||||
else:
|
||||
ignored_statement_lines_import_ids.append(line_vals['unique_import_id'])
|
||||
if len(filtered_st_lines) > 0:
|
||||
|
||||
@@ -356,7 +356,7 @@ class AccountVoucherLine(models.Model):
|
||||
self.company_id.id,
|
||||
self.voucher_id.currency_id.id,
|
||||
self.voucher_id.voucher_type)
|
||||
for fname, fvalue in onchange_res['values'].iteritems():
|
||||
for fname, fvalue in onchange_res['value'].iteritems():
|
||||
setattr(self, fname, fvalue)
|
||||
|
||||
def _get_account(self, product, fpos, type):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class Board(models.Model):
|
||||
class Board(models.AbstractModel):
|
||||
_name = 'board.board'
|
||||
_description = "Board"
|
||||
_auto = False
|
||||
|
||||
@@ -169,13 +169,13 @@ class Lead(FormatAddress, models.Model):
|
||||
def _compute_kanban_state(self):
|
||||
today = date.today()
|
||||
for lead in self:
|
||||
kanban_state = 'red'
|
||||
kanban_state = 'grey'
|
||||
if lead.date_action:
|
||||
lead_date = fields.Date.from_string(lead.date_action)
|
||||
if lead_date >= today:
|
||||
kanban_state = 'green'
|
||||
else:
|
||||
kanban_state = 'grey'
|
||||
kanban_state = 'red'
|
||||
lead.kanban_state = kanban_state
|
||||
|
||||
@api.depends('date_open')
|
||||
@@ -522,7 +522,7 @@ class Lead(FormatAddress, models.Model):
|
||||
merge_message = _('Merged leads') if result_type == 'lead' else _('Merged opportunities')
|
||||
subject = merge_message + ": " + ", ".join(opportunities.mapped('name'))
|
||||
# message bodies
|
||||
message_bodies = opportunities._mail_body(CRM_LEAD_FIELDS_TO_MERGE)
|
||||
message_bodies = opportunities._mail_body(list(CRM_LEAD_FIELDS_TO_MERGE))
|
||||
message_body = "\n\n".join(message_bodies)
|
||||
return self.message_post(body=message_body, subject=subject)
|
||||
|
||||
@@ -609,7 +609,7 @@ class Lead(FormatAddress, models.Model):
|
||||
|
||||
# merge all the sorted opportunity. This means the value of
|
||||
# the first (head opp) will be a priority.
|
||||
merged_data = opportunities._merge_data(CRM_LEAD_FIELDS_TO_MERGE)
|
||||
merged_data = opportunities._merge_data(list(CRM_LEAD_FIELDS_TO_MERGE))
|
||||
|
||||
# force value for saleperson and sales team
|
||||
if user_id:
|
||||
|
||||
@@ -35,7 +35,7 @@ tour.register('crm_tour', {
|
||||
position: "right",
|
||||
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
|
||||
}, {
|
||||
trigger: ".o_kanban_record:not(.o_updating) .oe_kanban_status_red",
|
||||
trigger: ".o_kanban_record:not(.o_updating) .oe_kanban_status_grey",
|
||||
extra_trigger: ".o_opportunity_kanban",
|
||||
content: _t("This opportunity has <b>no next activity scheduled</b>. <i>Click to set one.</i>"),
|
||||
position: "bottom"
|
||||
|
||||
@@ -284,14 +284,14 @@
|
||||
<span class='oe_kanban_mail_new' title='Unread Messages'><i class='fa fa-comments'/><t t-raw="record.message_needaction_counter.raw_value"/></span>
|
||||
</t>
|
||||
<div>
|
||||
<a name="%(crm.crm_activity_log_action)d" type="action" t-if="record.kanban_state.raw_value!='red'">
|
||||
<a name="%(crm.crm_activity_log_action)d" type="action" t-if="record.kanban_state.raw_value!='grey'">
|
||||
<span
|
||||
t-att-title="record.date_action.value + ': '+ record.title_action.value"
|
||||
t-attf-class="oe_kanban_status oe_kanban_status_#{record.kanban_state.raw_value}"/>
|
||||
</a>
|
||||
<a name="%(crm.crm_activity_schedule_action)d" type="action" t-if="record.kanban_state.raw_value=='red'">
|
||||
<a name="%(crm.crm_activity_schedule_action)d" type="action" t-if="record.kanban_state.raw_value=='grey'">
|
||||
<span
|
||||
class="oe_kanban_status oe_kanban_status_red"/>
|
||||
class="oe_kanban_status oe_kanban_status_grey"/>
|
||||
</a>
|
||||
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar pull-right"/>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,11 @@ class StockPicking(models.Model):
|
||||
@api.onchange('carrier_id')
|
||||
def onchange_carrier(self):
|
||||
if self.carrier_id.delivery_type in ['fixed', 'base_on_rule']:
|
||||
self.carrier_price = self.carrier_id.price
|
||||
order = self.sale_id
|
||||
if order:
|
||||
self.carrier_price = self.carrier_id.get_price_available(order)
|
||||
else:
|
||||
self.carrier_price = self.carrier_id.price
|
||||
|
||||
@api.depends('product_id', 'move_lines')
|
||||
def _cal_weight(self):
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
|
||||
<separator />
|
||||
<filter domain="[('state', '=', 'draft')]" string="To Submit" name="to_report" help="New Expense"/>
|
||||
<filter domain="[('state', '=', 'report')]" string="Reported" name="submitted" help="Confirmed Expenses"/>
|
||||
<filter domain="[('state', '=', 'reported')]" string="Reported" name="submitted" help="Confirmed Expenses"/>
|
||||
<filter domain="[('state', '=', 'refused')]" string="Refused" name="refused" help="Refused Expenses"/>
|
||||
<filter domain="[('state', '!=', 'cancel')]" string="Not Refused" name="uncancelled" help="Actual expense sheets, not the refused ones"/>
|
||||
<separator />
|
||||
|
||||
@@ -9,7 +9,7 @@ class AccountAnalyticLine(models.Model):
|
||||
|
||||
task_id = fields.Many2one('project.task', 'Task')
|
||||
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
|
||||
department_id = fields.Many2one('hr.department', "Department", related='user_id.employee_ids.department_id', store=True)
|
||||
department_id = fields.Many2one('hr.department', "Department", related='user_id.employee_ids.department_id', store=True, readonly=True)
|
||||
|
||||
@api.onchange('project_id')
|
||||
def onchange_project_id(self):
|
||||
|
||||
@@ -87,14 +87,11 @@ class HrTimesheetSheet(models.Model):
|
||||
if any(self.env.cr.fetchall()):
|
||||
raise ValidationError('You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.')
|
||||
|
||||
@api.multi
|
||||
@api.onchange('employee_id')
|
||||
def onchange_employee_id(self, employee_id):
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
for sheet in self:
|
||||
if employee_id:
|
||||
sheet.department_id = employee.department_id
|
||||
sheet.user_id = employee.user_id
|
||||
def onchange_employee_id(self):
|
||||
if self.employee_id:
|
||||
self.department_id = self.employee_id.department_id
|
||||
self.user_id = self.employee_id.user_id
|
||||
|
||||
def copy(self, *args, **argv):
|
||||
raise UserError(_('You cannot duplicate a timesheet.'))
|
||||
|
||||
@@ -14,6 +14,8 @@ import odoo.addons.hw_proxy.controllers.main as hw_proxy
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DRIVER_NAME = 'fiscal_data_module'
|
||||
|
||||
class Blackbox(Thread):
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
@@ -39,26 +41,31 @@ class Blackbox(Thread):
|
||||
# request. The first device to give an answer that makes sense
|
||||
# wins.
|
||||
def _find_device_path_by_probing(self):
|
||||
path = "/dev/serial/by-path/"
|
||||
probe_message = self._wrap_low_level_message_around("S000")
|
||||
with hw_proxy.rs232_lock:
|
||||
path = "/dev/serial/by-path/"
|
||||
probe_message = self._wrap_low_level_message_around("S000")
|
||||
|
||||
try:
|
||||
devices = listdir(path)
|
||||
except OSError:
|
||||
_logger.warning(path + " doesn't exist")
|
||||
else:
|
||||
for device in listdir(path):
|
||||
path_to_device = path + device
|
||||
_logger.debug("Probing " + device)
|
||||
try:
|
||||
devices = listdir(path)
|
||||
except OSError:
|
||||
_logger.warning(path + " doesn't exist")
|
||||
self.set_status("disconnected", ["No RS-232 device (or emulated ones) found"])
|
||||
else:
|
||||
for device in devices:
|
||||
if device in hw_proxy.rs232_devices:
|
||||
continue
|
||||
path_to_device = path + device
|
||||
_logger.debug("Probing " + device)
|
||||
|
||||
if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True):
|
||||
_logger.info(device + " will be used as the blackbox")
|
||||
self.set_status("connected", [device])
|
||||
return path_to_device
|
||||
if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True):
|
||||
_logger.info(device + " will be used as the blackbox")
|
||||
self.set_status("connected", [device])
|
||||
hw_proxy.rs232_devices[device] = DRIVER_NAME
|
||||
return path_to_device
|
||||
|
||||
_logger.warning("Blackbox could not be found")
|
||||
self.set_status("error", ["Couldn't find the Fiscal Data Module"])
|
||||
return ""
|
||||
_logger.warning("Blackbox could not be found")
|
||||
self.set_status("disconnected", ["Couldn't find the Fiscal Data Module"])
|
||||
return ""
|
||||
|
||||
def _lrc(self, msg):
|
||||
lrc = 0
|
||||
@@ -152,7 +159,7 @@ class Blackbox(Thread):
|
||||
|
||||
if isfile("/home/pi/registered_blackbox_be"):
|
||||
blackbox_thread = Blackbox()
|
||||
hw_proxy.drivers['fiscal_data_module'] = blackbox_thread
|
||||
hw_proxy.drivers[DRIVER_NAME] = blackbox_thread
|
||||
|
||||
class BlackboxDriver(hw_proxy.Proxy):
|
||||
@http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*')
|
||||
|
||||
@@ -100,8 +100,12 @@ class EscposDriver(Thread):
|
||||
|
||||
printers = self.connected_usb_devices()
|
||||
if len(printers) > 0:
|
||||
self.set_status('connected','Connected to '+printers[0]['name'])
|
||||
return Usb(printers[0]['vendor'], printers[0]['product'])
|
||||
print_dev = Usb(printers[0]['vendor'], printers[0]['product'])
|
||||
self.set_status(
|
||||
'connected',
|
||||
"Connected to %s (in=0x%02x,out=0x%02x)" % (printers[0]['name'], print_dev.in_ep, print_dev.out_ep)
|
||||
)
|
||||
return print_dev
|
||||
else:
|
||||
self.set_status('disconnected','Printer Not Found')
|
||||
return None
|
||||
|
||||
@@ -13,7 +13,7 @@ from time import sleep
|
||||
class Usb(Escpos):
|
||||
""" Define USB printer """
|
||||
|
||||
def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01):
|
||||
def __init__(self, idVendor, idProduct, interface=0, in_ep=None, out_ep=None):
|
||||
"""
|
||||
@param idVendor : Vendor ID
|
||||
@param idProduct : Product ID
|
||||
@@ -42,6 +42,23 @@ class Usb(Escpos):
|
||||
self.device.detach_kernel_driver(self.interface)
|
||||
self.device.set_configuration()
|
||||
usb.util.claim_interface(self.device, self.interface)
|
||||
|
||||
cfg = self.device.get_active_configuration()
|
||||
intf = cfg[(0,0)] # first interface
|
||||
if self.in_ep is None:
|
||||
# Attempt to detect IN/OUT endpoint addresses
|
||||
try:
|
||||
is_IN = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||
is_OUT = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||
endpoint_in = usb.util.find_descriptor(intf, custom_match=is_IN)
|
||||
endpoint_out = usb.util.find_descriptor(intf, custom_match=is_OUT)
|
||||
self.in_ep = endpoint_in.bEndpointAddress
|
||||
self.out_ep = endpoint_out.bEndpointAddress
|
||||
except usb.core.USBError:
|
||||
# default values for officially supported printers
|
||||
self.in_ep = 0x82
|
||||
self.out_ep = 0x01
|
||||
|
||||
except usb.core.USBError as e:
|
||||
raise HandleDeviceError(e)
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ index_template = """
|
||||
<body>
|
||||
<h1>Your PosBox is up and running</h1>
|
||||
<p>
|
||||
The PosBox is an hardware adapter that allows you to use
|
||||
The PosBox is a hardware adapter that allows you to use
|
||||
receipt printers and barcode scanners with Odoo's Point of
|
||||
Sale, <b>version 8.0 or later</b>. You can start an <a href='https://www.odoo.com/start'>online free trial</a>,
|
||||
or <a href='https://www.odoo.com/start?download'>download and install</a> it yourself.
|
||||
or <a href='https://www.odoo.com/page/download'>download and install</a> it yourself.
|
||||
</p>
|
||||
<p>
|
||||
For more information on how to setup the Point of Sale with
|
||||
@@ -53,7 +53,10 @@ index_template = """
|
||||
Wi-Fi can be configured by visiting the <a href='/wifi'>Wi-Fi configuration page</a>.
|
||||
</p>
|
||||
<p>
|
||||
The PosBox software installed on this posbox is <b>version 14</b>,
|
||||
If you need to grant remote debugging access to a developer, you can do it <a href='/remote_connect'>here</a>.
|
||||
</p>
|
||||
<p>
|
||||
The PosBox software installed on this posbox is <b>version 15</b>,
|
||||
the posbox version number is independent from Odoo. You can upgrade
|
||||
the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>.
|
||||
</p>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import commands
|
||||
import logging
|
||||
import time
|
||||
from threading import Lock
|
||||
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
@@ -24,6 +26,11 @@ BANNED_DEVICES = set([
|
||||
# so that 'status' can return the status of all active drivers
|
||||
drivers = {}
|
||||
|
||||
# keep a list of RS-232 devices that have been recognized by a driver,
|
||||
# so other drivers can skip them during probes
|
||||
rs232_devices = {} # {'/path/to/device': 'driver'}
|
||||
rs232_lock = Lock() # must be held to update `rs232_devices`
|
||||
|
||||
class Proxy(http.Controller):
|
||||
|
||||
def get_status(self):
|
||||
@@ -41,7 +48,7 @@ class Proxy(http.Controller):
|
||||
return True
|
||||
|
||||
@http.route('/hw_proxy/status', type='http', auth='none', cors='*')
|
||||
def status_http(self):
|
||||
def status_http(self, debug=None, **kwargs):
|
||||
resp = """
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
@@ -89,6 +96,8 @@ class Proxy(http.Controller):
|
||||
<h2>Connected Devices</h2>
|
||||
<p>The list of connected USB devices as seen by the posbox</p>
|
||||
"""
|
||||
if debug is None:
|
||||
resp += """(<a href="/hw_proxy/status?debug">debug version</a>)"""
|
||||
devices = commands.getoutput("lsusb").split('\n')
|
||||
count = 0
|
||||
resp += "<div class='devices'>\n"
|
||||
@@ -104,6 +113,17 @@ class Proxy(http.Controller):
|
||||
|
||||
resp += "</div>\n</body>\n</html>\n\n"
|
||||
|
||||
if debug is not None:
|
||||
resp += """
|
||||
|
||||
<h3>Debug version</h3>
|
||||
<p><tt>lsusb -v</tt> output:</p>
|
||||
<pre>
|
||||
%s
|
||||
</pre>
|
||||
|
||||
""" % subprocess.check_output('lsusb -v', shell=True)
|
||||
|
||||
return request.make_response(resp,{
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
|
||||
+250
-105
@@ -3,7 +3,10 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
from collections import namedtuple
|
||||
from os import listdir
|
||||
from threading import Thread, Lock
|
||||
|
||||
@@ -13,6 +16,8 @@ import odoo.addons.hw_proxy.controllers.main as hw_proxy
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DRIVER_NAME = 'scale'
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
@@ -20,6 +25,99 @@ except ImportError:
|
||||
serial = None
|
||||
|
||||
|
||||
def _toledo8217StatusParse(status):
|
||||
""" Parse a scale's status, returning a `(weight, weight_info)` pair. """
|
||||
weight, weight_info = None, None
|
||||
stat = ord(status[status.index('?') + 1])
|
||||
if stat == 0:
|
||||
weight_info = 'ok'
|
||||
else:
|
||||
weight_info = []
|
||||
if stat & 1 :
|
||||
weight_info.append('moving')
|
||||
if stat & 1 << 1:
|
||||
weight_info.append('over_capacity')
|
||||
if stat & 1 << 2:
|
||||
weight_info.append('negative')
|
||||
weight = 0.0
|
||||
if stat & 1 << 3:
|
||||
weight_info.append('outside_zero_capture_range')
|
||||
if stat & 1 << 4:
|
||||
weight_info.append('center_of_zero')
|
||||
if stat & 1 << 5:
|
||||
weight_info.append('net_weight')
|
||||
return weight, weight_info
|
||||
|
||||
ScaleProtocol = namedtuple(
|
||||
'ScaleProtocol',
|
||||
"name baudrate bytesize stopbits parity timeout writeTimeout weightRegexp statusRegexp "
|
||||
"statusParse commandTerminator commandDelay weightDelay newWeightDelay "
|
||||
"weightCommand zeroCommand tareCommand clearCommand emptyAnswerValid autoResetWeight")
|
||||
|
||||
# 8217 Mettler-Toledo (Weight-only) Protocol, as described in the scale's Service Manual.
|
||||
# e.g. here: https://www.manualslib.com/manual/861274/Mettler-Toledo-Viva.html?page=51#manual
|
||||
# Our recommended scale, the Mettler-Toledo "Ariva-S", supports this protocol on
|
||||
# both the USB and RS232 ports, it can be configured in the setup menu as protocol option 3.
|
||||
# We use the default serial protocol settings, the scale's settings can be configured in the
|
||||
# scale's menu anyway.
|
||||
Toledo8217Protocol = ScaleProtocol(
|
||||
name='Toledo 8217',
|
||||
baudrate=9600,
|
||||
bytesize=serial.SEVENBITS,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
parity=serial.PARITY_EVEN,
|
||||
timeout=1,
|
||||
writeTimeout=1,
|
||||
weightRegexp="\x02\\s*([0-9.]+)N?\\r",
|
||||
statusRegexp="\x02\\s*(\\?.)\\r",
|
||||
statusParse=_toledo8217StatusParse,
|
||||
commandDelay=0.2,
|
||||
weightDelay=0.5,
|
||||
newWeightDelay=0.2,
|
||||
commandTerminator='',
|
||||
weightCommand='W',
|
||||
zeroCommand='Z',
|
||||
tareCommand='T',
|
||||
clearCommand='C',
|
||||
emptyAnswerValid=False,
|
||||
autoResetWeight=False,
|
||||
)
|
||||
|
||||
# The ADAM scales have their own RS232 protocol, usually documented in the scale's manual
|
||||
# e.g at https://www.adamequipment.com/media/docs/Print%20Publications/Manuals/PDF/AZEXTRA/AZEXTRA-UM.pdf
|
||||
# https://www.manualslib.com/manual/879782/Adam-Equipment-Cbd-4.html?page=32#manual
|
||||
# Only the baudrate and label format seem to be configurable in the AZExtra series.
|
||||
ADAMEquipmentProtocol = ScaleProtocol(
|
||||
name='Adam Equipment',
|
||||
baudrate=4800,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
parity=serial.PARITY_NONE,
|
||||
timeout=0.2,
|
||||
writeTimeout=0.2,
|
||||
weightRegexp=r"\s*([0-9.]+)kg", # LABEL format 3 + KG in the scale settings, but Label 1/2 should work
|
||||
statusRegexp=None,
|
||||
statusParse=None,
|
||||
commandTerminator="\r\n",
|
||||
commandDelay=0.2,
|
||||
weightDelay=0.5,
|
||||
newWeightDelay=5, # AZExtra beeps every time you ask for a weight that was previously returned!
|
||||
# Adding an extra delay gives the operator a chance to remove the products
|
||||
# before the scale starts beeping. Could not find a way to disable the beeps.
|
||||
weightCommand='P',
|
||||
zeroCommand='Z',
|
||||
tareCommand='T',
|
||||
clearCommand=None, # No clear command -> Tare again
|
||||
emptyAnswerValid=True, # AZExtra does not answer unless a new non-zero weight has been detected
|
||||
autoResetWeight=True, # AZExtra will not return 0 after removing products
|
||||
)
|
||||
|
||||
|
||||
SCALE_PROTOCOLS = (
|
||||
Toledo8217Protocol,
|
||||
ADAMEquipmentProtocol, # must be listed last, as it supports no probing!
|
||||
)
|
||||
|
||||
class Scale(Thread):
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
@@ -30,8 +128,8 @@ class Scale(Thread):
|
||||
self.weight = 0
|
||||
self.weight_info = 'ok'
|
||||
self.device = None
|
||||
self.probed_device_paths = []
|
||||
self.path_to_scale = ''
|
||||
self.protocol = None
|
||||
|
||||
def lockedstart(self):
|
||||
with self.lock:
|
||||
@@ -39,15 +137,15 @@ class Scale(Thread):
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def set_status(self, status, message = None):
|
||||
def set_status(self, status, message=None):
|
||||
if status == self.status['status']:
|
||||
if message != None and message != self.status['messages'][-1]:
|
||||
if message is not None and message != self.status['messages'][-1]:
|
||||
self.status['messages'].append(message)
|
||||
|
||||
if status == 'error' and message:
|
||||
_logger.error('Scale Error: '+message)
|
||||
_logger.error('Scale Error: '+ message)
|
||||
elif status == 'disconnected' and message:
|
||||
_logger.warning('Disconnected Scale: '+message)
|
||||
_logger.warning('Disconnected Scale: '+ message)
|
||||
else:
|
||||
self.status['status'] = status
|
||||
if message:
|
||||
@@ -56,61 +154,105 @@ class Scale(Thread):
|
||||
self.status['messages'] = []
|
||||
|
||||
if status == 'error' and message:
|
||||
_logger.error('Scale Error: '+message)
|
||||
_logger.error('Scale Error: '+ message)
|
||||
elif status == 'disconnected' and message:
|
||||
_logger.info('Disconnected Scale: %s', message)
|
||||
|
||||
def _get_raw_response(self, connection):
|
||||
response = ""
|
||||
answer = []
|
||||
while True:
|
||||
byte = connection.read(1)
|
||||
|
||||
if byte:
|
||||
response += byte
|
||||
char = connection.read(1) # may return `bytes` or `str`
|
||||
if not char:
|
||||
break
|
||||
else:
|
||||
return response
|
||||
answer.append(char)
|
||||
return ''.join(answer)
|
||||
|
||||
def _parse_weight_answer(self, protocol, answer):
|
||||
""" Parse a scale's answer to a weighing request, returning
|
||||
a `(weight, weight_info, status)` pair.
|
||||
"""
|
||||
weight, weight_info, status = None, None, None
|
||||
try:
|
||||
_logger.debug("Parsing weight [%r]", answer)
|
||||
if not answer and protocol.emptyAnswerValid:
|
||||
# Some scales do not return the same value again, but we
|
||||
# should not clear the weight data, POS may still be reading it
|
||||
return weight, weight_info, status
|
||||
|
||||
if protocol.statusRegexp and re.search(protocol.statusRegexp, answer):
|
||||
# parse status to set weight_info - we'll try weighing again later
|
||||
weight, weight_info = protocol.statusParse(answer)
|
||||
else:
|
||||
match = re.search(protocol.weightRegexp, answer)
|
||||
if match:
|
||||
weight_text = match.group(1)
|
||||
try:
|
||||
weight = float(weight_text)
|
||||
_logger.info('Weight: %s', weight)
|
||||
except ValueError:
|
||||
_logger.exception("Cannot parse weight [%r]", weight_text)
|
||||
status = 'Invalid weight, please power-cycle the scale'
|
||||
else:
|
||||
_logger.error("Cannot parse scale answer [%r]", answer)
|
||||
status = 'Invalid scale answer, please power-cycle the scale'
|
||||
except Exception as e:
|
||||
_logger.exception("Cannot parse scale answer [%r]", answer)
|
||||
status = ("Could not weigh on scale %s with protocol %s: %s" %
|
||||
(self.path_to_scale, protocol.name, e))
|
||||
return weight, weight_info, status
|
||||
|
||||
def get_device(self):
|
||||
try:
|
||||
if not os.path.exists(self.input_dir):
|
||||
self.set_status('disconnected','Scale Not Found')
|
||||
return None
|
||||
devices = [ device for device in listdir(self.input_dir)]
|
||||
if self.device:
|
||||
return self.device
|
||||
|
||||
with hw_proxy.rs232_lock:
|
||||
try:
|
||||
if not os.path.exists(self.input_dir):
|
||||
self.set_status('disconnected', 'No RS-232 device found')
|
||||
return None
|
||||
|
||||
devices = [device for device in listdir(self.input_dir)]
|
||||
|
||||
if len(devices) > 0:
|
||||
for device in devices:
|
||||
driver = hw_proxy.rs232_devices.get(device)
|
||||
if driver and driver != DRIVER_NAME:
|
||||
# belongs to another driver
|
||||
_logger.info('Ignoring %s, belongs to %s', device, driver)
|
||||
continue
|
||||
path = self.input_dir + device
|
||||
|
||||
# don't keep probing devices that are not a scale,
|
||||
# only keep probing if in the past the device was
|
||||
# confirmed to be a scale
|
||||
if path not in self.probed_device_paths or path == self.path_to_scale:
|
||||
_logger.debug('Probing: ' + path)
|
||||
for protocol in SCALE_PROTOCOLS:
|
||||
_logger.info('Probing %s with protocol %s', path, protocol)
|
||||
connection = serial.Serial(path,
|
||||
baudrate = 9600,
|
||||
bytesize = serial.SEVENBITS,
|
||||
stopbits = serial.STOPBITS_ONE,
|
||||
parity = serial.PARITY_EVEN,
|
||||
timeout = 1,
|
||||
writeTimeout = 1)
|
||||
|
||||
connection.write("W")
|
||||
self.probed_device_paths.append(path)
|
||||
|
||||
if self._get_raw_response(connection):
|
||||
_logger.debug(path + ' is scale')
|
||||
baudrate=protocol.baudrate,
|
||||
bytesize=protocol.bytesize,
|
||||
stopbits=protocol.stopbits,
|
||||
parity=protocol.parity,
|
||||
timeout=1, # longer timeouts for probing
|
||||
writeTimeout=1) # longer timeouts for probing
|
||||
connection.write(protocol.weightCommand + protocol.commandTerminator)
|
||||
time.sleep(protocol.commandDelay)
|
||||
answer = self._get_raw_response(connection)
|
||||
weight, weight_info, status = self._parse_weight_answer(protocol, answer)
|
||||
if status:
|
||||
_logger.info('Probing %s: no valid answer to protocol %s', path, protocol.name)
|
||||
else:
|
||||
_logger.info('Probing %s: answer looks ok for protocol %s', path, protocol.name)
|
||||
self.path_to_scale = path
|
||||
self.set_status('connected','Connected to '+device)
|
||||
connection.timeout = 0.02
|
||||
connection.writeTimeout = 0.02
|
||||
self.protocol = protocol
|
||||
self.set_status(
|
||||
'connected',
|
||||
'Connected to %s with %s protocol' % (device, protocol.name)
|
||||
)
|
||||
connection.timeout = protocol.timeout
|
||||
connection.writeTimeout = protocol.writeTimeout
|
||||
hw_proxy.rs232_devices[path] = DRIVER_NAME
|
||||
return connection
|
||||
else:
|
||||
_logger.debug('Already probed: ' + path)
|
||||
|
||||
self.set_status('disconnected','Scale Not Found')
|
||||
return None
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
self.set_status('disconnected', 'No supported RS-232 scale found')
|
||||
except Exception as e:
|
||||
_logger.exception('Failed probing for scales')
|
||||
self.set_status('error', 'Failed probing for scales: %s' % e)
|
||||
return None
|
||||
|
||||
def get_weight(self):
|
||||
@@ -120,108 +262,111 @@ class Scale(Thread):
|
||||
def get_weight_info(self):
|
||||
self.lockedstart()
|
||||
return self.weight_info
|
||||
|
||||
|
||||
def get_status(self):
|
||||
self.lockedstart()
|
||||
return self.status
|
||||
|
||||
def read_weight(self):
|
||||
with self.scalelock:
|
||||
if self.device:
|
||||
try:
|
||||
self.device.write('W')
|
||||
time.sleep(0.2)
|
||||
answer = []
|
||||
|
||||
while True:
|
||||
char = self.device.read(1)
|
||||
if not char:
|
||||
break
|
||||
else:
|
||||
answer.append(char)
|
||||
|
||||
if '?' in answer:
|
||||
stat = ord(answer[answer.index('?')+1])
|
||||
if stat == 0:
|
||||
self.weight_info = 'ok'
|
||||
else:
|
||||
self.weight_info = []
|
||||
if stat & 1 :
|
||||
self.weight_info.append('moving')
|
||||
if stat & 1 << 1:
|
||||
self.weight_info.append('over_capacity')
|
||||
if stat & 1 << 2:
|
||||
self.weight_info.append('negative')
|
||||
self.weight = 0.0
|
||||
if stat & 1 << 3:
|
||||
self.weight_info.append('outside_zero_capture_range')
|
||||
if stat & 1 << 4:
|
||||
self.weight_info.append('center_of_zero')
|
||||
if stat & 1 << 5:
|
||||
self.weight_info.append('net_weight')
|
||||
else:
|
||||
answer = answer[1:-1]
|
||||
if 'N' in answer:
|
||||
answer = answer[0:-1]
|
||||
try:
|
||||
self.weight = float(''.join(answer))
|
||||
except ValueError as v:
|
||||
self.set_status('error','No data Received, please power-cycle the scale');
|
||||
self.device = None
|
||||
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
p = self.protocol
|
||||
try:
|
||||
self.device.write(p.weightCommand + p.commandTerminator)
|
||||
time.sleep(p.commandDelay)
|
||||
answer = self._get_raw_response(self.device)
|
||||
weight, weight_info, status = self._parse_weight_answer(p, answer)
|
||||
if status:
|
||||
self.set_status('error', status)
|
||||
self.device = None
|
||||
else:
|
||||
if weight is not None:
|
||||
self.weight = weight
|
||||
if weight_info is not None:
|
||||
self.weight_info = weight_info
|
||||
except Exception as e:
|
||||
self.set_status(
|
||||
'error',
|
||||
"Could not weigh on scale %s with protocol %s: %s" %
|
||||
(self.path_to_scale, p.name, e))
|
||||
self.device = None
|
||||
|
||||
def set_zero(self):
|
||||
with self.scalelock:
|
||||
if self.device:
|
||||
try:
|
||||
self.device.write('Z')
|
||||
try:
|
||||
self.device.write(self.protocol.zeroCommand + self.protocol.commandTerminator)
|
||||
time.sleep(self.protocol.commandDelay)
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
self.set_status(
|
||||
'error',
|
||||
"Could not zero scale %s with protocol %s: %s" %
|
||||
(self.path_to_scale, self.protocol.name, e))
|
||||
self.device = None
|
||||
|
||||
def set_tare(self):
|
||||
with self.scalelock:
|
||||
if self.device:
|
||||
try:
|
||||
self.device.write('T')
|
||||
try:
|
||||
self.device.write(self.protocol.tareCommand + self.protocol.commandTerminator)
|
||||
time.sleep(self.protocol.commandDelay)
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
self.set_status(
|
||||
'error',
|
||||
"Could not tare scale %s with protocol %s: %s" %
|
||||
(self.path_to_scale, self.protocol.name, e))
|
||||
self.device = None
|
||||
|
||||
def clear_tare(self):
|
||||
with self.scalelock:
|
||||
if self.device:
|
||||
try:
|
||||
self.device.write('C')
|
||||
p = self.protocol
|
||||
try:
|
||||
# if the protocol has no clear, we can just tare again
|
||||
clearCommand = p.clearCommand or p.tareCommand
|
||||
self.device.write(clearCommand + p.commandTerminator)
|
||||
time.sleep(p.commandDelay)
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
self.set_status(
|
||||
'error',
|
||||
"Could not clear tare on scale %s with protocol %s: %s" %
|
||||
(self.path_to_scale, p.name, e))
|
||||
self.device = None
|
||||
|
||||
def run(self):
|
||||
self.device = None
|
||||
self.device = None
|
||||
|
||||
while True:
|
||||
while True:
|
||||
if self.device:
|
||||
old_weight = self.weight
|
||||
self.read_weight()
|
||||
time.sleep(0.15)
|
||||
if self.weight != old_weight:
|
||||
_logger.info('New Weight: %s, sleeping %ss', self.weight, self.protocol.newWeightDelay)
|
||||
time.sleep(self.protocol.newWeightDelay)
|
||||
if self.weight and self.protocol.autoResetWeight:
|
||||
self.weight = 0
|
||||
else:
|
||||
_logger.info('Weight: %s, sleeping %ss', self.weight, self.protocol.weightDelay)
|
||||
time.sleep(self.protocol.weightDelay)
|
||||
else:
|
||||
with self.scalelock:
|
||||
self.device = self.get_device()
|
||||
if not self.device:
|
||||
time.sleep(5)
|
||||
# retry later to support "plug and play"
|
||||
time.sleep(10)
|
||||
|
||||
scale_thread = None
|
||||
if serial:
|
||||
scale_thread = Scale()
|
||||
hw_proxy.drivers['scale'] = scale_thread
|
||||
hw_proxy.drivers[DRIVER_NAME] = scale_thread
|
||||
|
||||
class ScaleDriver(hw_proxy.Proxy):
|
||||
@http.route('/hw_proxy/scale_read/', type='json', auth='none', cors='*')
|
||||
def scale_read(self):
|
||||
return {'weight': scale_thread.get_weight(), 'unit':'kg', 'info': scale_thread.get_weight_info()} if scale_thread else None
|
||||
if scale_thread:
|
||||
return {'weight': scale_thread.get_weight(),
|
||||
'unit': 'kg',
|
||||
'info': scale_thread.get_weight_info()}
|
||||
return None
|
||||
|
||||
@http.route('/hw_proxy/scale_zero/', type='json', auth='none', cors='*')
|
||||
def scale_zero(self):
|
||||
|
||||
@@ -77,10 +77,6 @@ class LivechatController(http.Controller):
|
||||
anonymous_name = request.env.user.name
|
||||
return request.env["im_livechat.channel"].get_mail_channel(channel_id, anonymous_name)
|
||||
|
||||
@http.route('/im_livechat/history', type="json", auth="public")
|
||||
def history(self, channel_id, limit):
|
||||
return request.env["mail.channel"].browse(channel_id).channel_fetch_message(limit=limit)
|
||||
|
||||
@http.route('/im_livechat/feedback', type='json', auth='public')
|
||||
def feedback(self, uuid, rate, reason=None, **kwargs):
|
||||
Channel = request.env['mail.channel']
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
<record id="mail_shortcode_rating_0" model="mail.shortcode">
|
||||
<field name="source">:rating_0</field>
|
||||
<field name="substitution"><img src='/rating/static/src/img/rating_0.png'/></field>
|
||||
<field name="description">Rating Empty Smiley</field>
|
||||
<field name="shortcode_type">image</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_shortcode_rating_1" model="mail.shortcode">
|
||||
<field name="source">:rating_1</field>
|
||||
<field name="substitution"><img src='/rating/static/src/img/rating_1.png'/></field>
|
||||
<field name="description">Rating Smiley Bad</field>
|
||||
<field name="shortcode_type">image</field>
|
||||
</record>
|
||||
|
||||
@@ -47,7 +47,7 @@ var LivechatButton = Widget.extend({
|
||||
});
|
||||
} else {
|
||||
var channel = JSON.parse(cookie);
|
||||
ready = session.rpc("/im_livechat/history", {channel_id: channel.id, limit: 100}).then(function (history) {
|
||||
ready = session.rpc("/mail/chat_history", {uuid: channel.uuid, limit: 100}).then(function (history) {
|
||||
self.history = history;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="o_livechat_rating_choices">
|
||||
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_10.png'" alt="Good" data-value="10"/>
|
||||
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_5.png'" alt="OK" data-value="5"/>
|
||||
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_0.png'" alt="Bad" data-value="0" />
|
||||
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_1.png'" alt="Bad" data-value="1" />
|
||||
<div class="o_livechat_no_feedback text-muted">
|
||||
<em> I don't want to rate this conversation</em>
|
||||
</div>
|
||||
|
||||
@@ -540,7 +540,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_h_d" model="account.account.template">
|
||||
<field name="code">150001</field>
|
||||
<field name="code">1540</field>
|
||||
<field name="name">Af te dragen BTW hoog tarief diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -552,7 +552,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_l_d" model="account.account.template">
|
||||
<field name="code">150101</field>
|
||||
<field name="code">1541</field>
|
||||
<field name="name">Af te dragen BTW laag tarief diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -564,7 +564,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_0_d" model="account.account.template">
|
||||
<field name="code">150201</field>
|
||||
<field name="code">1542</field>
|
||||
<field name="name">Af te dragen BTW overige tarieven diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -582,7 +582,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_v_d" model="account.account.template">
|
||||
<field name="code">150401</field>
|
||||
<field name="code">1544</field>
|
||||
<field name="name">Af te dragen BTW heffing verlegd diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -601,7 +601,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_h_d_eu" model="account.account.template">
|
||||
<field name="code">151601</field>
|
||||
<field name="code">1576</field>
|
||||
<field name="name">Af te dragen BTW hoog tarief binnen de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -613,7 +613,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_l_d_eu" model="account.account.template">
|
||||
<field name="code">151701</field>
|
||||
<field name="code">1577</field>
|
||||
<field name="name">Af te dragen BTW laag tarief binnen de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -625,7 +625,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_0_d_eu" model="account.account.template">
|
||||
<field name="code">151801</field>
|
||||
<field name="code">1578</field>
|
||||
<field name="name">Af te dragen BTW overige tarieven binnen de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -638,7 +638,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_h_d_non_eu" model="account.account.template">
|
||||
<field name="code">151301</field>
|
||||
<field name="code">1573</field>
|
||||
<field name="name">Af te dragen BTW hoog tarief buiten de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -650,7 +650,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_l_d_non_eu" model="account.account.template">
|
||||
<field name="code">151401</field>
|
||||
<field name="code">1574</field>
|
||||
<field name="name">Af te dragen BTW laag tarief buiten de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -662,7 +662,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_payable_0_d_non_eu" model="account.account.template">
|
||||
<field name="code">151501</field>
|
||||
<field name="code">1575</field>
|
||||
<field name="name">Af te dragen BTW overige tarieven buiten de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -692,7 +692,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_0_d" model="account.account.template">
|
||||
<field name="code">151001</field>
|
||||
<field name="code">1570</field>
|
||||
<field name="name">Voorbelasting overige diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -704,7 +704,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_h_d" model="account.account.template">
|
||||
<field name="code">152001</field>
|
||||
<field name="code">1580</field>
|
||||
<field name="name">Voorbelasting hoog diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -716,7 +716,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_l_d" model="account.account.template">
|
||||
<field name="code">152101</field>
|
||||
<field name="code">1581</field>
|
||||
<field name="name">Voorbelasting laag diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -728,7 +728,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_v_d" model="account.account.template">
|
||||
<field name="code">152201</field>
|
||||
<field name="code">1582</field>
|
||||
<field name="name">Voorbelasting verlegd diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -741,7 +741,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_0_d_eu" model="account.account.template">
|
||||
<field name="code">152301</field>
|
||||
<field name="code">1583</field>
|
||||
<field name="name">Voorbelasting overige binnen de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -753,7 +753,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_h_d_eu" model="account.account.template">
|
||||
<field name="code">152401</field>
|
||||
<field name="code">1584</field>
|
||||
<field name="name">Voorbelasting hoog binnen de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -765,7 +765,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_l_d_eu" model="account.account.template">
|
||||
<field name="code">152501</field>
|
||||
<field name="code">1585</field>
|
||||
<field name="name">Voorbelasting laag binnen de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -777,7 +777,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_v_d_eu" model="account.account.template">
|
||||
<field name="code">152601</field>
|
||||
<field name="code">1586</field>
|
||||
<field name="name">Voorbelasting verlegd binnen de EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -790,7 +790,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_0_d_non_eu" model="account.account.template">
|
||||
<field name="code">152701</field>
|
||||
<field name="code">1587</field>
|
||||
<field name="name">Voorbelasting overige buiten EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -802,7 +802,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_h_d_non_eu" model="account.account.template">
|
||||
<field name="code">152801</field>
|
||||
<field name="code">1588</field>
|
||||
<field name="name">Voorbelasting hoog buiten EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -814,7 +814,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_l_d_non_eu" model="account.account.template">
|
||||
<field name="code">152901</field>
|
||||
<field name="code">1589</field>
|
||||
<field name="name">Voorbelasting laag buiten EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
@@ -826,7 +826,7 @@
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
</record>
|
||||
<record id="vat_refund_v_d_non_eu" model="account.account.template">
|
||||
<field name="code">153001</field>
|
||||
<field name="code">1535</field>
|
||||
<field name="name">Voorbelasting verlegd buiten EU diensten</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10nnl_chart_template"/>
|
||||
|
||||
@@ -661,9 +661,17 @@ var chat_manager = {
|
||||
|
||||
post_message: function (data, options) {
|
||||
options = options || {};
|
||||
|
||||
// This message will be received from the mail composer as html content subtype
|
||||
// but the urls will not be linkified. If the mail composer takes the responsibility
|
||||
// to linkify the urls we end up with double linkification a bit everywhere.
|
||||
// Ideally we want to keep the content as text internally and only make html
|
||||
// enrichment at display time but the current design makes this quite hard to do.
|
||||
var body = utils.linkify(_.str.trim(data.content));
|
||||
|
||||
var msg = {
|
||||
partner_ids: data.partner_ids,
|
||||
body: _.str.trim(data.content),
|
||||
body: body,
|
||||
attachment_ids: data.attachment_ids,
|
||||
};
|
||||
if ('subject' in data) {
|
||||
|
||||
@@ -61,12 +61,23 @@ function _parse_and_transform(nodes, transform_function) {
|
||||
|
||||
// suggested regexp (gruber url matching regexp, adapted to js, see https://gist.github.com/gruber/8891611)
|
||||
var url_regexp = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi;
|
||||
function linkify(text, attrs) {
|
||||
attrs = attrs || {};
|
||||
if (attrs.target === undefined) {
|
||||
attrs.target = '_blank';
|
||||
}
|
||||
attrs = _.map(attrs, function (value, key) {
|
||||
return key + '="' + _.escape(value) + '"';
|
||||
}).join(' ');
|
||||
return text.replace(url_regexp, function (url) {
|
||||
var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url;
|
||||
return '<a ' + attrs + ' href="' + href + '">' + url + '</a>';
|
||||
});
|
||||
}
|
||||
|
||||
function add_link (node, transform_children) {
|
||||
if (node.nodeType === 3) { // text node
|
||||
return node.data.replace(url_regexp, function (url) {
|
||||
var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url;
|
||||
return '<a target="_blank" href="' + href + '">' + url + '</a>';
|
||||
});
|
||||
return linkify(node.data);
|
||||
}
|
||||
if (node.tagName === "A") return node.outerHTML;
|
||||
node.innerHTML = transform_children();
|
||||
@@ -132,6 +143,7 @@ return {
|
||||
send_notification: send_notification,
|
||||
parse_and_transform: parse_and_transform,
|
||||
add_link: add_link,
|
||||
linkify: linkify,
|
||||
strip_html: strip_html,
|
||||
inline: inline,
|
||||
parse_email: parse_email,
|
||||
|
||||
@@ -259,6 +259,7 @@ snippets_editor.Class.include({
|
||||
*/
|
||||
check_selected_theme();
|
||||
$body.addClass(selected_theme.className);
|
||||
switch_images(selected_theme, $snippets);
|
||||
|
||||
$dropdown.insertAfter($snippets_menu);
|
||||
|
||||
@@ -292,6 +293,7 @@ snippets_editor.Class.include({
|
||||
}
|
||||
|
||||
function switch_images(theme_params, $container) {
|
||||
if (!theme_params) return;
|
||||
$container.find("img").each(function () {
|
||||
var $img = $(this);
|
||||
var src = $img.attr("src");
|
||||
|
||||
@@ -26,15 +26,12 @@ class TestMassMailing(models.TransientModel):
|
||||
'reply_to': mailing.reply_to,
|
||||
'email_to': test_mail,
|
||||
'subject': mailing.name,
|
||||
'body_html': '',
|
||||
'body_html': mailing.body_html,
|
||||
'notification': True,
|
||||
'mailing_id': mailing.id,
|
||||
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
|
||||
}
|
||||
mail = self.env['mail.mail'].create(mail_values)
|
||||
unsubscribe_url = mail._get_unsubscribe_url(test_mail)
|
||||
body = tools.append_content_to_html(mailing.body_html, unsubscribe_url, plaintext=False, container_tag='p')
|
||||
mail.write({'body_html': body})
|
||||
mails |= mail
|
||||
mails.send()
|
||||
return True
|
||||
|
||||
@@ -301,9 +301,12 @@ class MrpWorkorder(models.Model):
|
||||
if self.next_work_order_id and self.final_lot_id and not self.next_work_order_id.final_lot_id:
|
||||
self.next_work_order_id.final_lot_id = self.final_lot_id.id
|
||||
|
||||
# TODO: add filter for those that have not been done yet --> need to check as it can have different filters
|
||||
self.move_lot_ids.filtered(lambda x: not x.done_move and not x.lot_produced_id).write({'lot_produced_id': self.final_lot_id.id,
|
||||
'lot_produced_qty': self.qty_producing,})
|
||||
self.move_lot_ids.filtered(
|
||||
lambda move_lot: not move_lot.done_move and not move_lot.lot_produced_id and move_lot.quantity_done > 0
|
||||
).write({
|
||||
'lot_produced_id': self.final_lot_id.id,
|
||||
'lot_produced_qty': self.qty_producing
|
||||
})
|
||||
|
||||
# If last work order, then post lots used
|
||||
# TODO: should be same as checking if for every workorder something has been done?
|
||||
|
||||
@@ -34,3 +34,5 @@ access_barcode_nomenclature_pos_user,barcode.nomenclature.pos.user,barcodes.mode
|
||||
access_barcode_nomenclature_pos_manager,barcode.nomenclature.pos.manager,barcodes.model_barcode_nomenclature,group_pos_manager,1,1,1,1
|
||||
access_barcode_rule_pos_user,barcode.rule.pos.user,barcodes.model_barcode_rule,group_pos_user,1,0,0,0
|
||||
access_barcode_rule_pos_manager,barcode.rule.pos.manager,barcodes.model_barcode_rule,group_pos_manager,1,1,1,1
|
||||
access_bank_statement_cashbox_user,account.bank.statement.cashbox user,account.model_account_bank_statement_cashbox,group_pos_user,1,1,1,0
|
||||
access_account_cashbox_line_user,account.cashbox.line user,account.model_account_cashbox_line,group_pos_user,1,1,1,0
|
||||
|
||||
|
@@ -13,6 +13,14 @@ file_exists() {
|
||||
[[ -f $1 ]];
|
||||
}
|
||||
|
||||
require_command () {
|
||||
type "$1" &> /dev/null || { echo "Command $1 is missing. Install it e.g. with 'apt-get install $1'. Aborting." >&2; exit 1; }
|
||||
}
|
||||
|
||||
require_command kpartx
|
||||
require_command qemu-system-arm
|
||||
require_command zerofree
|
||||
|
||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
||||
__base="$(basename ${__file} .sh)"
|
||||
@@ -28,6 +36,7 @@ fi
|
||||
cp -a *raspbian*.img posbox.img
|
||||
|
||||
CLONE_DIR="${OVERWRITE_FILES_BEFORE_INIT_DIR}/home/pi/odoo"
|
||||
rm -rf "${CLONE_DIR}"
|
||||
mkdir "${CLONE_DIR}"
|
||||
git clone -b 8.0 --no-checkout --depth 1 https://github.com/odoo/odoo.git "${CLONE_DIR}"
|
||||
cd "${CLONE_DIR}"
|
||||
@@ -44,7 +53,7 @@ cd "${__dir}"
|
||||
USR_BIN="${OVERWRITE_FILES_BEFORE_INIT_DIR}/usr/bin/"
|
||||
mkdir -p "${USR_BIN}"
|
||||
cd "/tmp"
|
||||
curl 'https://dl.ngrok.com/ngrok_2.0.19_linux_arm.zip' > ngrok.zip
|
||||
curl 'https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip' > ngrok.zip
|
||||
unzip ngrok.zip
|
||||
rm ngrok.zip
|
||||
cd "${__dir}"
|
||||
@@ -68,6 +77,7 @@ START_OF_ROOT_PARTITION=$(fdisk -l posbox.img | tail -n 1 | awk '{print $2}')
|
||||
|
||||
LOOP_MAPPER_PATH=$(kpartx -av posbox.img | tail -n 1 | cut -d ' ' -f 3)
|
||||
LOOP_MAPPER_PATH="/dev/mapper/${LOOP_MAPPER_PATH}"
|
||||
sleep 5
|
||||
|
||||
# resize filesystem
|
||||
e2fsck -f "${LOOP_MAPPER_PATH}" # resize2fs requires clean fs
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
wget 'https://downloads.raspberrypi.org/raspbian_lite_latest' -O raspbian.img.zip
|
||||
unzip raspbian.img.zip
|
||||
wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.1.13-jessie' -O kernel-qemu
|
||||
wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.4.13-jessie' -O kernel-qemu
|
||||
|
||||
@@ -151,7 +151,7 @@ class ProductTemplate(models.Model):
|
||||
except ValueError:
|
||||
main_company = self.env['res.company'].sudo().search([], limit=1, order="id")
|
||||
for template in self:
|
||||
template.currency_id = template.company_id.currency_id.id or main_company.currency_id.id
|
||||
template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id
|
||||
|
||||
@api.multi
|
||||
def _compute_template_price(self):
|
||||
|
||||
@@ -405,7 +405,7 @@ class PurchaseOrder(models.Model):
|
||||
else:
|
||||
picking = pickings[0]
|
||||
moves = order.order_line._create_stock_moves(picking)
|
||||
moves = moves.action_confirm()
|
||||
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel')).action_confirm()
|
||||
moves.force_assign()
|
||||
picking.message_post_with_view('mail.message_origin_link',
|
||||
values={'self': picking, 'origin': order},
|
||||
|
||||
@@ -128,7 +128,7 @@ class StockWarehouse(models.Model):
|
||||
res = super(StockWarehouse, self)._update_name_and_code(name, code)
|
||||
warehouse = self[0]
|
||||
#change the buy procurement rule name
|
||||
if warehouse.buy_pull_id:
|
||||
if warehouse.buy_pull_id and name:
|
||||
warehouse.buy_pull_id.write({'name': warehouse.buy_pull_id.name.replace(warehouse.name, name, 1)})
|
||||
return res
|
||||
|
||||
|
||||
@@ -38,5 +38,6 @@ class Rating(http.Controller):
|
||||
if request.session.uid:
|
||||
return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record_sudo._name, record_sudo.id))
|
||||
return request.render('rating.rating_external_page_view', {
|
||||
'web_base_url': request.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
'web_base_url': request.env['ir.config_parameter'].sudo().get_param('web.base.url'),
|
||||
'rating': rating,
|
||||
})
|
||||
|
||||
@@ -43,8 +43,8 @@ class Task(models.Model):
|
||||
force_send = self.env.context.get('force_send', True)
|
||||
task.rating_send_request(rating_template, reuse_rating=False, force_send=force_send)
|
||||
|
||||
def _rating_get_partner_id(self):
|
||||
res = super(Task, self)._rating_get_partner_id()
|
||||
def rating_get_partner_id(self):
|
||||
res = super(Task, self).rating_get_partner_id()
|
||||
if not res and self.project_id.partner_id:
|
||||
return self.project_id.partner_id
|
||||
return res
|
||||
|
||||
@@ -49,7 +49,7 @@ class SaleOrder(models.Model):
|
||||
refund is not directly linked to the SO.
|
||||
"""
|
||||
for order in self:
|
||||
invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id')
|
||||
invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
|
||||
# Search for invoices which have been 'cancelled' (filter_refund = 'modify' in
|
||||
# 'account.invoice.refund')
|
||||
# use like as origin may contains multiple references (e.g. 'SO01, SO02')
|
||||
|
||||
@@ -46,7 +46,7 @@ class SaleOrder(models.Model):
|
||||
for pick in order.picking_ids:
|
||||
dates_list.append(fields.Datetime.from_string(pick.date))
|
||||
if dates_list:
|
||||
order.write({'effective_date': fields.Datetime.to_string(min(dates_list))})
|
||||
order.effective_date = fields.Datetime.to_string(min(dates_list))
|
||||
|
||||
@api.onchange('requested_date')
|
||||
def onchange_requested_date(self):
|
||||
|
||||
@@ -750,8 +750,8 @@ class StockMove(models.Model):
|
||||
owner_id=pack_operation.owner_id.id, src_package_id=pack_operation.package_id.id,
|
||||
dest_package_id=dest_package_id)
|
||||
if redo_false_quants:
|
||||
false_quants_move = [x for x in move_rec_updateme.reserved_quant_ids if (not x.lot_id) and (x.owner_id.id == pack_operation.owner_id.id) \
|
||||
and (x.location_id.id == pack_operation.location_id.id) and (x.package_id.id != pack_operation.package_id.id)]
|
||||
false_quants_move = [x for x in move_rec_updateme.reserved_quant_ids if (not x.lot_id) and (x.owner_id.id == pack_operation.owner_id.id) and
|
||||
(x.location_id.id == pack_operation.location_id.id) and (x.package_id.id == pack_operation.package_id.id)]
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -725,7 +725,7 @@ class Picking(models.Model):
|
||||
|
||||
# check if the quant is matching the operation details
|
||||
if ops.package_id:
|
||||
flag = quant.package_id and bool(QuantPackage.search([('id', 'child_of', [ops.package_id.id])])) or False
|
||||
flag = quant.package_id == ops.package_id
|
||||
else:
|
||||
flag = not quant.package_id.id
|
||||
flag = flag and (ops.owner_id.id == quant.owner_id.id)
|
||||
@@ -924,7 +924,7 @@ class Picking(models.Model):
|
||||
vals = self._prepare_values_extra_move(pack_operation, product, remaining_qty)
|
||||
moves |= moves.create(vals)
|
||||
if moves:
|
||||
moves.action_confirm()
|
||||
moves.with_context(skip_check=True).action_confirm()
|
||||
return moves
|
||||
|
||||
@api.model
|
||||
@@ -967,6 +967,7 @@ class Picking(models.Model):
|
||||
""" Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
|
||||
"""
|
||||
# TDE note: o2o conversion, todo multi
|
||||
backorders = self.env['stock.picking']
|
||||
for picking in self:
|
||||
backorder_moves = backorder_moves or picking.move_lines
|
||||
if self._context.get('do_only_split'):
|
||||
@@ -987,7 +988,8 @@ class Picking(models.Model):
|
||||
picking.write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
|
||||
backorder_picking.action_confirm()
|
||||
backorder_picking.action_assign()
|
||||
return True
|
||||
backorders |= backorder_picking
|
||||
return backorders
|
||||
|
||||
@api.multi
|
||||
def put_in_pack(self):
|
||||
|
||||
@@ -27,15 +27,17 @@ class TestStockCommon(common.TransactionCase):
|
||||
self.picking_type_out = self.ModelDataObj.xmlid_to_res_id('stock.picking_type_out')
|
||||
self.supplier_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_suppliers')
|
||||
self.stock_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_stock')
|
||||
self.pack_location = self.ModelDataObj.xmlid_to_res_id('stock.location_pack_zone')
|
||||
self.customer_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_customers')
|
||||
self.categ_unit = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_unit')
|
||||
self.categ_kgm = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_kgm')
|
||||
|
||||
# Product Created A, B, C, D
|
||||
self.productA = self.ProductObj.create({'name': 'Product A'})
|
||||
self.productB = self.ProductObj.create({'name': 'Product B'})
|
||||
self.productC = self.ProductObj.create({'name': 'Product C'})
|
||||
self.productD = self.ProductObj.create({'name': 'Product D'})
|
||||
self.productA = self.ProductObj.create({'name': 'Product A', 'type': 'product'})
|
||||
self.productB = self.ProductObj.create({'name': 'Product B', 'type': 'product'})
|
||||
self.productC = self.ProductObj.create({'name': 'Product C', 'type': 'product'})
|
||||
self.productD = self.ProductObj.create({'name': 'Product D', 'type': 'product'})
|
||||
self.productE = self.ProductObj.create({'name': 'Product E', 'type': 'product'})
|
||||
|
||||
# Configure unit of measure.
|
||||
self.uom_kg = self.UomObj.create({
|
||||
@@ -90,9 +92,9 @@ class TestStockCommon(common.TransactionCase):
|
||||
'rounding': 1.0})
|
||||
|
||||
# Product for different unit of measure.
|
||||
self.DozA = self.ProductObj.create({'name': 'Dozon-A', 'uom_id': self.uom_dozen.id, 'uom_po_id': self.uom_dozen.id})
|
||||
self.SDozA = self.ProductObj.create({'name': 'SuperDozon-A', 'uom_id': self.uom_sdozen.id, 'uom_po_id': self.uom_sdozen.id})
|
||||
self.SDozARound = self.ProductObj.create({'name': 'SuperDozenRound-A', 'uom_id': self.uom_sdozen_round.id, 'uom_po_id': self.uom_sdozen_round.id})
|
||||
self.UnitA = self.ProductObj.create({'name': 'Unit-A'})
|
||||
self.kgB = self.ProductObj.create({'name': 'kg-B', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id})
|
||||
self.gB = self.ProductObj.create({'name': 'g-B', 'uom_id': self.uom_gm.id, 'uom_po_id': self.uom_gm.id})
|
||||
self.DozA = self.ProductObj.create({'name': 'Dozon-A', 'type': 'product', 'uom_id': self.uom_dozen.id, 'uom_po_id': self.uom_dozen.id})
|
||||
self.SDozA = self.ProductObj.create({'name': 'SuperDozon-A', 'type': 'product', 'uom_id': self.uom_sdozen.id, 'uom_po_id': self.uom_sdozen.id})
|
||||
self.SDozARound = self.ProductObj.create({'name': 'SuperDozenRound-A', 'type': 'product', 'uom_id': self.uom_sdozen_round.id, 'uom_po_id': self.uom_sdozen_round.id})
|
||||
self.UnitA = self.ProductObj.create({'name': 'Unit-A', 'type': 'product'})
|
||||
self.kgB = self.ProductObj.create({'name': 'kg-B', 'type': 'product', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id})
|
||||
self.gB = self.ProductObj.create({'name': 'g-B', 'type': 'product', 'uom_id': self.uom_gm.id, 'uom_po_id': self.uom_gm.id})
|
||||
|
||||
@@ -205,19 +205,21 @@ class TestStockFlow(TestStockCommon):
|
||||
self.assertEqual(move.state, 'confirmed', 'Wrong state of move line.')
|
||||
# Product assign to outgoing shipments
|
||||
picking_out.action_assign()
|
||||
for move in picking_out.move_lines:
|
||||
self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
|
||||
self.assertEqual(picking_out.move_lines[0].state, 'confirmed', 'Wrong state of move line.')
|
||||
self.assertEqual(picking_out.move_lines[1].state, 'assigned', 'Wrong state of move line.')
|
||||
self.assertEqual(picking_out.move_lines[2].state, 'assigned', 'Wrong state of move line.')
|
||||
self.assertEqual(picking_out.move_lines[3].state, 'confirmed', 'Wrong state of move line.')
|
||||
# Check availability for product A
|
||||
aval_a_qty = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
aval_a_qty = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(aval_a_qty, 4.0, 'Wrong move quantity availability of product A (%s found instead of 4)' % (aval_a_qty))
|
||||
# Check availability for product B
|
||||
aval_b_qty = self.MoveObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
aval_b_qty = self.MoveObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(aval_b_qty, 5.0, 'Wrong move quantity availability of product B (%s found instead of 5)' % (aval_b_qty))
|
||||
# Check availability for product C
|
||||
aval_c_qty = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
aval_c_qty = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(aval_c_qty, 3.0, 'Wrong move quantity availability of product C (%s found instead of 3)' % (aval_c_qty))
|
||||
# Check availability for product D
|
||||
aval_d_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
aval_d_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(aval_d_qty, 7.0, 'Wrong move quantity availability of product D (%s found instead of 7)' % (aval_d_qty))
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
@@ -815,23 +817,21 @@ class TestStockFlow(TestStockCommon):
|
||||
for move in picking_out.move_lines:
|
||||
self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
|
||||
# Check product A available quantity
|
||||
DozA_qty = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
DozA_qty = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(DozA_qty, 4.5, 'Wrong move quantity availability (%s found instead of 4.5)' % (DozA_qty))
|
||||
# Check product B available quantity
|
||||
SDozA_qty = self.MoveObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
SDozA_qty = self.MoveObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(SDozA_qty, 2, 'Wrong move quantity availability (%s found instead of 2)' % (SDozA_qty))
|
||||
# Check product C available quantity
|
||||
SDozARound_qty = self.MoveObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
SDozARound_qty = self.MoveObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(SDozARound_qty, 3, 'Wrong move quantity availability (%s found instead of 3)' % (SDozARound_qty))
|
||||
# Check product D available quantity
|
||||
gB_qty = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
gB_qty = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(gB_qty, 503, 'Wrong move quantity availability (%s found instead of 503)' % (gB_qty))
|
||||
# Check product D available quantity
|
||||
kgB_qty = self.MoveObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_out.id)], limit=1).availability
|
||||
kgB_qty = self.MoveObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
|
||||
self.assertEqual(kgB_qty, 0.020, 'Wrong move quantity availability (%s found instead of 0.020)' % (kgB_qty))
|
||||
|
||||
picking_out.action_confirm()
|
||||
picking_out.action_assign()
|
||||
picking_out.do_prepare_partial()
|
||||
picking_out.do_transfer()
|
||||
|
||||
@@ -1276,4 +1276,263 @@ class TestStockFlow(TestStockCommon):
|
||||
self.assertEqual(sum([x.qty for x in quants if not x.lot_id]), 0.0, 'Wrong sum of quants with no lot')
|
||||
self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot1.id]), 1.0, 'Wrong sum of quants with lot 1')
|
||||
self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot2.id]), 1.0, 'Wrong sum of quants with lot 2')
|
||||
self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot3.id]), 2.0, 'Wrong sum of quants with lot 3')
|
||||
self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot3.id]), 2.0, 'Wrong sum of quants with lot 3')
|
||||
|
||||
|
||||
def test_40_pack_in_pack(self):
|
||||
""" Put a pack in pack"""
|
||||
picking_out = self.PickingObj.create({
|
||||
'partner_id': self.partner_agrolite_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
'location_id': self.pack_location,
|
||||
'location_dest_id': self.customer_location})
|
||||
move_out = self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 3,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': picking_out.id,
|
||||
'location_id': self.pack_location,
|
||||
'location_dest_id': self.customer_location})
|
||||
picking_pack = self.PickingObj.create({
|
||||
'partner_id': self.partner_agrolite_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
'location_id': self.stock_location ,
|
||||
'location_dest_id': self.pack_location})
|
||||
move_pack = self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 3,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': picking_pack.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.pack_location,
|
||||
'move_dest_id': move_out.id})
|
||||
picking_in = self.PickingObj.create({
|
||||
'partner_id': self.partner_delta_id,
|
||||
'picking_type_id': self.picking_type_in,
|
||||
'location_id': self.supplier_location,
|
||||
'location_dest_id': self.stock_location})
|
||||
move_in = self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 3,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': picking_in.id,
|
||||
'location_id': self.supplier_location,
|
||||
'location_dest_id': self.stock_location,
|
||||
'move_dest_id': move_pack.id})
|
||||
|
||||
# Check incoming shipment move lines state.
|
||||
for move in picking_in.move_lines:
|
||||
self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
|
||||
# Confirm incoming shipment.
|
||||
picking_in.action_confirm()
|
||||
# Check incoming shipment move lines state.
|
||||
for move in picking_in.move_lines:
|
||||
self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
|
||||
|
||||
# Check incoming shipment move lines state.
|
||||
for move in picking_pack.move_lines:
|
||||
self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
|
||||
# Confirm incoming shipment.
|
||||
picking_pack.action_confirm()
|
||||
# Check incoming shipment move lines state.
|
||||
for move in picking_pack.move_lines:
|
||||
self.assertEqual(move.state, 'waiting', 'Wrong state of move line.')
|
||||
|
||||
# Check incoming shipment move lines state.
|
||||
for move in picking_out.move_lines:
|
||||
self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
|
||||
# Confirm incoming shipment.
|
||||
picking_out.action_confirm()
|
||||
# Check incoming shipment move lines state.
|
||||
for move in picking_out.move_lines:
|
||||
self.assertEqual(move.state, 'waiting', 'Wrong state of move line.')
|
||||
|
||||
# Set the quantity done on the pack operation
|
||||
picking_in.pack_operation_product_ids.qty_done = 3.0
|
||||
# Put in a pack
|
||||
picking_in.put_in_pack()
|
||||
# Get the new package
|
||||
picking_in_package = picking_in.pack_operation_ids.result_package_id
|
||||
# Validate picking
|
||||
picking_in.do_new_transfer()
|
||||
|
||||
# Check first picking state changed to done
|
||||
for move in picking_in.move_lines:
|
||||
self.assertEqual(move.state, 'done', 'Wrong state of move line.')
|
||||
# Check next picking state changed to 'assigned'
|
||||
for move in picking_pack.move_lines:
|
||||
self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
|
||||
|
||||
# set the pack in pack operation to 'done'
|
||||
for pack in picking_pack.pack_operation_pack_ids:
|
||||
pack.qty_done = 1.0
|
||||
|
||||
# Put in a pack
|
||||
picking_pack.put_in_pack()
|
||||
# Get the new package
|
||||
picking_pack_package = picking_pack.pack_operation_ids.result_package_id
|
||||
# Validate picking
|
||||
picking_pack.do_new_transfer()
|
||||
|
||||
# Check second picking state changed to done
|
||||
for move in picking_pack.move_lines:
|
||||
self.assertEqual(move.state, 'done', 'Wrong state of move line.')
|
||||
# Check next picking state changed to 'assigned'
|
||||
for move in picking_out.move_lines:
|
||||
self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
|
||||
|
||||
# set the pack in pack operation to 'done'
|
||||
for pack in picking_out.pack_operation_pack_ids:
|
||||
pack.qty_done = 1.0
|
||||
|
||||
# Validate picking
|
||||
picking_out.do_new_transfer()
|
||||
|
||||
# check all pickings are done
|
||||
for move in picking_in.move_lines:
|
||||
self.assertEqual(move.state, 'done', 'Wrong state of move line.')
|
||||
for move in picking_pack.move_lines:
|
||||
self.assertEqual(move.state, 'done', 'Wrong state of move line.')
|
||||
for move in picking_out.move_lines:
|
||||
self.assertEqual(move.state, 'done', 'Wrong state of move line.')
|
||||
|
||||
# Check picking_in_package is in picking_pack_package
|
||||
self.assertEqual(picking_in_package.parent_id.id, picking_pack_package.id, 'The package created in the picking in is not in the one created in picking pack')
|
||||
# Check that both packages are in the customer location
|
||||
self.assertEqual(picking_pack_package.location_id.id, self.customer_location, 'The package created in picking pack is not in the customer location')
|
||||
self.assertEqual(picking_in_package.location_id.id, self.customer_location, 'The package created in picking in is not in the customer location')
|
||||
# Check that we have a quant in customer location, for the productA with qty 3
|
||||
quant = self.StockQuantObj.search([('location_id', '=', self.customer_location), ('product_id', '=', self.productA.id)])
|
||||
self.assertTrue(quant.id, 'There is no quant in customer location for productA')
|
||||
self.assertEqual(quant.qty, 3.0, 'The quant in customer location for productA has not a quantity of 3.0')
|
||||
# Check that the parent package of the quant is the picking_in_package
|
||||
self.assertEqual(quant.package_id.id, picking_in_package.id, 'The quant in customer location is not in its package created in picking in')
|
||||
|
||||
def test_50_create_in_out_with_product_pack_lines(self):
|
||||
picking_in = self.PickingObj.create({
|
||||
'partner_id': self.partner_delta_id,
|
||||
'picking_type_id': self.picking_type_in,
|
||||
'location_id': self.supplier_location,
|
||||
'location_dest_id': self.stock_location})
|
||||
self.MoveObj.create({
|
||||
'name': self.productE.name,
|
||||
'product_id': self.productE.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': self.productE.uom_id.id,
|
||||
'picking_id': picking_in.id,
|
||||
'location_id': self.supplier_location,
|
||||
'location_dest_id': self.stock_location})
|
||||
|
||||
picking_in.action_confirm()
|
||||
picking_in.action_assign()
|
||||
pack_obj = self.env['stock.quant.package']
|
||||
pack1 = pack_obj.create({'name': 'PACKINOUTTEST1'})
|
||||
pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'})
|
||||
picking_in.pack_operation_ids[0].result_package_id = pack1
|
||||
picking_in.pack_operation_ids[0].product_qty = 4
|
||||
packop2 = picking_in.pack_operation_ids[0].copy()
|
||||
packop2.product_qty = 6
|
||||
packop2.result_package_id = pack2
|
||||
picking_in.do_transfer()
|
||||
self.assertEqual(sum([x.qty for x in picking_in.move_lines[0].quant_ids]), 10.0, 'Expecting 10 pieces in stock')
|
||||
#check the quants are in the package
|
||||
self.assertEqual(sum(x.qty for x in pack1.quant_ids), 4.0, 'Pack 1 should have 4 pieces')
|
||||
self.assertEqual(sum(x.qty for x in pack2.quant_ids), 6.0, 'Pack 2 should have 6 pieces')
|
||||
picking_out = self.PickingObj.create({
|
||||
'partner_id': self.partner_agrolite_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location})
|
||||
self.MoveObj.create({
|
||||
'name': self.productE.name,
|
||||
'product_id': self.productE.id,
|
||||
'product_uom_qty': 3,
|
||||
'product_uom': self.productE.uom_id.id,
|
||||
'picking_id': picking_out.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location})
|
||||
picking_out.action_confirm()
|
||||
picking_out.action_assign()
|
||||
packout1 = picking_out.pack_operation_ids[0]
|
||||
packout2 = picking_out.pack_operation_ids[0].copy()
|
||||
packout1.product_qty = 2
|
||||
packout1.package_id = pack1
|
||||
packout2.package_id = pack2
|
||||
packout2.product_qty = 1
|
||||
picking_out.do_transfer()
|
||||
#Check there are no negative quants
|
||||
neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('qty', '<', 0.0)])
|
||||
self.assertEqual(len(neg_quants), 0, 'There are negative quants!')
|
||||
self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should have 2 links in the matching between the move and the operations')
|
||||
self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end')
|
||||
|
||||
def test_60_create_in_out_with_product_pack_lines(self):
|
||||
picking_in = self.PickingObj.create({
|
||||
'partner_id': self.partner_delta_id,
|
||||
'picking_type_id': self.picking_type_in,
|
||||
'location_id': self.supplier_location,
|
||||
'location_dest_id': self.stock_location})
|
||||
self.MoveObj.create({
|
||||
'name': self.productE.name,
|
||||
'product_id': self.productE.id,
|
||||
'product_uom_qty': 200,
|
||||
'product_uom': self.productE.uom_id.id,
|
||||
'picking_id': picking_in.id,
|
||||
'location_id': self.supplier_location,
|
||||
'location_dest_id': self.stock_location})
|
||||
|
||||
picking_in.action_confirm()
|
||||
picking_in.action_assign()
|
||||
pack_obj = self.env['stock.quant.package']
|
||||
pack1 = pack_obj.create({'name': 'PACKINOUTTEST1'})
|
||||
pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'})
|
||||
picking_in.pack_operation_ids[0].result_package_id = pack1
|
||||
picking_in.pack_operation_ids[0].product_qty = 120
|
||||
packop2 = picking_in.pack_operation_ids[0].copy()
|
||||
packop2.product_qty = 80
|
||||
packop2.result_package_id = pack2
|
||||
picking_in.do_transfer()
|
||||
self.assertEqual(sum([x.qty for x in picking_in.move_lines[0].quant_ids]), 200.0, 'Expecting 200 pieces in stock')
|
||||
#check the quants are in the package
|
||||
self.assertEqual(sum(x.qty for x in pack1.quant_ids), 120, 'Pack 1 should have 120 pieces')
|
||||
self.assertEqual(sum(x.qty for x in pack2.quant_ids), 80, 'Pack 2 should have 80 pieces')
|
||||
picking_out = self.PickingObj.create({
|
||||
'partner_id': self.partner_agrolite_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location})
|
||||
self.MoveObj.create({
|
||||
'name': self.productE.name,
|
||||
'product_id': self.productE.id,
|
||||
'product_uom_qty': 200,
|
||||
'product_uom': self.productE.uom_id.id,
|
||||
'picking_id': picking_out.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location})
|
||||
picking_out.action_confirm()
|
||||
picking_out.action_assign()
|
||||
#Convert entire packs into taking out of packs
|
||||
packout0 = picking_out.pack_operation_ids[0]
|
||||
packout1 = picking_out.pack_operation_ids[1]
|
||||
packout0.write({
|
||||
'package_id': pack1.id,
|
||||
'product_id': self.productE.id,
|
||||
'product_qty': 120.0,
|
||||
'product_uom_id': self.productE.uom_id.id,
|
||||
})
|
||||
packout1.write({
|
||||
'package_id': pack2.id,
|
||||
'product_id': self.productE.id,
|
||||
'product_qty': 80.0,
|
||||
'product_uom_id': self.productE.uom_id.id,
|
||||
})
|
||||
picking_out.do_transfer()
|
||||
#Check there are no negative quants
|
||||
neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('qty', '<', 0.0)])
|
||||
self.assertEqual(len(neg_quants), 0, 'There are negative quants!')
|
||||
# We should also make sure that when matching stock moves with pack operations, it takes the correct
|
||||
self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should only have 2 links beween the move and the 2 operations')
|
||||
self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end')
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
<field name="nbr_reordering_rules" widget="statinfo"/>
|
||||
</button>
|
||||
<button name="%(product_open_orderpoint)d" type="action"
|
||||
attrs="{'invisible':['|',('type', '!=', ['product', 'consu']),('nbr_reordering_rules', '!=', 1)]}"
|
||||
attrs="{'invisible':['|',('type', 'not in', ['product', 'consu']),('nbr_reordering_rules', '!=', 1)]}"
|
||||
class="oe_stat_button" icon="fa-refresh">
|
||||
<div class="o_form_field o_stat_info mr4">
|
||||
<span class="o_stat_text">Min :</span>
|
||||
|
||||
@@ -18,16 +18,15 @@ var AbstractFieldUpgrade = {
|
||||
events: {
|
||||
'click input': 'on_click_input',
|
||||
},
|
||||
|
||||
|
||||
start: function() {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.get_enterprise_label().after($("<span>", {
|
||||
text: "Enterprise",
|
||||
'class': "label label-primary oe_inline"
|
||||
}));
|
||||
},
|
||||
|
||||
|
||||
open_dialog: function() {
|
||||
var message = $(QWeb.render('EnterpriseUpgrade'));
|
||||
|
||||
@@ -43,7 +42,7 @@ var AbstractFieldUpgrade = {
|
||||
close: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return new Dialog(this, {
|
||||
size: 'medium',
|
||||
buttons: buttons,
|
||||
@@ -53,20 +52,20 @@ var AbstractFieldUpgrade = {
|
||||
title: _t("Odoo Enterprise"),
|
||||
}).open();
|
||||
},
|
||||
|
||||
|
||||
confirm_upgrade: function() {
|
||||
new Model("res.users").call("search_count", [[["share", "=", false]]]).then(function(data) {
|
||||
framework.redirect("https://www.odoo.com/odoo-enterprise/upgrade?num_users=" + data);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
get_enterprise_label: function() {},
|
||||
on_click_input: function() {},
|
||||
};
|
||||
|
||||
var UpgradeBoolean = form_widgets.FieldBoolean.extend(AbstractFieldUpgrade, {
|
||||
template: "FieldUpgradeBoolean",
|
||||
|
||||
|
||||
get_enterprise_label: function() {
|
||||
return this.$label;
|
||||
},
|
||||
@@ -82,11 +81,12 @@ var UpgradeBoolean = form_widgets.FieldBoolean.extend(AbstractFieldUpgrade, {
|
||||
|
||||
var UpgradeRadio = form_widgets.FieldRadio.extend(AbstractFieldUpgrade, {
|
||||
get_enterprise_label: function() {
|
||||
// override the margin:0px
|
||||
this.$('label').addClass('mr4');
|
||||
return this.$('label').last();
|
||||
},
|
||||
|
||||
on_click_input: function(event) {
|
||||
if($(event.target).val() === 1) {
|
||||
if($(event.target).val() === "1") {
|
||||
this.open_dialog().on('closed', this, function() {
|
||||
this.$('input').first().prop("checked", true);
|
||||
});
|
||||
|
||||
@@ -1584,8 +1584,8 @@ var AceEditor = common.AbstractField.extend(common.ReinitializeFieldMixin, {
|
||||
if (!window.ace && !this.loadJS_def) {
|
||||
this.loadJS_def = ajax.loadJS('/web/static/lib/ace/ace.odoo-custom.js').then(function () {
|
||||
return $.when(ajax.loadJS('/web/static/lib/ace/mode-python.js'),
|
||||
ajax.loadJS('/web/static/lib/ace/mode-xml.js'),
|
||||
ajax.loadJS('/web/static/lib/ace/theme-monokai.js'));
|
||||
ajax.loadJS('/web/static/lib/ace/mode-xml.js')
|
||||
);
|
||||
});
|
||||
}
|
||||
return $.when(this._super(), this.loadJS_def);
|
||||
|
||||
@@ -98,7 +98,8 @@ return Widget.extend({
|
||||
"there is no active filter in the search bar."),
|
||||
}));
|
||||
} else {
|
||||
this['display_' + this.mode]();
|
||||
var chart = this['display_' + this.mode]();
|
||||
chart.tooltip.chartContainer(this.$el[0]);
|
||||
}
|
||||
},
|
||||
display_bar: function () {
|
||||
@@ -182,6 +183,8 @@ return Widget.extend({
|
||||
chart(svg);
|
||||
this.to_remove = chart.update;
|
||||
nv.utils.onWindowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
},
|
||||
display_pie: function () {
|
||||
var data = [],
|
||||
@@ -232,6 +235,8 @@ return Widget.extend({
|
||||
chart(svg);
|
||||
this.to_remove = chart.update;
|
||||
nv.utils.onWindowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
},
|
||||
display_line: function () {
|
||||
if (this.data.length < 2) {
|
||||
@@ -309,7 +314,9 @@ return Widget.extend({
|
||||
|
||||
chart(svg);
|
||||
this.to_remove = chart.update;
|
||||
nv.utils.onWindowResize(chart.update);
|
||||
nv.utils.onWindowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
},
|
||||
destroy: function () {
|
||||
nv.utils.offWindowResize(this.to_remove);
|
||||
|
||||
@@ -554,8 +554,11 @@ var ListView = View.extend({
|
||||
if (self.display_nocontent_helper()) {
|
||||
self.no_result();
|
||||
} else {
|
||||
// Load previous page if the current one is empty
|
||||
if (self.records.length === 0 && self.dataset.size() > 0) {
|
||||
if (self.records.length && self.current_min === 1) {
|
||||
// Reload the list view if we delete all the records of the first page
|
||||
self.reload();
|
||||
} else if (self.records.length && self.dataset.size() > 0) {
|
||||
// Load previous page if the current one is empty
|
||||
self.pager.previous();
|
||||
}
|
||||
// Reload the list view if we are not on the last page
|
||||
|
||||
@@ -136,6 +136,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ace editor RO
|
||||
.oe_form_field_text .oe_form_text_content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
// Tags
|
||||
.o_form_field_many2manytags {
|
||||
.badge {
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.nvtooltip thead {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Control panel buttons
|
||||
// ------------------------------------------------------------------
|
||||
.o_graph_measures_list {
|
||||
.o-selected-li;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -914,9 +914,9 @@ var CalendarView = View.extend({
|
||||
var index = this.dataset.get_id_index(id);
|
||||
this.dataset.index = index;
|
||||
if (this.write_right) {
|
||||
this.do_switch_view('form', null, { mode: "edit" });
|
||||
this.do_switch_view('form', { mode: "edit" });
|
||||
} else {
|
||||
this.do_switch_view('form', null, { mode: "view" });
|
||||
this.do_switch_view('form', { mode: "view" });
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -930,7 +930,7 @@ var CalendarView = View.extend({
|
||||
buttons: [
|
||||
{text: _t("Edit"), classes: 'btn-primary', close: true, click: function() {
|
||||
self.dataset.index = self.dataset.get_id_index(id);
|
||||
self.do_switch_view('form', null, { mode: "edit" });
|
||||
self.do_switch_view('form', { mode: "edit" });
|
||||
}},
|
||||
|
||||
{text: _t("Delete"), close: true, click: function() {
|
||||
|
||||
@@ -161,6 +161,11 @@ var FieldTextHtmlSimple = widget.extend({
|
||||
return !this.get('value') || this.get('value') === "<p><br/></p>" || !this.get('value').match(/\S/);
|
||||
},
|
||||
commit_value: function () {
|
||||
// leave code view before saving
|
||||
if (this.$content.prev('textarea').length === 1) {
|
||||
var layoutInfo = { 'editor': function() { return this.$content; }.bind(this) };
|
||||
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo);
|
||||
}
|
||||
if (this.options['style-inline']) {
|
||||
transcoder.class_to_style(this.$content);
|
||||
transcoder.font_to_img(this.$content);
|
||||
@@ -377,6 +382,11 @@ var FieldTextHtml = widget.extend({
|
||||
this._dirty_flag = false;
|
||||
return this.editor.save();
|
||||
} else if (this._dirty_flag && this.editor && this.editor.buildingBlock) {
|
||||
// leave code view before saving
|
||||
if (this.$content.prev('textarea').length === 1) {
|
||||
var layoutInfo = { 'editor': function() { return this.$content; }.bind(this) };
|
||||
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo);
|
||||
}
|
||||
this.editor.buildingBlock.clean_for_save();
|
||||
this.internal_set_value( this.$content.html() );
|
||||
}
|
||||
|
||||
@@ -511,6 +511,8 @@ function prettify_html(html) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Change Warning: called with dummy arguments to quit code view before saving
|
||||
$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo) {
|
||||
if (layoutInfo.toolbar) {
|
||||
return eventHandler.modules.codeview.toggle(layoutInfo);
|
||||
|
||||
@@ -938,12 +938,19 @@ $.summernote.pluginEvents.insertTable = function (event, editor, layoutInfo, sDi
|
||||
var dimension = sDim.split('x');
|
||||
var r = range.create();
|
||||
if (!r) return;
|
||||
r = r.deleteContents();
|
||||
r = r.deleteContents(true);
|
||||
|
||||
var isBodyContainer = dom.isBodyContainer;
|
||||
dom.isBodyContainer = dom.isNotBreakable;
|
||||
r.insertNode(editor.table.createTable(dimension[0], dimension[1]));
|
||||
dom.isBodyContainer = isBodyContainer;
|
||||
var table = editor.table.createTable(dimension[0], dimension[1]);
|
||||
var parent = r.sc;
|
||||
while (dom.isText(parent.parentNode) || dom.isRemovableEmptyNode(parent.parentNode)) {
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
var node = dom.splitTree(parent, {'node': r.sc, 'offset': r.so}) || r.sc;
|
||||
node.parentNode.insertBefore(table, node);
|
||||
|
||||
if ($(node).text() === '' || node.textContent === '\u00A0') {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
editor.afterCommand($editable);
|
||||
event.preventDefault();
|
||||
|
||||
@@ -812,7 +812,7 @@ function createVideoNode(url, options) {
|
||||
class: 'vine-embed'
|
||||
});
|
||||
} else if (vimMatch && vimMatch[3].length) {
|
||||
$video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', {
|
||||
$video = $('<iframe>', {
|
||||
src: '//player.vimeo.com/video/' + vimMatch[3],
|
||||
width: '640',
|
||||
height: '360'
|
||||
@@ -824,14 +824,14 @@ function createVideoNode(url, options) {
|
||||
height: '360'
|
||||
});
|
||||
} else if (youkuMatch && youkuMatch[1].length) {
|
||||
$video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', {
|
||||
$video = $('<iframe>', {
|
||||
height: '498',
|
||||
width: '510',
|
||||
src: '//player.youku.com/embed/' + youkuMatch[1]
|
||||
});
|
||||
} else {
|
||||
// this is not a known video link. Now what, Cat? Now what?
|
||||
$video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', {
|
||||
$video = $('<iframe>', {
|
||||
width: '640',
|
||||
height: '360',
|
||||
src: url
|
||||
|
||||
@@ -520,7 +520,7 @@ var KanbanView = View.extend({
|
||||
|
||||
open_record: function (event, options) {
|
||||
if (this.dataset.select_id(event.data.id)) {
|
||||
this.do_switch_view('form', null, options); //, null, { mode: "edit" });
|
||||
this.do_switch_view('form', options);
|
||||
} else {
|
||||
this.do_warn("Kanban: could not find id#" + event.data.id);
|
||||
}
|
||||
|
||||
@@ -11,13 +11,7 @@ var TourManager = require('web_tour.TourManager');
|
||||
var QWeb = core.qweb;
|
||||
|
||||
if (config.device.size_class <= config.device.SIZES.XS) {
|
||||
return {
|
||||
register: function () {},
|
||||
run: function () {
|
||||
console.warn("Tours are disabled for mobile mode.");
|
||||
},
|
||||
STEPS: {},
|
||||
};
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
|
||||
return session.is_bound.then(function () {
|
||||
|
||||
@@ -105,7 +105,7 @@ table.editorbar-panel {
|
||||
}
|
||||
|
||||
// parallax dropzones are in conflict with outside of parallax dropzones
|
||||
.parallax > .oe_structure > .oe_drop_zone {
|
||||
.parallax .oe_structure > .oe_drop_zone {
|
||||
&:first-child {
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
@@ -393,8 +393,26 @@ div.carousel {
|
||||
|
||||
.parallax {
|
||||
position: relative;
|
||||
|
||||
.oe_structure {
|
||||
> :first-child {
|
||||
margin-top: 32px;
|
||||
}
|
||||
> :last-child {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.s_parallax {
|
||||
min-height: 200px;
|
||||
|
||||
> .oe_structure {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
@@ -403,17 +421,8 @@ div.carousel {
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> .oe_structure {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
|
||||
> :first-child {
|
||||
margin-top: 32px;
|
||||
}
|
||||
> :last-child {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.s_big_message { // TO REMOVE IN MASTER > 10.0
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
@@ -108,10 +108,11 @@
|
||||
<field name="favicon" widget="image" class="oe_avatar oe_left"/>
|
||||
</div>
|
||||
<field name="module_website_form_editor" widget="radio"/>
|
||||
<field name="module_website_version" widget="radio"/>
|
||||
<label for="id"/>
|
||||
|
||||
<label for="module_website_version"/>
|
||||
<div>
|
||||
<a href="https://www.odoo.com/slides/slide/how-a-b-testing-can-help-you-optimize-your-website-341" target="_blank">More Info</a>
|
||||
<field name="module_website_version" widget="upgrade_radio"/>
|
||||
<a href="https://www.odoo.com/slides/slide/how-a-b-testing-can-help-you-optimize-your-website-341" target="_blank"> More Info</a>
|
||||
</div>
|
||||
</group>
|
||||
<group string="Performance Optimization" name="optimization" groups="base.group_no_one">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6 mt64 mb64 bg-black-25">
|
||||
<h2 style="font-size:62px">Headline</h2>
|
||||
<h4 class="text-muted">With a great subtitle</h4>
|
||||
<h4>With a great subtitle</h4>
|
||||
<p>Write one or two paragraphs describing your product, services or a specific feature. To be successful your content needs to be useful to your readers.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@
|
||||
<template id="s_share" name="Share">
|
||||
<div class="oe_share s_share">
|
||||
<h3>
|
||||
Share
|
||||
<span>Share</span>
|
||||
<a target="_Blank" class="oe_share_facebook" href="https://www.facebook.com/sharer/sharer.php?u={url}"><i class="fa fa-facebook-square"></i></a>
|
||||
<a target="_Blank" class="oe_share_twitter" href="https://twitter.com/intent/tweet?text={title}&url={url}"><i class="fa fa-twitter"></i></a>
|
||||
<a target="_Blank" class="oe_share_linkedin" href="http://www.linkedin.com/shareArticle?mini=true&url={url}&title={title}&"><i class="fa fa-linkedin"></i></a>
|
||||
@@ -378,7 +378,7 @@
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="/page/website.contactus" class="btn btn-primary btn-lg pull-right mt8">
|
||||
Contact Us Now
|
||||
<span>Contact Us Now</span>
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,13 @@ class CrmPartnerReportAssign(models.Model):
|
||||
turnover = fields.Float('Turnover', readonly=True)
|
||||
date = fields.Date('Invoice Account Date', readonly=True)
|
||||
|
||||
_depends = {
|
||||
'account.invoice.report': ['date', 'partner_id', 'price_total', 'state', 'type'],
|
||||
'crm.lead': ['partner_assigned_id'],
|
||||
'res.partner': ['activation', 'country_id', 'date_partnership', 'date_review',
|
||||
'grade_id', 'parent_id', 'team_id', 'user_id'],
|
||||
}
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
"""
|
||||
@@ -50,13 +57,3 @@ class CrmPartnerReportAssign(models.Model):
|
||||
left join account_invoice_report i
|
||||
on (i.partner_id=p.id and i.type in ('out_invoice','out_refund') and i.state in ('open','paid'))
|
||||
)""")
|
||||
|
||||
class AccountInvoiceReport(models.Model):
|
||||
_inherit = 'account.invoice.report'
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
# ensure we re-create the crm_partner_report_assign view whenever
|
||||
# the underlying account_invoice_report view is changed
|
||||
super(AccountInvoiceReport, self).init()
|
||||
self.env['crm.partner.report.assign'].init()
|
||||
|
||||
@@ -376,7 +376,7 @@ odoo.define('website_sale.website_sale', function (require) {
|
||||
//$("input[name='phone']").attr('placeholder', data.phone_code !== 0 ? '+'+ data.phone_code : '');
|
||||
|
||||
// populate states and display
|
||||
var selectStates = $("select[name='state_id']");
|
||||
var selectStates = $("select[name='state_id']:visible");
|
||||
// dont reload state at first loading (done in qweb)
|
||||
if (selectStates.data('init')===0 || selectStates.find('option').length===1) {
|
||||
if (data.states.length) {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
France, 12-11-2016
|
||||
|
||||
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,
|
||||
|
||||
Florent de Labarre florent.mirieu@gmail.com https://github.com/fmdl
|
||||
@@ -0,0 +1,11 @@
|
||||
Estonia, 2016-11-10
|
||||
|
||||
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,
|
||||
|
||||
Uku Lagle ukulagle@gmail.com https://github.com/ruuter
|
||||
@@ -442,6 +442,16 @@ class IrActionsServer(models.Model):
|
||||
_sequence = 'ir_actions_id_seq'
|
||||
_order = 'sequence,name'
|
||||
|
||||
DEFAULT_PYTHON_CODE = """# Available variables:
|
||||
# - time, datetime, dateutil, timezone: Python libraries
|
||||
# - env: Odoo Environement
|
||||
# - model: Model of the record on which the action is triggered
|
||||
# - record: Record on which the action is triggered if there is one, otherwise None
|
||||
# - records: Records on which the action is triggered if there is one, otherwise None
|
||||
# - log : log(message), function to log debug information in logging table
|
||||
# - Warning: Warning Exception to use with raise
|
||||
# To return an action, assign: action = {...}\n\n\n\n"""
|
||||
|
||||
@api.model
|
||||
def _select_objects(self):
|
||||
records = self.env['ir.model'].search([])
|
||||
@@ -492,15 +502,7 @@ class IrActionsServer(models.Model):
|
||||
help="Select the client action that has to be executed.")
|
||||
# Python code
|
||||
code = fields.Text(string='Python Code',
|
||||
default="""# Available locals:
|
||||
# - time, datetime, dateutil: Python libraries
|
||||
# - env: Odoo Environement
|
||||
# - model: Model of the record on which the action is triggered
|
||||
# - object: Record on which the action is triggered if there is one, otherwise None
|
||||
# - workflow: Workflow engine
|
||||
# - log : log(message), function to log debug information in logging table
|
||||
# - Warning: Warning Exception to use with raise
|
||||
# To return an action, assign: action = {...}""",
|
||||
default=DEFAULT_PYTHON_CODE,
|
||||
help="Write Python code that the action will execute. Some variables are "
|
||||
"available for use; help about pyhon expression is given in the help tab.")
|
||||
# Workflow signal
|
||||
@@ -573,8 +575,8 @@ class IrActionsServer(models.Model):
|
||||
# transform expression into a path that should look like 'object.many2onefield.many2onefield'
|
||||
path = expression.split('.')
|
||||
initial = path.pop(0)
|
||||
if initial not in ['obj', 'object']:
|
||||
return (False, None, 'Your expression should begin with obj or object.\nAn expression builder is available in the help tab.')
|
||||
if initial not in ['obj', 'object', 'record']:
|
||||
return (False, None, 'Your expression should begin with obj, object, record.\nAn expression builder is available in the help tab.')
|
||||
# analyze path
|
||||
while path:
|
||||
step = path.pop(0)
|
||||
@@ -901,23 +903,29 @@ class IrActionsServer(models.Model):
|
||||
|
||||
eval_context = super(IrActionsServer, self)._get_eval_context(action=action)
|
||||
model = self.env[action.model_id.model]
|
||||
obj = None
|
||||
record = None
|
||||
records = None
|
||||
if self._context.get('active_model') == action.model_id.model and self._context.get('active_id'):
|
||||
obj = model.browse(self._context['active_id'])
|
||||
record = model.browse(self._context['active_id'])
|
||||
if self._context.get('active_model') == action.model_id.model and self._context.get('active_ids'):
|
||||
records = model.browse(self._context['active_ids'])
|
||||
if self._context.get('onchange_self'):
|
||||
obj = self._context['onchange_self']
|
||||
record = self._context['onchange_self']
|
||||
eval_context.update({
|
||||
# orm
|
||||
'env': self.env,
|
||||
'model': model,
|
||||
'workflow': workflow,
|
||||
# Exceptions
|
||||
'Warning': odoo.exceptions.Warning,
|
||||
# record
|
||||
# TODO: When porting to master move badly named obj and object to
|
||||
# deprecated and define record (active_id) and records (active_ids)
|
||||
'object': obj,
|
||||
'obj': obj,
|
||||
'record': record,
|
||||
'records': records,
|
||||
|
||||
# TODO: REMOVE ME IN saas-14
|
||||
'workflow': workflow,
|
||||
# deprecated use record, records
|
||||
'object': record,
|
||||
'obj': record,
|
||||
# Deprecated use env or model instead
|
||||
'pool': self.pool,
|
||||
'cr': self._cr,
|
||||
|
||||
@@ -367,11 +367,11 @@
|
||||
<label for="link_new_record" attrs="{'invisible': ['|', ('state', '!=', 'object_write'), ('use_write', '!=', 'expression')]}"/>
|
||||
<div attrs="{'invisible': ['|', ('state', '!=', 'object_write'), ('use_write', '!=', 'expression')]}">
|
||||
<p class="oe_grey oe_edit_only" style="margin: 0px;">
|
||||
Write a python expression, beginning with object, that gives the record to update. An expression builder is available in the help tab. Examples:
|
||||
Write a python expression, beginning with record, that gives the record to update. An expression builder is available in the help tab. Examples:
|
||||
</p>
|
||||
<ul class="oe_grey oe_edit_only">
|
||||
<li>object.partner_id</li>
|
||||
<li>object.partner_id.currency_id</li>
|
||||
<li>record.partner_id</li>
|
||||
<li>record.partner_id.currency_id</li>
|
||||
</ul>
|
||||
<field name="write_expression"
|
||||
attrs="{'required': [('state', '=', 'object_write'), ('use_write', '=', 'expression')]}"/>
|
||||
@@ -410,29 +410,24 @@
|
||||
<h3>Help with Python expressions.</h3>
|
||||
<p>Various fields may use Python code or Python expressions. The following variables can be used:</p>
|
||||
<ul>
|
||||
<li>self: ORM model of the record on which the action is triggered</li>
|
||||
<li>object or obj: browse_record of the record on which the action is triggered</li>
|
||||
<li>pool: ORM model pool (i.e. self.pool)</li>
|
||||
<li>time: Python time module</li>
|
||||
<li>cr: database cursor</li>
|
||||
<li>uid: current user id</li>
|
||||
<li>context: current context</li>
|
||||
<li>workflow: Workflow engine</li>
|
||||
<li>env: Odoo Environement on which the action is triggered</li>
|
||||
<li>record: browse_record of the record on which the action is triggered</li>
|
||||
<li>time, datetime, timezone: Python module</li>
|
||||
<li>log(message, level='info'): a logging function to create ir_logging records</li>
|
||||
</ul>
|
||||
<div>
|
||||
<p>Example of condition expression using Python</p>
|
||||
<ul>
|
||||
<li>condition: True</li>
|
||||
<li>condition: object.list_price > 5000</li>
|
||||
<li>condition: record.list_price > 5000</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div attrs="{'invisible': [('state', '!=', 'code')]}">
|
||||
<p>Example of python code</p>
|
||||
<code>
|
||||
partner_name = obj.name + '_code'
|
||||
env["res.partner"].create({"name": partner_name})
|
||||
</code>
|
||||
<code style='white-space: pre-wrap'>
|
||||
partner_name = record.name + '_code'
|
||||
env["res.partner"].create({"name": partner_name})
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
@@ -36,6 +38,11 @@ class IrFilters(models.Model):
|
||||
default = dict(default or {}, name=_('%s (copy)') % self.name)
|
||||
return super(IrFilters, self).copy(default)
|
||||
|
||||
@api.multi
|
||||
def _get_eval_domain(self):
|
||||
self.ensure_one()
|
||||
return ast.literal_eval(self.domain)
|
||||
|
||||
@api.model
|
||||
def _get_action_domain(self, action_id=None):
|
||||
"""Return a domain component for matching filters that are visible in the
|
||||
|
||||
@@ -286,7 +286,7 @@ class IrHttp(models.AbstractModel):
|
||||
module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/'
|
||||
module_resource_path = os.path.normpath(module_resource_path)
|
||||
if module_resource_path.startswith(module_path):
|
||||
with open(module_resource_path, 'r') as f:
|
||||
with open(module_resource_path, 'rb') as f:
|
||||
content = base64.b64encode(f.read())
|
||||
last_update = str(os.path.getmtime(module_resource_path))
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ TRANSLATED_ATTRS_RE = re.compile(r"@(%s)\b" % "|".join(TRANSLATED_ATTRS))
|
||||
|
||||
class View(models.Model):
|
||||
_name = 'ir.ui.view'
|
||||
_order = "priority,name"
|
||||
_order = "priority,name,id"
|
||||
|
||||
# Holds the RNG schema
|
||||
_relaxng_validator = None
|
||||
|
||||
@@ -708,7 +708,7 @@ class Module(models.Model):
|
||||
to_install = self.search([('name', 'in', urls.keys()), ('state', '=', 'uninstalled')])
|
||||
post_install_action = to_install.button_immediate_install()
|
||||
|
||||
if installed:
|
||||
if installed or to_install:
|
||||
# in this case, force server restart to reload python code...
|
||||
self._cr.commit()
|
||||
odoo.service.server.restart()
|
||||
|
||||
@@ -20,11 +20,10 @@ class BaseLanguageInstall(models.TransientModel):
|
||||
|
||||
@api.multi
|
||||
def lang_install(self):
|
||||
this = self[0]
|
||||
this = this.with_context(overwrite=this.overwrite)
|
||||
self.ensure_one()
|
||||
mods = self.env['ir.module.module'].search([('state', '=', 'installed')])
|
||||
mods.update_translations(this.lang)
|
||||
this.state = 'done'
|
||||
mods.with_context(overwrite=self.overwrite).update_translations(self.lang)
|
||||
self.state = 'done'
|
||||
return {
|
||||
'name': _('Language Pack'),
|
||||
'view_type': 'form',
|
||||
@@ -35,5 +34,5 @@ class BaseLanguageInstall(models.TransientModel):
|
||||
'context': dict(self._context, active_ids=self.ids),
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'res_id': this.id,
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
@@ -490,7 +490,7 @@ state_mx_col,mx,"Colima","COL"
|
||||
state_mx_camp,mx,"Campeche","CAMP"
|
||||
state_mx_coah,mx,"Coahuila","COAH"
|
||||
state_mx_chis,mx,"Chiapas","CHIS"
|
||||
state_mx_df,mx,"Distrito Federal","DF"
|
||||
state_mx_df,mx,"Ciudad de México","DF"
|
||||
state_mx_dgo,mx,"Durango","DGO"
|
||||
state_mx_gro,mx,"Guerrero","GRO"
|
||||
state_mx_gto,mx,"Guanajuato","GTO"
|
||||
|
||||
|
@@ -58,7 +58,7 @@ base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",",
|
||||
"base.lang_es_AR","Spanish (AR) / Español (AR)","es_AR","es_AR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True"
|
||||
"base.lang_es_BO","Spanish (BO) / Español (BO)","es_BO","es_BO","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True"
|
||||
"base.lang_es_CL","Spanish (CL) / Español (CL)","es_CL","es_CL","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True"
|
||||
"base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True"
|
||||
"base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","True"
|
||||
"base.lang_es_CR","Spanish (CR) / Español (CR)","es_CR","es_CR","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","True"
|
||||
"base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True"
|
||||
"base.lang_es_EC","Spanish (EC) / Español (EC)","es_EC","es_EC","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True"
|
||||
|
||||
|
@@ -273,6 +273,13 @@ class TestAPI(common.TransactionCase):
|
||||
self.assertEqual(set(partner2.child_ids), set(children2))
|
||||
self.env.check_cache()
|
||||
|
||||
# convert from the cache format to the write format
|
||||
partner = partner1
|
||||
partner.country_id, partner.child_ids
|
||||
data = partner._convert_to_write(partner._cache)
|
||||
self.assertEqual(data['country_id'], partner.country_id.id)
|
||||
self.assertEqual(data['child_ids'], [(6, 0, partner.child_ids.ids)])
|
||||
|
||||
@mute_logger('odoo.models')
|
||||
def test_60_prefetch(self):
|
||||
""" Check the record cache prefetching """
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
'website': "http://www.yourcompany.com",
|
||||
|
||||
# Categories can be used to filter modules in modules listing
|
||||
# Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml
|
||||
# Check https://github.com/odoo/odoo/blob/master/odoo/addons/base/module/module_data.xml
|
||||
# for the full list
|
||||
'category': 'Uncategorized',
|
||||
'version': '0.1',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{%- set mod= name|snake -%}
|
||||
{%- set model = "%s.%s"|format(mod, mod) -%}
|
||||
<openerp>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- {% for item in range(5) %} -->
|
||||
<!-- <record id="object{{ item }}" model="{{ model }}"> -->
|
||||
@@ -9,4 +9,4 @@
|
||||
<!-- </record> -->
|
||||
<!-- {% endfor %} -->
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<openerp>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- <template id="listing"> -->
|
||||
<!-- <ul> -->
|
||||
@@ -19,4 +19,4 @@
|
||||
<!-- </dl> -->
|
||||
<!-- </template> -->
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{%- set mod = name|snake -%}
|
||||
{%- set model = "%s.%s"|format(mod, mod) -%}
|
||||
<openerp>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- explicit list view definition -->
|
||||
<!--
|
||||
@@ -58,4 +58,4 @@
|
||||
action="{{mod}}.action_server"/>
|
||||
-->
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
+6
-5
@@ -456,14 +456,14 @@ class Field(object):
|
||||
if not attrs.get('readonly'):
|
||||
attrs['inverse'] = self._inverse_sparse
|
||||
|
||||
self.set_all_attrs(attrs)
|
||||
|
||||
# check for renamed attributes (conversion errors)
|
||||
for key1, key2 in RENAMED_ATTRS:
|
||||
if key1 in attrs:
|
||||
_logger.warning("Field %s: parameter %r is no longer supported; use %r instead.",
|
||||
self, key1, key2)
|
||||
|
||||
self.set_all_attrs(attrs)
|
||||
|
||||
# prefetch only stored, column, non-manual and non-deprecated fields
|
||||
if not (self.store and self.column_type) or self.manual or self.deprecated:
|
||||
self.prefetch = False
|
||||
@@ -1889,7 +1889,8 @@ class Many2one(_Relational):
|
||||
return process(value._ids)
|
||||
raise ValueError("Wrong value for %s: %r" % (self, value))
|
||||
elif isinstance(value, tuple):
|
||||
return process((value[0],))
|
||||
# value is either a pair (id, name), or a tuple of ids
|
||||
return process(value[:1])
|
||||
elif isinstance(value, dict):
|
||||
return process(record.env[self.comodel_name].new(value)._ids)
|
||||
else:
|
||||
@@ -1963,8 +1964,8 @@ class _RelationalMulti(_Relational):
|
||||
if isinstance(value, BaseModel):
|
||||
if not validate or (value._name == self.comodel_name):
|
||||
return process(value._ids)
|
||||
elif isinstance(value, list):
|
||||
# value is a list of record ids or commands
|
||||
elif isinstance(value, (list, tuple)):
|
||||
# value is a list/tuple of commands, dicts or record ids
|
||||
comodel = record.env[self.comodel_name]
|
||||
# determine the value ids; by convention empty on new records
|
||||
ids = OrderedSet(record[self.name].ids if record.id else ())
|
||||
|
||||
@@ -327,6 +327,24 @@ class Registry(Mapping):
|
||||
models[0].recompute()
|
||||
cr.commit()
|
||||
|
||||
# make sure all tables are present
|
||||
missing = [name
|
||||
for name, model in env.items()
|
||||
if not model._abstract and not model._table_exist()]
|
||||
if missing:
|
||||
_logger.warning("Models have no table: %s.", ", ".join(missing))
|
||||
# recreate missing tables following model dependencies
|
||||
deps = {name: model._depends for name, model in env.items()}
|
||||
for name in topological_sort(deps):
|
||||
if name in missing:
|
||||
_logger.info("Recreate table of model %s.", name)
|
||||
env[name].init()
|
||||
cr.commit()
|
||||
# check again, and log errors if tables are still missing
|
||||
for name, model in env.items():
|
||||
if not model._abstract and not model._table_exist():
|
||||
_logger.error("Model %s has no table.", name)
|
||||
|
||||
def clear_caches(self):
|
||||
""" Clear the caches associated to methods decorated with
|
||||
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
|
||||
|
||||
Reference in New Issue
Block a user