[PATCH] Upstream patch - 04122022

This commit is contained in:
Parthiv Patel
2022-12-04 08:35:21 +00:00
parent e1da90251c
commit 83133f0f89
20 changed files with 615 additions and 357 deletions

View File

@@ -2685,6 +2685,9 @@ class AccountMove(models.Model):
if move.is_invoice(include_receipts=True) and float_compare(move.amount_total, 0.0, precision_rounding=move.currency_id.rounding) < 0:
raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead. Use the action menu to transform it into a credit note or refund."))
if move.line_ids.account_id.filtered(lambda account: account.deprecated):
raise UserError(_("A line of this move is using a deprecated account, you cannot post it."))
# Handle case when the invoice_date is not set. In that case, the invoice_date is set at today and then,
# lines are recomputed accordingly.
# /!\ 'check_move_validity' must be there since the dynamic lines will be recomputed outside the 'onchange'

View File

@@ -3426,3 +3426,22 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
},
],
)
def test_out_invoice_depreciated_account(self):
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner_a.id,
'journal_id': self.company_data['default_journal_sale'].id,
'invoice_line_ids': [
(0, 0, {
'name': 'My super product.',
'quantity': 1.0,
'price_unit': 750.0,
'account_id': self.product_a.property_account_income_id.id,
})
],
})
self.product_a.property_account_income_id.deprecated = True
with self.assertRaises(UserError), self.cr.savepoint():
move.action_post()

View File

@@ -148,6 +148,7 @@ class AccountEdiFormat(models.Model):
'net_price_subtotal': taxes_res['total_excluded'],
'price_discount_unit': (gross_price_subtotal - line.price_subtotal) / line.quantity if line.quantity else 0.0,
'unece_uom_code': line.product_id.product_tmpl_id.uom_id._get_unece_code(),
'gross_price_total_unit': line._prepare_edi_vals_to_export()['gross_price_total_unit']
}
for tax_res in taxes_res['taxes']:
@@ -159,7 +160,6 @@ class AccountEdiFormat(models.Model):
'tax_base_amount': tax_res['base'],
'unece_tax_category_code': tax_category_code,
})
line_template_values['gross_price_total_unit'] = line._prepare_edi_vals_to_export()['gross_price_total_unit']
template_values['invoice_line_values'].append(line_template_values)

View File

@@ -80,9 +80,9 @@ EU_TAG_MAP = {
},
# France
'l10n_fr.l10n_fr_pcg_chart_template': {
'invoice_base_tag': None,
'invoice_base_tag': 'l10n_fr.tax_report_E3',
'invoice_tax_tag': None,
'refund_base_tag': None,
'refund_base_tag': 'l10n_fr.tax_report_F8',
'refund_tax_tag': None,
},
# Germany SKR03

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,23 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import models, fields
from flectra.tools.sql import column_exists, create_column
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
l10n_pe_group_id = fields.Many2one("account.group", related="account_id.group_id", store=True)
def _auto_init(self):
"""
Create column to stop ORM from computing it himself (too slow)
"""
if not column_exists(self.env.cr, self._table, 'l10n_pe_group_id'):
create_column(self.env.cr, self._table, 'l10n_pe_group_id', 'int4')
self.env.cr.execute("""
UPDATE account_move_line line
SET l10n_pe_group_id = account.group_id
FROM account_account account
WHERE account.id = line.account_id
""")
return super()._auto_init()

View File

@@ -349,6 +349,7 @@ class PosOrder(models.Model):
def action_stock_picking(self):
self.ensure_one()
action = self.env['ir.actions.act_window']._for_xml_id('stock.action_picking_tree_ready')
action['display_name'] = _('Pickings')
action['context'] = {}
action['domain'] = [('id', 'in', self.picking_ids.ids)]
return action

View File

@@ -147,6 +147,7 @@ class PosSession(models.Model):
def action_stock_picking(self):
self.ensure_one()
action = self.env['ir.actions.act_window']._for_xml_id('stock.action_picking_tree_ready')
action['display_name'] = _('Pickings')
action['context'] = {}
action['domain'] = [('id', 'in', self.picking_ids.ids)]
return action

