mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-19 01:34:43 -05:00
[PATCH] Upstream patch - 01102021
This commit is contained in:
@@ -13,9 +13,8 @@ class PortalAccount(CustomerPortal):
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
values = super()._prepare_home_portal_values(counters)
|
||||
if 'invoice_count' in counters:
|
||||
invoice_count = request.env['account.move'].search_count([
|
||||
('move_type', 'in', ('out_invoice', 'in_invoice', 'out_refund', 'in_refund', 'out_receipt', 'in_receipt')),
|
||||
]) if request.env['account.move'].check_access_rights('read', raise_exception=False) else 0
|
||||
invoice_count = request.env['account.move'].search_count(self._get_invoices_domain()) \
|
||||
if request.env['account.move'].check_access_rights('read', raise_exception=False) else 0
|
||||
values['invoice_count'] = invoice_count
|
||||
return values
|
||||
|
||||
@@ -30,12 +29,15 @@ class PortalAccount(CustomerPortal):
|
||||
}
|
||||
return self._get_page_view_values(invoice, access_token, values, 'my_invoices_history', False, **kwargs)
|
||||
|
||||
def _get_invoices_domain(self):
|
||||
return [('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))]
|
||||
|
||||
@http.route(['/my/invoices', '/my/invoices/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def portal_my_invoices(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw):
|
||||
values = self._prepare_portal_layout_values()
|
||||
AccountInvoice = request.env['account.move']
|
||||
|
||||
domain = [('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))]
|
||||
domain = self._get_invoices_domain()
|
||||
|
||||
searchbar_sortings = {
|
||||
'date': {'label': _('Date'), 'order': 'invoice_date desc'},
|
||||
|
||||
@@ -1251,7 +1251,7 @@ class AccountMove(models.Model):
|
||||
param = {'journal_id': self.journal_id.id}
|
||||
|
||||
if not relaxed:
|
||||
domain = [('journal_id', '=', self.journal_id.id), ('id', '!=', self.id or self._origin.id), ('name', 'not in', ('/', False))]
|
||||
domain = [('journal_id', '=', self.journal_id.id), ('id', '!=', self.id or self._origin.id), ('name', 'not in', ('/', '', False))]
|
||||
if self.journal_id.refund_sequence:
|
||||
refund_types = ('out_refund', 'in_refund')
|
||||
domain += [('move_type', 'in' if self.move_type in refund_types else 'not in', refund_types)]
|
||||
@@ -2992,7 +2992,7 @@ class AccountMove(models.Model):
|
||||
return None
|
||||
|
||||
unstruct_ref = self.ref if self.ref else self.name
|
||||
rslt = self.partner_bank_id.build_qr_code_url(self.amount_residual, unstruct_ref, self.payment_reference, self.currency_id, self.partner_id, qr_code_method, silent_errors=False)
|
||||
rslt = self.partner_bank_id.build_qr_code_base64(self.amount_residual, unstruct_ref, self.payment_reference, self.currency_id, self.partner_id, qr_code_method, silent_errors=False)
|
||||
|
||||
# We only set qr_code_method after generating the url; otherwise, it
|
||||
# could be set even in case of a failure in the QR code generation
|
||||
|
||||
@@ -426,7 +426,7 @@ class AccountPayment(models.Model):
|
||||
and pay.currency_id:
|
||||
|
||||
if pay.partner_bank_id:
|
||||
qr_code = pay.partner_bank_id.build_qr_code_url(pay.amount, pay.ref, pay.ref, pay.currency_id, pay.partner_id)
|
||||
qr_code = pay.partner_bank_id.build_qr_code_base64(pay.amount, pay.ref, pay.ref, pay.currency_id, pay.partner_id)
|
||||
else:
|
||||
qr_code = None
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
|
||||
from flectra import api, models, fields, _
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.tools.image import image_data_uri
|
||||
|
||||
import werkzeug
|
||||
import werkzeug.exceptions
|
||||
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = 'res.partner.bank'
|
||||
|
||||
def build_qr_code_url(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
|
||||
def build_qr_code_vals(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
|
||||
""" Returns the QR-code report URL to pay to this account with the given parameters,
|
||||
or None if no QR-code could be generated.
|
||||
|
||||
@@ -33,7 +38,7 @@ class ResPartnerBank(models.Model):
|
||||
error_message = self._check_for_qr_code_errors(candidate_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
if not error_message:
|
||||
return self._get_qr_code_url(candidate_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
return candidate_method, amount, currency, debtor_partner, free_communication, structured_communication
|
||||
|
||||
elif not silent_errors:
|
||||
error_header = _("The following error prevented '%s' QR-code to be generated though it was detected as eligible: ", candidate_name)
|
||||
@@ -41,6 +46,23 @@ class ResPartnerBank(models.Model):
|
||||
|
||||
return None
|
||||
|
||||
def build_qr_code_url(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
|
||||
candidate_method, amount, currency, debtor_partner, free_communication, structured_communication = \
|
||||
self.build_qr_code_vals(amount, free_communication, structured_communication, currency, debtor_partner, qr_method, silent_errors)
|
||||
return self._get_qr_code_url(candidate_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
|
||||
def build_qr_code_base64(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
|
||||
candidate_method, amount, currency, debtor_partner, free_communication, structured_communication = \
|
||||
self.build_qr_code_vals(amount, free_communication, structured_communication, currency, debtor_partner, qr_method, silent_errors)
|
||||
return self._get_qr_code_base64(candidate_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
return None
|
||||
|
||||
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
return None
|
||||
|
||||
def _get_qr_code_url(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
""" Hook for extension, to support the different QR generation methods.
|
||||
This function uses the provided qr_method to try generation a QR-code for
|
||||
@@ -54,6 +76,32 @@ class ResPartnerBank(models.Model):
|
||||
:param free_communication: Free communication to add to the payment when generating one with the QR-code
|
||||
:param structured_communication: Structured communication to add to the payment when generating one with the QR-code
|
||||
"""
|
||||
params = self._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
if params:
|
||||
params['value'] = '\n'.join(params['value'])
|
||||
params['type'] = params.pop('barcode_type')
|
||||
return '/report/barcode/?' + werkzeug.urls.url_encode(params)
|
||||
return None
|
||||
|
||||
def _get_qr_code_base64(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
""" Hook for extension, to support the different QR generation methods.
|
||||
This function uses the provided qr_method to try generation a QR-code for
|
||||
the given data. It it succeeds, it returns QR code in base64 url; else None.
|
||||
|
||||
:param qr_method: The QR generation method to be used to make the QR-code.
|
||||
:param amount: The amount to be paid
|
||||
:param currency: The currency in which amount is expressed
|
||||
:param debtor_partner: The partner to which this QR-code is aimed (so the one who will have to pay)
|
||||
:param free_communication: Free communication to add to the payment when generating one with the QR-code
|
||||
:param structured_communication: Structured communication to add to the payment when generating one with the QR-code
|
||||
"""
|
||||
params = self._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
if params:
|
||||
try:
|
||||
barcode = self.env['ir.actions.report'].barcode(**params)
|
||||
except (ValueError, AttributeError):
|
||||
raise werkzeug.exceptions.HTTPException(description='Cannot convert into barcode.')
|
||||
return image_data_uri(base64.b64encode(barcode))
|
||||
return None
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
from flectra import models, fields, api, _
|
||||
|
||||
import werkzeug
|
||||
|
||||
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = 'res.partner.bank'
|
||||
|
||||
def _get_qr_code_url(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'sct_qr':
|
||||
|
||||
comment = (free_communication or '') if not structured_communication else ''
|
||||
|
||||
qr_code_vals = [
|
||||
@@ -27,10 +24,19 @@ class ResPartnerBank(models.Model):
|
||||
comment[:141], # Remittance Information (Unstructured) (can't be set if there is a structured one)
|
||||
'', # Beneficiary to Originator Information
|
||||
]
|
||||
return qr_code_vals
|
||||
return super()._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
return '/report/barcode/?' + werkzeug.urls.url_encode({'type': 'QR', 'value': '\n'.join(qr_code_vals), 'width': 128, 'height': 128, 'humanreadable': 1})
|
||||
|
||||
return super()._get_qr_code_url(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'sct_qr':
|
||||
return {
|
||||
'barcode_type': 'QR',
|
||||
'width': 128,
|
||||
'height': 128,
|
||||
'humanreadable': 1,
|
||||
'value': self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication),
|
||||
}
|
||||
return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _eligible_for_qr_code(self, qr_method, debtor_partner, currency):
|
||||
if qr_method == 'sct_qr':
|
||||
|
||||
@@ -67,7 +67,7 @@ class HrEmployeeBase(models.AbstractModel):
|
||||
for employee in self:
|
||||
state = 'to_define'
|
||||
if check_login:
|
||||
if employee.user_id.im_status == 'online' or employee.last_activity:
|
||||
if employee.user_id.im_status == 'online':
|
||||
state = 'present'
|
||||
elif employee.user_id.im_status == 'offline' and employee.id not in working_now_list:
|
||||
state = 'absent'
|
||||
|
||||
@@ -163,11 +163,10 @@ class AccountMove(models.Model):
|
||||
for rec in ar_invoices:
|
||||
rec.l10n_ar_afip_responsibility_type_id = rec.commercial_partner_id.l10n_ar_afip_responsibility_type_id.id
|
||||
if rec.company_id.currency_id == rec.currency_id:
|
||||
l10n_ar_currency_rate = 1.0
|
||||
else:
|
||||
l10n_ar_currency_rate = rec.currency_id._convert(
|
||||
rec.l10n_ar_currency_rate = 1.0
|
||||
elif not rec.l10n_ar_currency_rate:
|
||||
rec.l10n_ar_currency_rate = rec.currency_id._convert(
|
||||
1.0, rec.company_id.currency_id, rec.company_id, rec.invoice_date or fields.Date.today(), round=False)
|
||||
rec.l10n_ar_currency_rate = l10n_ar_currency_rate
|
||||
|
||||
# We make validations here and not with a constraint because we want validation before sending electronic
|
||||
# data on l10n_ar_edi
|
||||
|
||||
@@ -6,9 +6,7 @@ import re
|
||||
from flectra import api, fields, models, _
|
||||
from flectra.exceptions import ValidationError
|
||||
from flectra.tools.misc import mod10r
|
||||
from flectra.exceptions import UserError
|
||||
|
||||
import werkzeug.urls
|
||||
|
||||
ISR_SUBSCRIPTION_CODE = {'CHF': '01', 'EUR': '03'}
|
||||
CLEARING = "09000"
|
||||
@@ -176,14 +174,6 @@ class ResPartnerBank(models.Model):
|
||||
return self._pretty_postal_num(iban[-9:])
|
||||
return None
|
||||
|
||||
def _get_qr_code_url(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'ch_qr':
|
||||
qr_code_vals = self._l10n_ch_get_qr_vals(amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
return '/report/barcode/?type=%s&value=%s&width=%s&height=%s&quiet=1&mask=ch_cross' % ('QR', werkzeug.urls.url_quote_plus('\n'.join(qr_code_vals)), 256, 256)
|
||||
|
||||
return super()._get_qr_code_url(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _l10n_ch_get_qr_vals(self, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
comment = ""
|
||||
if free_communication:
|
||||
@@ -238,6 +228,23 @@ class ResPartnerBank(models.Model):
|
||||
'EPD', # Mandatory trailer part
|
||||
]
|
||||
|
||||
def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'ch_qr':
|
||||
return self._l10n_ch_get_qr_vals(amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
return super()._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'ch_qr':
|
||||
return {
|
||||
'barcode_type': 'QR',
|
||||
'width': 256,
|
||||
'height': 256,
|
||||
'quiet': 1,
|
||||
'mask': 'ch_cross',
|
||||
'value': self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication),
|
||||
}
|
||||
return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _get_partner_address_lines(self, partner):
|
||||
""" Returns a tuple of two elements containing the address lines to use
|
||||
for this partner. Line 1 contains the street and number, line 2 contains
|
||||
|
||||
@@ -12,7 +12,7 @@ class ReportSwissQR(models.AbstractModel):
|
||||
|
||||
qr_code_urls = {}
|
||||
for invoice in docs:
|
||||
qr_code_urls[invoice.id] = invoice.partner_bank_id.build_qr_code_url(invoice.amount_residual, invoice.ref or invoice.name, invoice.payment_reference, invoice.currency_id, invoice.partner_id, qr_method='ch_qr', silent_errors=False)
|
||||
qr_code_urls[invoice.id] = invoice.partner_bank_id.build_qr_code_base64(invoice.amount_residual, invoice.ref or invoice.name, invoice.payment_reference, invoice.currency_id, invoice.partner_id, qr_method='ch_qr', silent_errors=False)
|
||||
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
<template id="l10n_ch_swissqr_template">
|
||||
<t t-set="o" t-value="o.with_context(lang=lang)"/>
|
||||
<t t-call="web.external_layout">
|
||||
<!-- add class to body tag -->
|
||||
<script>document.body.className += " l10n_ch_qr";</script>
|
||||
<!-- add default margin for header (matching A4 European margin) -->
|
||||
<t t-set="report_header_style">padding-top:6.2mm; padding-left:8.2mm; padding-right:8.2mm;</t>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
body.l10n_ch_qr {
|
||||
padding:0;
|
||||
body {
|
||||
padding: 0!important;
|
||||
|
||||
/* Disable custom bakground */
|
||||
.o_report_layout_background {
|
||||
@@ -10,7 +10,7 @@ body.l10n_ch_qr {
|
||||
.swissqr_title {
|
||||
position: absolute;
|
||||
padding: 15px;
|
||||
padding-top: 150px;
|
||||
padding-top: 200px;
|
||||
}
|
||||
|
||||
.swissqr_content {
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
</group>
|
||||
<group>
|
||||
<group string="Availability">
|
||||
<field name="tz" groups="base.group_no_one"/>
|
||||
<field name="recurrency_monday"/>
|
||||
<field name="recurrency_tuesday"/>
|
||||
<field name="recurrency_wednesday"/>
|
||||
@@ -62,6 +61,7 @@
|
||||
<field name="send_by" widget="radio"/>
|
||||
<label for="automatic_email_time" attrs="{'invisible': [('send_by', '!=', 'mail')]}"/>
|
||||
<div class="o_row" attrs="{'invisible': [('send_by', '!=', 'mail')]}"><field name="automatic_email_time" widget="float_time"/> <field name="moment"/></div>
|
||||
<field name="tz" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
|
||||
@@ -89,7 +89,11 @@ class StockRule(models.Model):
|
||||
# Create now the procurement group that will be assigned to the new MO
|
||||
# This ensure that the outgoing move PostProduction -> Stock is linked to its MO
|
||||
# rather than the original record (MO or SO)
|
||||
procurement.values['group_id'] = self.env["procurement.group"].create({'name': name})
|
||||
group = procurement.values.get('group_id')
|
||||
if group:
|
||||
procurement.values['group_id'] = group.copy({'name': name})
|
||||
else:
|
||||
procurement.values['group_id'] = self.env["procurement.group"].create({'name': name})
|
||||
return super()._run_pull(procurements)
|
||||
|
||||
def _get_custom_move_fields(self):
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
|
||||
<div t-if="tx.acquirer_id.qr_code">
|
||||
<t t-set="qr_code" t-value="tx.acquirer_id.journal_id.bank_account_id.build_qr_code_url(tx.amount, tx.reference, None, tx.currency_id, tx.partner_id)"/>
|
||||
<t t-set="qr_code" t-value="tx.acquirer_id.journal_id.bank_account_id.build_qr_code_base64(tx.amount, tx.reference, None, tx.currency_id, tx.partner_id)"/>
|
||||
<div class="card-body" t-if="qr_code">
|
||||
<h3>Or scan me with your banking app.</h3>
|
||||
<img class="border border-dark rounded" t-att-src="qr_code"/>
|
||||
@@ -136,7 +136,7 @@
|
||||
</div>
|
||||
|
||||
<div t-if="payment_tx_id.acquirer_id.qr_code and (payment_tx_id.acquirer_id.provider == 'transfer')">
|
||||
<t t-set="qr_code" t-value="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(payment_tx_id.amount,payment_tx_id.reference, None, payment_tx_id.currency_id, payment_tx_id.partner_id)"/>
|
||||
<t t-set="qr_code" t-value="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_base64(payment_tx_id.amount,payment_tx_id.reference, None, payment_tx_id.currency_id, payment_tx_id.partner_id)"/>
|
||||
<div class="card-body" t-if="qr_code">
|
||||
<h3>Or scan me with your banking app.</h3>
|
||||
<img class="border border-dark rounded" t-att-src="qr_code"/>
|
||||
|
||||
@@ -38,8 +38,8 @@ flectra.define('web.web_client', function (require) {
|
||||
webClient.isStarted = true;
|
||||
const chrome = new (Registries.Component.get(Chrome))(null, { webClient });
|
||||
await chrome.mount(document.querySelector('.o_action_manager'));
|
||||
await chrome.start();
|
||||
configureGui({ component: chrome });
|
||||
await chrome.start();
|
||||
}
|
||||
|
||||
AbstractService.prototype.deployServices(env);
|
||||
|
||||
@@ -25,4 +25,6 @@ class PosOrder(models.Model):
|
||||
def _prepare_invoice_vals(self):
|
||||
invoice_vals = super(PosOrder, self)._prepare_invoice_vals()
|
||||
invoice_vals['team_id'] = self.crm_team_id
|
||||
addr = self.partner_id.address_get(['delivery'])
|
||||
invoice_vals['partner_shipping_id'] = addr['delivery']
|
||||
return invoice_vals
|
||||
|
||||
@@ -469,7 +469,10 @@ class ProductTemplate(models.Model):
|
||||
while True:
|
||||
domain = templates and [('product_tmpl_id', 'not in', templates.ids)] or []
|
||||
args = args if args is not None else []
|
||||
products_ids = Product._name_search(name, args+domain, operator=operator, name_get_uid=name_get_uid)
|
||||
# Product._name_search has default value limit=100
|
||||
# So, we either use that value or override it to None to fetch all products at once
|
||||
kwargs = {} if limit else {'limit': None}
|
||||
products_ids = Product._name_search(name, args+domain, operator=operator, name_get_uid=name_get_uid, **kwargs)
|
||||
products = Product.browse(products_ids)
|
||||
new_templates = products.mapped('product_tmpl_id')
|
||||
if new_templates & templates:
|
||||
|
||||
@@ -9,7 +9,8 @@ from flectra import api, fields, models
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = "stock.move.line"
|
||||
|
||||
expiration_date = fields.Datetime(string='Expiration Date', compute='_compute_expiration_date', store=True,
|
||||
expiration_date = fields.Datetime(
|
||||
string='Expiration Date', compute='_compute_expiration_date', store=True,
|
||||
help='This is the date on which the goods with this Serial Number may'
|
||||
' become dangerous and must not be consumed.')
|
||||
|
||||
@@ -18,7 +19,8 @@ class StockMoveLine(models.Model):
|
||||
for move_line in self:
|
||||
if move_line.picking_type_use_create_lots:
|
||||
if move_line.product_id.use_expiration_date:
|
||||
move_line.expiration_date = fields.Datetime.today() + datetime.timedelta(days=move_line.product_id.expiration_time)
|
||||
if not move_line.expiration_date:
|
||||
move_line.expiration_date = fields.Datetime.today() + datetime.timedelta(days=move_line.product_id.expiration_time)
|
||||
else:
|
||||
move_line.expiration_date = False
|
||||
|
||||
@@ -31,6 +33,16 @@ class StockMoveLine(models.Model):
|
||||
else:
|
||||
self.expiration_date = False
|
||||
|
||||
@api.onchange('product_id', 'product_uom_id')
|
||||
def _onchange_product_id(self):
|
||||
res = super()._onchange_product_id()
|
||||
if self.picking_type_use_create_lots:
|
||||
if self.product_id.use_expiration_date:
|
||||
self.expiration_date = fields.Datetime.today() + datetime.timedelta(days=self.product_id.expiration_time)
|
||||
else:
|
||||
self.expiration_date = False
|
||||
return res
|
||||
|
||||
def _assign_production_lot(self, lot):
|
||||
super()._assign_production_lot(lot)
|
||||
self.lot_id._update_date_values(self.expiration_date)
|
||||
self.lot_id._update_date_values(self[0].expiration_date)
|
||||
|
||||
@@ -293,12 +293,12 @@ class TestStockProductionLot(TestStockCommon):
|
||||
receipt.action_confirm()
|
||||
|
||||
# Defines a date during the receipt.
|
||||
move = receipt.move_ids_without_package[0]
|
||||
line = move.move_line_ids[0]
|
||||
self.assertEqual(move.use_expiration_date, True)
|
||||
line.lot_name = 'Apple Box #2'
|
||||
line.expiration_date = expiration_date
|
||||
line.qty_done = 4
|
||||
move_form = Form(receipt.move_ids_without_package, view="stock.view_stock_move_operations")
|
||||
with move_form.move_line_ids.new() as line:
|
||||
line.lot_name = 'Apple Box #2'
|
||||
line.expiration_date = expiration_date
|
||||
line.qty_done = 4
|
||||
move = move_form.save()
|
||||
|
||||
receipt._action_done()
|
||||
# Get back the lot created when the picking was done...
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<field name="inherit_id" ref="utm.utm_campaign_view_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='utm_statistics']" position="inside">
|
||||
<div class="mr-3" title="Revenues">
|
||||
<div class="mr-3" title="Revenues" groups="sales_team.group_sale_salesman">
|
||||
<field name="currency_id" invisible="True"/>
|
||||
<small class="font-weight-bold">
|
||||
<field name="invoiced_amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||
</small>
|
||||
</div>
|
||||
<div class="mr-3" title="Quotations">
|
||||
<div class="mr-3" title="Quotations" groups="sales_team.group_sale_salesman">
|
||||
<i class="fa fa-money text-muted"></i>
|
||||
<small class="font-weight-bold">
|
||||
<field name="quotation_count"/>
|
||||
@@ -29,11 +29,11 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
|
||||
<button name="action_redirect_to_invoiced"
|
||||
type="object" class="oe_stat_button order-1" icon="fa-usd">
|
||||
type="object" class="oe_stat_button order-1" icon="fa-usd" groups="sales_team.group_sale_salesman">
|
||||
<field name="invoiced_amount" widget="statinfo" string="Revenues"/>
|
||||
</button>
|
||||
<button name="action_redirect_to_quotations"
|
||||
type="object" class="oe_stat_button order-2" icon="fa-money">
|
||||
type="object" class="oe_stat_button order-2" icon="fa-money" groups="sales_team.group_sale_salesman">
|
||||
<field name="quotation_count" widget="statinfo" string="Quotations"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
@@ -14,12 +14,16 @@ class SaleOrder(models.Model):
|
||||
|
||||
@api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids')
|
||||
def _compute_mrp_production_count(self):
|
||||
data = self.env['procurement.group'].read_group([('sale_id', 'in', self.ids), ('mrp_production_ids', '!=', False)], ['id'], ['sale_id'])
|
||||
mrp_count = dict()
|
||||
for item in data:
|
||||
mrp_count[item['sale_id'][0]] = item['sale_id_count']
|
||||
for sale in self:
|
||||
sale.mrp_production_count = len(sale.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids)
|
||||
sale.mrp_production_count = mrp_count.get(sale.id)
|
||||
|
||||
def action_view_mrp_production(self):
|
||||
self.ensure_one()
|
||||
mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids
|
||||
mrp_production_ids = self.env['mrp.production'].search([('procurement_group_id.sale_id', '=', self.id)]).ids
|
||||
action = {
|
||||
'res_model': 'mrp.production',
|
||||
'type': 'ir.actions.act_window',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
from flectra.tools.sql import column_exists, create_column
|
||||
|
||||
|
||||
class StockLocationRoute(models.Model):
|
||||
@@ -74,6 +75,18 @@ class StockPicking(models.Model):
|
||||
|
||||
sale_id = fields.Many2one(related="group_id.sale_id", string="Sales Order", store=True, readonly=False)
|
||||
|
||||
def _auto_init(self):
|
||||
"""
|
||||
Create related field here, too slow
|
||||
when computing it afterwards through _compute_related.
|
||||
|
||||
Since group_id.sale_id is created in this module,
|
||||
no need for an UPDATE statement.
|
||||
"""
|
||||
if not column_exists(self.env.cr, 'stock_picking', 'sale_id'):
|
||||
create_column(self.env.cr, 'stock_picking', 'sale_id', 'int4')
|
||||
return super()._auto_init()
|
||||
|
||||
def _action_done(self):
|
||||
res = super()._action_done()
|
||||
sale_order_lines_vals = []
|
||||
|
||||
@@ -503,6 +503,9 @@ class StockMove(models.Model):
|
||||
move_line_vals['product_uom_id'] = move.product_id.uom_id.id
|
||||
move_line_vals['qty_done'] = 1
|
||||
move_lines_commands.append((0, 0, move_line_vals))
|
||||
else:
|
||||
move_line = move.move_line_ids.filtered(lambda line: line.lot_id.id == lot.id)
|
||||
move_line.qty_done = 1
|
||||
move.write({'move_line_ids': move_lines_commands})
|
||||
|
||||
@api.constrains('product_uom')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import Counter
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
from flectra import _, api, fields, tools, models
|
||||
from flectra.exceptions import UserError, ValidationError
|
||||
@@ -535,20 +535,28 @@ class StockMoveLine(models.Model):
|
||||
|
||||
def _create_and_assign_production_lot(self):
|
||||
""" Creates and assign new production lots for move lines."""
|
||||
lot_vals = [{
|
||||
'company_id': ml.move_id.company_id.id,
|
||||
'name': ml.lot_name,
|
||||
'product_id': ml.product_id.id,
|
||||
} for ml in self]
|
||||
lot_vals = []
|
||||
# It is possible to have multiple time the same lot to create & assign,
|
||||
# so we handle the case with 2 dictionaries.
|
||||
key_to_index = {} # key to index of the lot
|
||||
key_to_mls = defaultdict(lambda: self.env['stock.move.line']) # key to all mls
|
||||
for ml in self:
|
||||
key = (ml.company_id.id, ml.product_id.id, ml.lot_name)
|
||||
key_to_mls[key] |= ml
|
||||
if ml.tracking != 'lot' or key not in key_to_index:
|
||||
key_to_index[key] = len(lot_vals)
|
||||
lot_vals.append({
|
||||
'company_id': ml.company_id.id,
|
||||
'name': ml.lot_name,
|
||||
'product_id': ml.product_id.id
|
||||
})
|
||||
|
||||
lots = self.env['stock.production.lot'].create(lot_vals)
|
||||
for ml, lot in zip(self, lots):
|
||||
ml._assign_production_lot(lot)
|
||||
for key, mls in key_to_mls.items():
|
||||
mls._assign_production_lot(lots[key_to_index[key]].with_prefetch(lots._ids)) # With prefetch to reconstruct the ones broke by accessing by index
|
||||
|
||||
def _assign_production_lot(self, lot):
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'lot_id': lot.id
|
||||
})
|
||||
self.write({'lot_id': lot.id})
|
||||
|
||||
def _reservation_is_updatable(self, quantity, reserved_quant):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -295,6 +295,7 @@ class StockGenerate(SavepointCase):
|
||||
self.assertEqual(move_line.qty_done, 1)
|
||||
# The location dest must be now the one from the putaway.
|
||||
self.assertEqual(move_line.location_dest_id.id, shelf_location.id)
|
||||
|
||||
def test_set_multiple_lot_name_01(self):
|
||||
""" Sets five SN in one time in stock move view form, then checks move
|
||||
has five new move lines with the right `lot_name`.
|
||||
|
||||
@@ -9,7 +9,6 @@ from flectra.exceptions import UserError
|
||||
from flectra.tests import Form
|
||||
from flectra.tools import float_is_zero, float_compare
|
||||
|
||||
from flectra.tests.common import Form
|
||||
|
||||
class TestPickShip(TestStockCommon):
|
||||
def create_pick_ship(self):
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flectra.addons.stock.tests.common import TestStockCommon
|
||||
from flectra.exceptions import ValidationError
|
||||
from flectra.tests import Form
|
||||
from flectra.tools import mute_logger, float_round
|
||||
from flectra.exceptions import UserError
|
||||
from flectra import fields
|
||||
|
||||
|
||||
class TestStockFlow(TestStockCommon):
|
||||
def setUp(cls):
|
||||
super(TestStockFlow, cls).setUp()
|
||||
@@ -15,12 +16,12 @@ class TestStockFlow(TestStockCommon):
|
||||
'name': 'My Company (Chicago)-demo',
|
||||
'email': 'chicago@yourcompany.com',
|
||||
'company_id': False,
|
||||
})
|
||||
})
|
||||
cls.company = cls.env['res.company'].create({
|
||||
'currency_id': cls.env.ref('base.USD').id,
|
||||
'partner_id': cls.partner_company2.id,
|
||||
'name': 'My Company (Chicago)-demo',
|
||||
})
|
||||
})
|
||||
|
||||
@mute_logger('flectra.addons.base.models.ir_model', 'flectra.models')
|
||||
def test_00_picking_create_and_transfer_quantity(self):
|
||||
@@ -105,7 +106,7 @@ class TestStockFlow(TestStockCommon):
|
||||
'location_dest_id': self.stock_location,
|
||||
'move_id': move_c.id,
|
||||
'lot_id': lot2_productC.id,
|
||||
})
|
||||
})
|
||||
self.StockPackObj.create({
|
||||
'product_id': self.productD.id,
|
||||
'qty_done': 2,
|
||||
@@ -113,11 +114,11 @@ class TestStockFlow(TestStockCommon):
|
||||
'location_id': self.supplier_location,
|
||||
'location_dest_id': self.stock_location,
|
||||
'move_id': move_d.id
|
||||
})
|
||||
})
|
||||
|
||||
# Check incoming shipment total quantity of pack operation
|
||||
total_qty = sum(self.StockPackObj.search([('move_id', 'in', picking_in.move_lines.ids)]).mapped('qty_done'))
|
||||
self.assertEqual(total_qty, 23, 'Wrong quantity in pack operation')
|
||||
self.assertEqual(total_qty, 23, 'Wrong quantity in pack operation')
|
||||
|
||||
# Transfer Incoming Shipment.
|
||||
picking_in._action_done()
|
||||
@@ -1976,3 +1977,118 @@ class TestStockFlow(TestStockCommon):
|
||||
picking = f.save()
|
||||
|
||||
self.assertEqual(f.state, 'confirmed')
|
||||
|
||||
def test_validate_multiple_pickings_with_same_lot_names(self):
|
||||
""" Checks only one lot is created when the same lot name is used in
|
||||
different pickings and those pickings are validated together.
|
||||
"""
|
||||
# Creates two tracked products (one by lots and one by SN).
|
||||
product_lot = self.env['product.product'].create({
|
||||
'name': 'Tracked by lot',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
product_serial = self.env['product.product'].create({
|
||||
'name': 'Tracked by SN',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
# Creates two receipts using some lot names in common.
|
||||
picking_type = self.env['stock.picking.type'].browse(self.picking_type_in)
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = picking_type
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = product_lot
|
||||
move.product_uom_qty = 8
|
||||
receipt_1 = picking_form.save()
|
||||
receipt_1.action_confirm()
|
||||
|
||||
move_form = Form(receipt_1.move_lines, view="stock.view_stock_move_operations")
|
||||
with move_form.move_line_ids.edit(0) as line:
|
||||
line.lot_name = 'lot-001'
|
||||
line.qty_done = 3
|
||||
with move_form.move_line_ids.new() as line:
|
||||
line.lot_name = 'lot-002'
|
||||
line.qty_done = 3
|
||||
with move_form.move_line_ids.new() as line:
|
||||
line.lot_name = 'lot-003'
|
||||
line.qty_done = 2
|
||||
move = move_form.save()
|
||||
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = picking_type
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = product_lot
|
||||
move.product_uom_qty = 8
|
||||
receipt_2 = picking_form.save()
|
||||
receipt_2.action_confirm()
|
||||
|
||||
move_form = Form(receipt_2.move_lines, view="stock.view_stock_move_operations")
|
||||
with move_form.move_line_ids.edit(0) as line:
|
||||
line.lot_name = 'lot-003'
|
||||
line.qty_done = 2
|
||||
with move_form.move_line_ids.new() as line:
|
||||
line.lot_name = 'lot-004'
|
||||
line.qty_done = 4
|
||||
with move_form.move_line_ids.new() as line:
|
||||
line.lot_name = 'lot-001'
|
||||
line.qty_done = 1
|
||||
with move_form.move_line_ids.new() as line:
|
||||
line.lot_name = 'lot-005'
|
||||
line.qty_done = 1
|
||||
move = move_form.save()
|
||||
|
||||
# Validates the two receipts and checks the move lines' lot.
|
||||
(receipt_1 | receipt_2).button_validate()
|
||||
lots = self.env['stock.production.lot'].search([('product_id', '=', product_lot.id)])
|
||||
self.assertEqual(len(lots), 5)
|
||||
lot1, lot2, lot3, lot4, lot5 = lots
|
||||
self.assertEqual(lot1.name, 'lot-001')
|
||||
self.assertEqual(lot2.name, 'lot-002')
|
||||
self.assertEqual(lot3.name, 'lot-003')
|
||||
self.assertEqual(lot4.name, 'lot-004')
|
||||
self.assertEqual(lot5.name, 'lot-005')
|
||||
self.assertEqual(receipt_1.move_line_ids[0].lot_id.id, lot1.id)
|
||||
self.assertEqual(receipt_1.move_line_ids[1].lot_id.id, lot2.id)
|
||||
self.assertEqual(receipt_1.move_line_ids[2].lot_id.id, lot3.id)
|
||||
self.assertEqual(receipt_2.move_line_ids[0].lot_id.id, lot3.id)
|
||||
self.assertEqual(receipt_2.move_line_ids[1].lot_id.id, lot4.id)
|
||||
self.assertEqual(receipt_2.move_line_ids[2].lot_id.id, lot1.id)
|
||||
self.assertEqual(receipt_2.move_line_ids[3].lot_id.id, lot5.id)
|
||||
|
||||
# Checks also it still raise an error when it tries to create multiple time
|
||||
# the same serial numbers (same scenario but with SN instead of lots).
|
||||
picking_type = self.env['stock.picking.type'].browse(self.picking_type_in)
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = picking_type
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = product_serial
|
||||
move.product_uom_qty = 2
|
||||
receipt_1 = picking_form.save()
|
||||
receipt_1.action_confirm()
|
||||
|
||||
move_form = Form(receipt_1.move_lines, view="stock.view_stock_move_operations")
|
||||
with move_form.move_line_ids.edit(0) as line:
|
||||
line.lot_name = 'sn-001'
|
||||
with move_form.move_line_ids.new() as line:
|
||||
line.lot_name = 'sn-002'
|
||||
move = move_form.save()
|
||||
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = picking_type
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = product_serial
|
||||
move.product_uom_qty = 2
|
||||
receipt_2 = picking_form.save()
|
||||
receipt_2.action_confirm()
|
||||
|
||||
move_form = Form(receipt_2.move_lines, view="stock.view_stock_move_operations")
|
||||
with move_form.move_line_ids.edit(0) as line:
|
||||
line.lot_name = 'sn-002'
|
||||
with move_form.move_line_ids.new() as line:
|
||||
line.lot_name = 'sn-001'
|
||||
move = move_form.save()
|
||||
|
||||
# Validates the two receipts => It should raise an error as there is duplicate SN.
|
||||
with self.assertRaises(ValidationError):
|
||||
(receipt_1 | receipt_2).button_validate()
|
||||
|
||||
@@ -220,9 +220,9 @@ class StockLandedCost(models.Model):
|
||||
AdjustementLines = self.env['stock.valuation.adjustment.lines']
|
||||
AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink()
|
||||
|
||||
digits = self.env['decimal.precision'].precision_get('Product Price')
|
||||
towrite_dict = {}
|
||||
for cost in self.filtered(lambda cost: cost._get_targeted_move_ids()):
|
||||
rounding = cost.currency_id.rounding
|
||||
total_qty = 0.0
|
||||
total_cost = 0.0
|
||||
total_weight = 0.0
|
||||
@@ -239,7 +239,7 @@ class StockLandedCost(models.Model):
|
||||
|
||||
former_cost = val_line_values.get('former_cost', 0.0)
|
||||
# round this because former_cost on the valuation lines is also rounded
|
||||
total_cost += tools.float_round(former_cost, precision_digits=digits) if digits else former_cost
|
||||
total_cost += cost.currency_id.round(former_cost)
|
||||
|
||||
total_line += 1
|
||||
|
||||
@@ -265,8 +265,8 @@ class StockLandedCost(models.Model):
|
||||
else:
|
||||
value = (line.price_unit / total_line)
|
||||
|
||||
if digits:
|
||||
value = tools.float_round(value, precision_digits=digits, rounding_method='UP')
|
||||
if rounding:
|
||||
value = tools.float_round(value, precision_rounding=rounding, rounding_method='UP')
|
||||
fnc = min if line.price_unit > 0 else max
|
||||
value = fnc(value, line.price_unit - value_split)
|
||||
value_split += value
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.addons.stock_landed_costs.tests.common import TestStockLandedCostsCommon
|
||||
from flectra.tests import tagged
|
||||
from flectra.tests import tagged, Form
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
@@ -152,3 +152,53 @@ class TestStockLandedCostsRounding(TestStockLandedCostsCommon):
|
||||
# I check that the landed cost is now "Closed" and that it has an accounting entry
|
||||
self.assertEqual(stock_landed_cost_3.state, 'done')
|
||||
self.assertTrue(stock_landed_cost_3.account_move_id)
|
||||
|
||||
def test_stock_landed_costs_rounding_02(self):
|
||||
""" The landed costs should be correctly computed, even when the decimal accuracy
|
||||
of the deciaml price is increased. """
|
||||
self.env.ref("product.decimal_price").digits = 4
|
||||
|
||||
fifo_pc = self.env['product.category'].create({
|
||||
'name': 'Fifo Category',
|
||||
'parent_id': self.env.ref("product.product_category_all").id,
|
||||
'property_valuation': 'real_time',
|
||||
'property_cost_method': 'fifo',
|
||||
})
|
||||
|
||||
products = self.Product.create([{
|
||||
'name': 'Super Product %s' % price,
|
||||
'categ_id': fifo_pc.id,
|
||||
'type': 'product',
|
||||
'standard_price': price,
|
||||
} for price in [0.91, 0.93, 75.17, 20.54]])
|
||||
|
||||
landed_product = self.Product.create({
|
||||
'name': 'Landed Costs',
|
||||
'type': 'service',
|
||||
'landed_cost_ok': True,
|
||||
'split_method_landed_cost': 'by_quantity',
|
||||
'standard_price': 1000.0,
|
||||
})
|
||||
|
||||
po = self.env['purchase.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': product.id,
|
||||
'product_qty': qty,
|
||||
'price_unit': product.standard_price,
|
||||
}) for product, qty in zip(products, [6, 6, 3, 6])]
|
||||
})
|
||||
po.button_confirm()
|
||||
|
||||
res_dict = po.picking_ids.button_validate()
|
||||
validate_wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict.get('context'))).save()
|
||||
validate_wizard.process()
|
||||
|
||||
lc_form = Form(self.LandedCost)
|
||||
lc_form.picking_ids.add(po.picking_ids)
|
||||
with lc_form.cost_lines.new() as line:
|
||||
line.product_id = landed_product
|
||||
lc = lc_form.save()
|
||||
lc.compute_landed_cost()
|
||||
|
||||
self.assertEqual(sum(lc.valuation_adjustment_lines.mapped('additional_landed_cost')), 1000.0)
|
||||
|
||||
@@ -265,6 +265,7 @@ class Survey(http.Controller):
|
||||
|
||||
if not answer_sudo.is_session_answer and survey_sudo.is_time_limited and answer_sudo.start_datetime:
|
||||
data.update({
|
||||
'server_time': fields.Datetime.now(),
|
||||
'timer_start': answer_sudo.start_datetime.isoformat(),
|
||||
'time_limit_minutes': survey_sudo.time_limit
|
||||
})
|
||||
|
||||
@@ -61,7 +61,9 @@ class SurveyUserInput(models.Model):
|
||||
# sum(multi-choice question scores) + sum(simple answer_type scores)
|
||||
total_possible_score = 0
|
||||
for question in user_input.predefined_question_ids:
|
||||
if question.question_type in ['simple_choice', 'multiple_choice']:
|
||||
if question.question_type == 'simple_choice':
|
||||
total_possible_score += max([score for score in question.mapped('suggested_answer_ids.answer_score') if score > 0], default=0)
|
||||
elif question.question_type == 'multiple_choice':
|
||||
total_possible_score += sum(score for score in question.mapped('suggested_answer_ids.answer_score') if score > 0)
|
||||
elif question.is_scored_question:
|
||||
total_possible_score += question.answer_score
|
||||
|
||||
@@ -881,6 +881,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
var questionTimeLimitReached = $timerData.data('questionTimeLimitReached');
|
||||
var timeLimitMinutes = $timerData.data('timeLimitMinutes');
|
||||
var hasAnswered = $timerData.data('hasAnswered');
|
||||
const serverTime = $timerData.data('serverTime');
|
||||
|
||||
if (!questionTimeLimitReached && !hasAnswered && timeLimitMinutes) {
|
||||
var timer = $timerData.data('timer');
|
||||
@@ -889,6 +890,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
});
|
||||
this.$('.o_survey_timer_container').append($timer);
|
||||
this.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(this, {
|
||||
'serverTime': serverTime,
|
||||
'timer': timer,
|
||||
'timeLimitMinutes': timeLimitMinutes
|
||||
});
|
||||
|
||||
@@ -16,11 +16,18 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({
|
||||
this.timer = params.timer;
|
||||
this.timeLimitMinutes = params.timeLimitMinutes;
|
||||
this.surveyTimerInterval = null;
|
||||
this.timeDifference = null;
|
||||
if (params.serverTime) {
|
||||
this.timeDifference = moment.utc().diff(moment.utc(params.serverTime), 'milliseconds');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Two responsabilities : Validate that time limit is not exceeded and Run timer otherwise.
|
||||
* If end-user's clock OR the system clock is de-synchronized before the survey is started, we apply the
|
||||
* difference in timer (if time difference is more than 5 seconds) so that we can
|
||||
* display the 'absolute' counter
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
@@ -28,6 +35,9 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.countDownDate = moment.utc(self.timer).add(self.timeLimitMinutes, 'minutes');
|
||||
if (Math.abs(self.timeDifference) >= 5000) {
|
||||
self.countDownDate = self.countDownDate.add(self.timeDifference, 'milliseconds');
|
||||
}
|
||||
if (self.timeLimitMinutes <= 0 || self.countDownDate.diff(moment.utc(), 'seconds') < 0) {
|
||||
self.trigger_up('time_up');
|
||||
} else {
|
||||
|
||||
@@ -104,6 +104,46 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
{}
|
||||
)
|
||||
|
||||
def test_partial_scores_simple_choice(self):
|
||||
"""" Check that if partial scores are given for partially correct answers, in the case of a multiple
|
||||
choice question with single choice, choosing the answer with max score gives 100% of points. """
|
||||
|
||||
partial_scores_survey = self.env['survey.survey'].create({
|
||||
'title': 'How much do you know about words?',
|
||||
'scoring_type': 'scoring_with_answers',
|
||||
'scoring_success_min': 90.0,
|
||||
})
|
||||
[a_01, a_02, a_03] = self.env['survey.question.answer'].create([{
|
||||
'value': 'A thing full of letters.',
|
||||
'answer_score': 1.0
|
||||
}, {
|
||||
'value': 'A unit of language, [...], carrying a meaning.',
|
||||
'answer_score': 4.0,
|
||||
'is_correct': True
|
||||
}, {
|
||||
'value': '42',
|
||||
'answer_score': -4.0
|
||||
}])
|
||||
q_01 = self.env['survey.question'].create({
|
||||
'survey_id': partial_scores_survey.id,
|
||||
'title': 'What is a word?',
|
||||
'sequence': 1,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': [(6, 0, (a_01 | a_02 | a_03).ids)]
|
||||
})
|
||||
|
||||
user_input = self.env['survey.user_input'].create({'survey_id': partial_scores_survey.id})
|
||||
self.env['survey.user_input.line'].create({
|
||||
'user_input_id': user_input.id,
|
||||
'question_id': q_01.id,
|
||||
'answer_type': 'suggestion',
|
||||
'suggested_answer_id': a_02.id
|
||||
})
|
||||
|
||||
# Check that scoring is correct and survey is passed
|
||||
self.assertEqual(user_input.scoring_percentage, 100)
|
||||
self.assertTrue(user_input.scoring_success)
|
||||
|
||||
@users('survey_manager')
|
||||
def test_skipped_values(self):
|
||||
""" Create one question per type of questions.
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
t-att-data-question-time-limit-reached="answer.question_time_limit_reached"
|
||||
t-att-data-has-answered="bool(has_answered)"
|
||||
t-att-data-is-page-description="bool(question and question.is_page and not is_html_empty(question.description))"
|
||||
t-att-data-server-time="server_time"
|
||||
t-att-data-timer="timer_start"
|
||||
t-att-data-time-limit-minutes="time_limit_minutes"/>
|
||||
<t t-if="survey.questions_layout == 'one_page'">
|
||||
|
||||
@@ -144,7 +144,7 @@ var FormViewDialog = ViewDialog.extend({
|
||||
.then(function () {
|
||||
// reset default name field from context when Save & New is clicked, pass additional
|
||||
// context so that when getContext is called additional context resets it
|
||||
var additionalContext = self._createContext && self._createContext(false) || {};
|
||||
const additionalContext = self._createContext && self._createContext(false);
|
||||
self.form_view.createRecord(self.parentID, additionalContext);
|
||||
})
|
||||
.then(function () {
|
||||
|
||||
@@ -433,6 +433,85 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Form dialog replaces the context with _createContext method when specified", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
"partner,false,form":
|
||||
`<form string="Partner">
|
||||
<sheet>
|
||||
<group><field name="foo"/></group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
},
|
||||
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
assert.step(JSON.stringify(args.kwargs.context));
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.FormViewDialog(parent, {
|
||||
res_model: "partner",
|
||||
context: { answer: 42 },
|
||||
_createContext: () => ({ dolphin: 64 }),
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.notOk($(".modal-body button").length,
|
||||
"should not have any button in body");
|
||||
assert.strictEqual($(".modal-footer button").length, 3,
|
||||
"should have 3 buttons in footer");
|
||||
|
||||
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
|
||||
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
|
||||
assert.verifySteps(['{"answer":42}', '{"dolphin":64}']);
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Form dialog keeps full context when no _createContext is specified", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
"partner,false,form":
|
||||
`<form string="Partner">
|
||||
<sheet>
|
||||
<group><field name="foo"/></group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
},
|
||||
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
assert.step(JSON.stringify(args.kwargs.context));
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.FormViewDialog(parent, {
|
||||
res_model: "partner",
|
||||
context: { answer: 42 }
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.notOk($(".modal-body button").length,
|
||||
"should not have any button in body");
|
||||
assert.strictEqual($(".modal-footer button").length, 3,
|
||||
"should have 3 buttons in footer");
|
||||
|
||||
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
|
||||
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
|
||||
assert.verifySteps(['{"answer":42}', '{"answer":42}']);
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog: save current search', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
|
||||
@@ -945,9 +945,19 @@
|
||||
context.scale(scaleX, scaleY);
|
||||
context.imageSmoothingEnabled = imageSmoothingEnabled;
|
||||
context.imageSmoothingQuality = imageSmoothingQuality;
|
||||
context.drawImage.apply(context, [image].concat(_toConsumableArray(params.map(function (param) {
|
||||
return Math.floor(normalizeDecimalNumber(param));
|
||||
}))));
|
||||
/**
|
||||
* FLECTRA FIX START
|
||||
*
|
||||
* Canevas is translated and then translated back. For the second translation the
|
||||
* translation distances were rounded to the nearest integer below when it should
|
||||
* not since the distances of the first translation are either an integer or the
|
||||
* half of an integer.
|
||||
*
|
||||
* Fix proposed by https://github.com/fengyuanchen/cropperjs/pull/866
|
||||
*/
|
||||
params = params.map(normalizeDecimalNumber);
|
||||
context.drawImage(image, params[0], params[1], Math.floor(params[2]), Math.floor(params[3]));
|
||||
// FLECTRA FIX END
|
||||
context.restore();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@@ -2263,6 +2263,9 @@ var SnippetsMenu = Widget.extend({
|
||||
const mutexExecResult = this._mutex.exec(action);
|
||||
if (!this.loadingTimers[contentLoading]) {
|
||||
const addLoader = () => {
|
||||
if (this.loadingElements[contentLoading]) {
|
||||
return;
|
||||
}
|
||||
this.loadingElements[contentLoading] = this._createLoadingElement();
|
||||
if (contentLoading) {
|
||||
this.$snippetEditorArea.append(this.loadingElements[contentLoading]);
|
||||
|
||||
@@ -1258,7 +1258,12 @@ header {
|
||||
@include media-breakpoint-up(lg) {
|
||||
#wrapwrap.o_footer_effect_enable {
|
||||
> main {
|
||||
background-color: $body-bg;
|
||||
@if o-website-value('layout') == 'full' {
|
||||
// Ensure a transparent snippet at the end of the content
|
||||
// still appears with the same background when hovering the
|
||||
// footer during the scroll effect.
|
||||
background-color: $body-bg;
|
||||
}
|
||||
@if o-website-value('footer-effect') == 'slideout_shadow' {
|
||||
box-shadow: $box-shadow;
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
|
||||
$ribbons.removeClass(htmlClasses);
|
||||
|
||||
$ribbons.addClass(ribbon.html_class || '');
|
||||
$ribbons.css('color', ribbon.text_color);
|
||||
$ribbons.css('color', ribbon.text_color || '');
|
||||
$ribbons.css('background-color', ribbon.bg_color || '');
|
||||
|
||||
if (!this.ribbons[widgetValue]) {
|
||||
@@ -448,6 +448,7 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
|
||||
*/
|
||||
createRibbon(previewMode, widgetValue, params) {
|
||||
this.saveMethod = 'create';
|
||||
this.setRibbon(false);
|
||||
this.$ribbon.html('Ribbon text');
|
||||
this.$ribbon.addClass('bg-primary o_ribbon_left');
|
||||
this._toggleEditingUI(true);
|
||||
@@ -585,7 +586,7 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
|
||||
colorClasses,
|
||||
isTag: /o_tag_(left|right)/.test(ribbon.html_class),
|
||||
isLeft: /o_(tag|ribbon)_left/.test(ribbon.html_class),
|
||||
textColor: ribbon.text_color || colorClasses ? 'currentColor' : defaultTextColor,
|
||||
textColor: ribbon.text_color || (colorClasses ? 'currentColor' : defaultTextColor),
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1783,7 +1783,7 @@
|
||||
</div>
|
||||
|
||||
<div t-if="payment_tx_id.acquirer_id.qr_code and (payment_tx_id.acquirer_id.provider == 'transfer')">
|
||||
<t t-set="qr_code" t-value="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(order.amount_total,payment_tx_id.reference, None, payment_tx_id.currency_id, payment_tx_id.partner_id)"/>
|
||||
<t t-set="qr_code" t-value="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_base64(order.amount_total,payment_tx_id.reference, None, payment_tx_id.currency_id, payment_tx_id.partner_id)"/>
|
||||
<div class="card-body" t-if="qr_code">
|
||||
<h3>Or scan me with your banking app.</h3>
|
||||
<img class="border border-dark rounded" t-att-src="qr_code"/>
|
||||
|
||||
Reference in New Issue
Block a user