[PATCH] Upstream patch - 01102021

This commit is contained in:
Parthiv Patel
2021-10-01 09:35:51 +00:00
parent 9de7d82200
commit 3b665814f6
42 changed files with 520 additions and 91 deletions
+6 -4
View File
@@ -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'},
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+50 -2
View File
@@ -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
+13 -7
View File
@@ -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':
+1 -1
View File
@@ -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'
+3 -4
View File
@@ -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
+17 -10
View File
@@ -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
+1 -1
View File
@@ -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,
-2
View File
@@ -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 {
+1 -1
View File
@@ -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>
+5 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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);
+2
View File
@@ -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
+4 -1
View File
@@ -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...
+4 -4
View File
@@ -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>
+6 -2
View File
@@ -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',
+13
View File
@@ -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 = []
+3
View File
@@ -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')
+20 -12
View File
@@ -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`.
-1
View File
@@ -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):
+122 -6
View File
@@ -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)
+1
View File
@@ -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
})
+3 -1
View File
@@ -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 {
+40
View File
@@ -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.
+1
View File
@@ -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]);
+6 -1
View File
@@ -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),
}));
});
},
+1 -1
View File
@@ -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"/>