View File

@@ -45,7 +45,7 @@ class PurchaseOrderLine(models.Model):
def _compute_qty_received(self):
kit_lines = self.env['purchase.order.line']
for line in self:
if line.qty_received_method == 'stock_moves' and line.move_ids:
if line.qty_received_method == 'stock_moves' and line.move_ids.filtered(lambda m: m.state != 'cancel'):
kit_bom = self.env['mrp.bom']._bom_find(product=line.product_id, company_id=line.company_id.id, bom_type='phantom')
if kit_bom:
moves = line.move_ids.filtered(lambda m: m.state == 'done' and not m.scrapped)

View File

@@ -67,13 +67,22 @@ class StockMove(models.Model):
if self.purchase_line_id:
purchase_currency = self.purchase_line_id.currency_id
if purchase_currency != self.company_id.currency_id:
# Do not use price_unit since we want the price tax excluded. And by the way, qty
# is in the UOM of the product, not the UOM of the PO line.
purchase_price_unit = (
self.purchase_line_id.price_subtotal / self.purchase_line_id.product_uom_qty
if self.purchase_line_id.product_uom_qty
else self.purchase_line_id.price_unit
)
if(self.purchase_line_id.product_id.cost_method == 'standard'):
purchase_price_unit = self.purchase_line_id.product_id.cost_currency_id._convert(
self.purchase_line_id.product_id.standard_price,
purchase_currency,
self.company_id,
self.date,
round=False,
)
else:
# Do not use price_unit since we want the price tax excluded. And by the way, qty
# is in the UOM of the product, not the UOM of the PO line.
purchase_price_unit = (
self.purchase_line_id.price_subtotal / self.purchase_line_id.product_uom_qty
if self.purchase_line_id.product_uom_qty
else self.purchase_line_id.price_unit
)
currency_move_valuation = purchase_currency.round(purchase_price_unit * abs(qty))
rslt['credit_line_vals']['amount_currency'] = rslt['credit_line_vals']['credit'] and -currency_move_valuation or currency_move_valuation
rslt['credit_line_vals']['currency_id'] = purchase_currency.id

View File

@@ -527,6 +527,186 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon):
# has gone to the stock account, and must be reflected in inventory valuation
self.assertEqual(self.product1.value_svl, 150)
def test_standard_valuation_multicurrency(self):
company = self.env.user.company_id
company.anglo_saxon_accounting = True
company.currency_id = self.usd_currency
date_po = '2019-01-01'
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
self.product1.property_account_creditor_price_difference = self.price_diff_account
self.product1.standard_price = 10
# SetUp currency and rates 1$ = 2 Euros
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
self.env['res.currency.rate'].search([]).unlink()
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 1.0,
'currency_id': self.usd_currency.id,
'company_id': company.id,
})
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 2,
'currency_id': self.eur_currency.id,
'company_id': company.id,
})
# Create PO
po = self.env['purchase.order'].create({
'currency_id': self.eur_currency.id,
'partner_id': self.partner_id.id,
'order_line': [
(0, 0, {
'name': self.product1.name,
'product_id': self.product1.id,
'product_qty': 1.0,
'product_uom': self.product1.uom_po_id.id,
'price_unit': 100.0, # 50$
'date_planned': date_po,
}),
],
})
po.button_confirm()
# Receive the goods
receipt = po.picking_ids[0]
receipt.move_lines.quantity_done = 1
receipt.button_validate()
# Create a vendor bill
inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
'move_type': 'in_invoice',
'invoice_date': date_po,
'date': date_po,
'currency_id': self.eur_currency.id,
'partner_id': self.partner_id.id,
'invoice_line_ids': [(0, 0, {
'name': 'Test',
'price_unit': 100.0,
'product_id': self.product1.id,
'purchase_line_id': po.order_line.id,
'quantity': 1.0,
'account_id': self.stock_input_account.id,
})]
})
inv.action_post()
# Check what was posted in stock input account
input_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
self.assertEqual(len(input_amls), 3, "Only three lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
invoice_amls = input_amls.filtered(lambda l: l.move_id == inv)
picking_aml = input_amls - invoice_amls
payable_aml = invoice_amls.filtered(lambda l: l.amount_currency > 0)
diff_aml = invoice_amls - payable_aml
# check USD
self.assertAlmostEqual(payable_aml.debit, 50, "Total debit value should be equal to the original PO price of the product.")
self.assertAlmostEqual(picking_aml.credit, 10, "credit value for stock should be equal to the standard price of the product.")
self.assertAlmostEqual(diff_aml.credit, 40, "credit value for price difference")
# check EUR
self.assertAlmostEqual(payable_aml.amount_currency, 100, "Total debit value should be equal to the original PO price of the product.")
self.assertAlmostEqual(picking_aml.amount_currency, -20, "credit value for stock should be equal to the standard price of the product.")
self.assertAlmostEqual(diff_aml.amount_currency, -80, "credit value for price difference")
def test_valuation_multicurecny_with_tax(self):
""" Check that a tax without account will increment the stock value.
"""
company = self.env.user.company_id
company.anglo_saxon_accounting = True
company.currency_id = self.usd_currency
date_po = '2019-01-01'
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
self.product1.property_account_creditor_price_difference = self.price_diff_account
# SetUp currency and rates 1$ = 2Euros
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
self.env['res.currency.rate'].search([]).unlink()
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 1.0,
'currency_id': self.usd_currency.id,
'company_id': company.id,
})
self.env['res.currency.rate'].create({
'name': date_po,
'rate': 2,
'currency_id': self.eur_currency.id,
'company_id': company.id,
})
tax_with_no_account = self.env['account.tax'].create({
'name': "Tax with no account",
'amount_type': 'fixed',
'amount': 5,
'sequence': 8,
'price_include': True,
})
# Create PO
po = self.env['purchase.order'].create({
'currency_id': self.eur_currency.id,
'partner_id': self.partner_id.id,
'order_line': [
(0, 0, {
'name': self.product1.name,
'product_id': self.product1.id,
'product_qty': 1.0,
'product_uom': self.product1.uom_po_id.id,
'price_unit': 100.0, # 50$
'taxes_id': [(4, tax_with_no_account.id)],
'date_planned': date_po,
}),
],
})
po.button_confirm()
# Receive the goods
receipt = po.picking_ids[0]
receipt.move_lines.quantity_done = 1
receipt.button_validate()
# Create a vendor bill
inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
'move_type': 'in_invoice',
'invoice_date': date_po,
'date': date_po,
'currency_id': self.eur_currency.id,
'partner_id': self.partner_id.id,
'invoice_line_ids': [(0, 0, {
'name': 'Test',
'price_unit': 100.0,
'product_id': self.product1.id,
'purchase_line_id': po.order_line.id,
'quantity': 1.0,
'account_id': self.stock_input_account.id,
})]
})
inv.action_post()
# Check what was posted in stock input account
input_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
self.assertEqual(len(input_amls), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
invoice_aml = input_amls.filtered(lambda l: l.move_id == inv)
picking_aml = input_amls - invoice_aml
# check EUR
self.assertAlmostEqual(invoice_aml.amount_currency, 100, "Total debit value should be equal to the original PO price of the product.")
self.assertAlmostEqual(picking_aml.amount_currency, -95, "credit value for stock should be equal to the untaxed price of the product.")
def test_average_realtime_anglo_saxon_valuation_multicurrency_same_date(self):
"""
The PO and invoice are in the same foreign currency.

View File

@@ -4,7 +4,7 @@ import re
import base64
import io
from PyPDF2 import PdfFileReader, PdfFileMerger
from PyPDF2 import PdfFileReader, PdfFileMerger, PdfFileWriter
from reportlab.platypus import Frame, Paragraph, KeepInFrame
from reportlab.lib.units import mm
from reportlab.lib.pagesizes import A4
@@ -144,6 +144,7 @@ class SnailmailLetter(models.Model):
if (paperformat.format == 'custom' and paperformat.page_width != 210 and paperformat.page_height != 297) or paperformat.format != 'A4':
raise UserError(_("Please use an A4 Paper format."))
pdf_bin, unused_filetype = report.with_context(snailmail_layout=not self.cover, lang='en_US')._render_qweb_pdf(self.res_id)
pdf_bin = self._overwrite_margins(pdf_bin)
if self.cover:
pdf_bin = self._append_cover_page(pdf_bin)
attachment = self.env['ir.attachment'].create({
@@ -457,3 +458,49 @@ class SnailmailLetter(models.Model):
out_buff = io.BytesIO()
merger.write(out_buff)
return out_buff.getvalue()
def _overwrite_margins(self, invoice_bin: bytes):
"""
Fill the margins with white for validation purposes.
"""
pdf_buf = io.BytesIO()
canvas = Canvas(pdf_buf, pagesize=A4)
canvas.setFillColorRGB(255, 255, 255)
page_width = A4[0]
page_height = A4[1]
# Horizontal Margin
hmargin_width = page_width
hmargin_height = 5 * mm
# Vertical Margin
vmargin_width = 5 * mm
vmargin_height = page_height
# Bottom left square
sq_width = 15 * mm
# Draw the horizontal margins
canvas.rect(0, 0, hmargin_width, hmargin_height, stroke=0, fill=1)
canvas.rect(0, page_height, hmargin_width, -hmargin_height, stroke=0, fill=1)
# Draw the vertical margins
canvas.rect(0, 0, vmargin_width, vmargin_height, stroke=0, fill=1)
canvas.rect(page_width, 0, -vmargin_width, vmargin_height, stroke=0, fill=1)
# Draw the bottom left white square
canvas.rect(0, 0, sq_width, sq_width, stroke=0, fill=1)
canvas.save()
pdf_buf.seek(0)
new_pdf = PdfFileReader(pdf_buf)
curr_pdf = PdfFileReader(io.BytesIO(invoice_bin))
out = PdfFileWriter()
for page in curr_pdf.pages:
page.mergePage(new_pdf.getPage(0))
out.addPage(page)
out_stream = io.BytesIO()
out.write(out_stream)
out_bin = out_stream.getvalue()
out_stream.close()
return out_bin

View File

@@ -241,7 +241,8 @@ class StockQuant(models.Model):
@api.constrains('quantity')
def check_quantity(self):
for quant in self:
if float_compare(quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding) > 0 and quant.lot_id and quant.product_id.tracking == 'serial':
if quant.location_id.usage != 'inventory' and quant.lot_id and quant.product_id.tracking == 'serial' \
and float_compare(abs(quant.quantity), 1, precision_rounding=quant.product_uom_id.rounding) > 0:
raise ValidationError(_('The serial number has already been assigned: \n Product: %s, Serial Number: %s') % (quant.product_id.display_name, quant.lot_id.name))
@api.constrains('location_id')

View File

@@ -226,9 +226,9 @@ class StockPickingBatch(models.Model):
precision_rounding=ml.product_uom_id.rounding) > 0 and float_compare(ml.qty_done, 0.0,
precision_rounding=ml.product_uom_id.rounding) == 0)
if move_line_ids:
res = self.picking_ids[0]._pre_put_in_pack_hook(move_line_ids)
res = move_line_ids.picking_id[0]._pre_put_in_pack_hook(move_line_ids)
if not res:
res = self.picking_ids[0]._put_in_pack(move_line_ids, False)
res = move_line_ids.picking_id[0]._put_in_pack(move_line_ids, False)
return res
else:
raise UserError(_("Please add 'Done' quantities to the batch picking to create a new pack."))

View File

@@ -197,6 +197,7 @@ var SnippetEditor = Widget.extend({
if (this.isDestroyed()) {
return;
}
this.willDestroyEditors = true;
await this.toggleTargetVisibility(!this.$target.hasClass('o_snippet_invisible'));
const proms = _.map(this.styles, option => {
return option.cleanForSave();
@@ -925,6 +926,10 @@ var SnippetEditor = Widget.extend({
* @param {FlectraEvent} ev
*/
_onSnippetOptionVisibilityUpdate: function (ev) {
if (this.willDestroyEditors) {
// Do not update the option visibilities if we are destroying them.
return;
}
ev.data.show = this._toggleVisibilityStatus(ev.data.show);
},
/**
@@ -1290,6 +1295,7 @@ var SnippetsMenu = Widget.extend({
// may be the moment where the public widgets need to be destroyed).
this.trigger_up('ready_to_clean_for_save');
this.willDestroyEditors = true;
// Then destroy all snippet editors, making them call their own
// "clean for save" methods (and options ones).
await this._destroyEditors();
@@ -2840,6 +2846,10 @@ var SnippetsMenu = Widget.extend({
* @param {FlectraEvent} ev
*/
_onSnippetOptionVisibilityUpdate: async function (ev) {
if (this.willDestroyEditors) {
// Do not update the option visibilities if we are destroying them.
return;
}
if (!ev.data.show) {
await this._activateSnippet(false);
}

View File

@@ -1939,8 +1939,20 @@ const VisibilityPageOptionUpdate = options.Class.extend({
*/
async start() {
await this._super(...arguments);
const shown = await this._isShown();
this.trigger_up('snippet_option_visibility_update', {show: shown});
// When entering edit mode via the URL (enable_editor) the WebsiteNavbar
// is not yet ReadyForActions because it is waiting for its
// sub-component EditPageMenu to start edit mode. Then invisible blocks
// options start (so this option too). But for isShown() to work, the
// navbar must be ReadyForActions. This is the reason why we can't wait
// for isShown here, otherwise we would have a deadlock. On one hand the
// navbar waiting for the invisible snippets options to be started to be
// ReadyForActions and on the other hand this option which needs the
// navbar to be ReadyForActions to be started.
// TODO in master: Use the data-invisible system to get rid of this
// piece of code.
this._isShown().then(isShown => {
this.trigger_up('snippet_option_visibility_update', {show: isShown});
});
},
/**
* @override

View File

@@ -15,8 +15,9 @@
// $enable-gradients: true;
//
// Notice that Flectra already overrides bootstrap variables according to your
// choices in the "Customize Theme" dialog, you should first take a look at
// it and do customizations this way. Indeed, if you overridde the same
// variables, Flectra will either have to ignore them or not be able to make
// the "Customize Theme" dialog work for these variables anymore.
// choices via the website builder (especially 3rd tab of the editor panel). You
// should first take a look at it and do customizations this way. Indeed, if you
// override the same variables yourself, Flectra will either have to ignore them or
// not be able to make the website builder work properly for these variables
// anymore.
//

View File

@@ -1271,7 +1271,21 @@ header {
@if index(('slideout_slide_hover', 'slideout_shadow'), o-website-value('footer-effect')) {
@include media-breakpoint-up(lg) {
#wrapwrap.o_footer_effect_enable {
// This effect is disabled when a modal is opened. This is the easiest
// and probably most stable solution for this problem:
// - Add a popup in your page and select it to be for "All pages"
// => In that case it ends up in the footer of your page
// - Enable the "Slide Hover" effect for your footer
//
// => In that case, when the popup opens, it is not visible because of
// the footer z-index ("Slide Hover" effect) and it actually also
// prevents the user to scroll.
//
// TODO in master, we may want to put such popups elsewhere than in the
// footer. When the footer is hidden, this is also a problem: the popup
// for all pages cannot be visible ever. This is considered a limitation
// in stable versions though.
body:not(.modal-open) #wrapwrap.o_footer_effect_enable {
> main {
@if o-website-value('layout') == 'full' {
// Ensure a transparent snippet at the end of the content

View File

@@ -51,6 +51,7 @@ function clickAndCheck(blockID, expected) {
window.focusBlurSnippetsResult = [];
tour.register('focus_blur_snippets', {
test: true,
url: '/?enable_editor=1',
}, [
{

View File

@@ -5,7 +5,7 @@ var rpc = require('web.rpc');
var tour = require("web_tour.tour");
tour.register('shop_wishlist_admin', {
test: false,
test: true,
url: '/shop',
},
[