mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-17 16:54:42 -05:00
[PATCH] Upstream patch - 04032023
This commit is contained in:
@@ -8,6 +8,7 @@ from flectra.tools import html_escape
|
||||
from flectra.exceptions import RedirectWarning
|
||||
|
||||
from lxml import etree
|
||||
from struct import error as StructError
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
@@ -393,7 +394,7 @@ class AccountEdiFormat(models.Model):
|
||||
try:
|
||||
for xml_name, content in pdf_reader.getAttachments():
|
||||
to_process.extend(self._decode_xml(xml_name, content))
|
||||
except NotImplementedError as e:
|
||||
except (NotImplementedError, StructError) as e:
|
||||
_logger.warning("Unable to access the attachments of %s. Tried to decrypt it, but %s." % (filename, e))
|
||||
|
||||
# Process the pdf itself.
|
||||
|
||||
@@ -35,7 +35,7 @@ def validate_iban(iban):
|
||||
raise ValidationError(_("The IBAN is invalid, it should begin with the country code"))
|
||||
|
||||
iban_template = _map_iban_template[country_code]
|
||||
if len(iban) != len(iban_template.replace(' ', '')):
|
||||
if len(iban) != len(iban_template.replace(' ', '')) or not re.fullmatch("[a-zA-Z0-9]+", iban):
|
||||
raise ValidationError(_("The IBAN does not seem to be correct. You should have entered something like this %s\n"
|
||||
"Where B = National bank code, S = Branch code, C = Account No, k = Check digit") % iban_template)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<field name="model_id" ref="model_hr_contract"/>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.update_state()</field>
|
||||
<field name="code">model.with_context(from_cron=True).update_state()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import threading
|
||||
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
@@ -9,6 +11,10 @@ from flectra.exceptions import ValidationError
|
||||
|
||||
from flectra.osv import expression
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Contract(models.Model):
|
||||
_name = 'hr.contract'
|
||||
_description = 'Contract'
|
||||
@@ -130,6 +136,7 @@ class Contract(models.Model):
|
||||
|
||||
@api.model
|
||||
def update_state(self):
|
||||
from_cron = 'from_cron' in self.env.context
|
||||
contracts = self.search([
|
||||
('state', '=', 'open'), ('kanban_state', '!=', 'blocked'),
|
||||
'|',
|
||||
@@ -147,20 +154,23 @@ class Contract(models.Model):
|
||||
_("The contract of %s is about to expire.", contract.employee_id.name),
|
||||
user_id=contract.hr_responsible_id.id or self.env.uid)
|
||||
|
||||
contracts.write({'kanban_state': 'blocked'})
|
||||
if contracts:
|
||||
contracts._safe_write_for_cron({'kanban_state': 'blocked'}, from_cron)
|
||||
|
||||
self.search([
|
||||
contracts_to_close = self.search([
|
||||
('state', '=', 'open'),
|
||||
'|',
|
||||
('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))),
|
||||
('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))),
|
||||
]).write({
|
||||
'state': 'close'
|
||||
})
|
||||
])
|
||||
|
||||
self.search([('state', '=', 'draft'), ('kanban_state', '=', 'done'), ('date_start', '<=', fields.Date.to_string(date.today())),]).write({
|
||||
'state': 'open'
|
||||
})
|
||||
if contracts_to_close:
|
||||
contracts_to_close._safe_write_for_cron({'state': 'close'}, from_cron)
|
||||
|
||||
contracts_to_open = self.search([('state', '=', 'draft'), ('kanban_state', '=', 'done'), ('date_start', '<=', fields.Date.to_string(date.today())),])
|
||||
|
||||
if contracts_to_open:
|
||||
contracts_to_open._safe_write_for_cron({'state': 'open'}, from_cron)
|
||||
|
||||
contract_ids = self.search([('date_end', '=', False), ('state', '=', 'close'), ('employee_id', '!=', False)])
|
||||
# Ensure all closed contract followed by a new contract have a end date.
|
||||
@@ -172,17 +182,32 @@ class Contract(models.Model):
|
||||
('date_start', '>', contract.date_start)
|
||||
], order="date_start asc", limit=1)
|
||||
if next_contract:
|
||||
contract.date_end = next_contract.date_start - relativedelta(days=1)
|
||||
contract._safe_write_for_cron({'date_end': next_contract.date_start - relativedelta(days=1)}, from_cron)
|
||||
continue
|
||||
next_contract = self.search([
|
||||
('employee_id', '=', contract.employee_id.id),
|
||||
('date_start', '>', contract.date_start)
|
||||
], order="date_start asc", limit=1)
|
||||
if next_contract:
|
||||
contract.date_end = next_contract.date_start - relativedelta(days=1)
|
||||
contract._safe_write_for_cron({'date_end': next_contract.date_start - relativedelta(days=1)}, from_cron)
|
||||
|
||||
return True
|
||||
|
||||
def _safe_write_for_cron(self, vals, from_cron=False):
|
||||
if from_cron:
|
||||
auto_commit = not getattr(threading.current_thread(), 'testing', False)
|
||||
for contract in self:
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
contract.write(vals)
|
||||
except ValidationError as e:
|
||||
_logger.warning(e)
|
||||
else:
|
||||
if auto_commit:
|
||||
self.env.cr.commit()
|
||||
else:
|
||||
self.write(vals)
|
||||
|
||||
def _assign_open_contract(self):
|
||||
for contract in self:
|
||||
contract.employee_id.sudo().write({'contract_id': contract.id})
|
||||
|
||||
@@ -513,10 +513,9 @@
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="priority" eval="1"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Purchase Order" multi_edit="1" decoration-bf="message_unread==True"
|
||||
<tree string="Purchase Order" multi_edit="1"
|
||||
decoration-muted="state=='cancel'" decoration-info="state in ('wait','confirmed')" sample="1">
|
||||
<field name="priority" optional="show" widget="priority" nolabel="1"/>
|
||||
<field name="message_unread" invisible="1"/>
|
||||
<field name="partner_ref" optional="hide"/>
|
||||
<field name="name" string="Reference" readonly="1"/>
|
||||
<field name="date_order" invisible="not context.get('quotation_only', False)" optional="show"/>
|
||||
@@ -544,13 +543,12 @@
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Purchase Order" multi_edit="1" decoration-bf="message_unread==True"
|
||||
class="o_purchase_order" js_class="purchase_list_dashboard" sample="1">
|
||||
<tree string="Purchase Order" multi_edit="1" class="o_purchase_order"
|
||||
js_class="purchase_list_dashboard" sample="1">
|
||||
<header>
|
||||
<button name="action_create_invoice" type="object" string="Create Bills"/>
|
||||
</header>
|
||||
<field name="priority" optional="show" widget="priority" nolabel="1"/>
|
||||
<field name="message_unread" invisible="1"/>
|
||||
<field name="partner_ref" optional="hide"/>
|
||||
<field name="name" string="Reference" readonly="1" decoration-bf="1"/>
|
||||
<field name="date_approve" invisible="context.get('quotation_only', False)" optional="show"/>
|
||||
@@ -577,8 +575,7 @@
|
||||
<field name="name">purchase.order.view.tree</field>
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-bf="message_unread==True"
|
||||
decoration-muted="state=='cancel'"
|
||||
<tree decoration-muted="state=='cancel'"
|
||||
decoration-info="state in ('wait','confirmed')"
|
||||
string="Purchase Order"
|
||||
class="o_purchase_order"
|
||||
@@ -587,7 +584,6 @@
|
||||
<button name="action_create_invoice" type="object" string="Create Bills"/>
|
||||
</header>
|
||||
<field name="priority" optional="show" widget="priority" nolabel="1"/>
|
||||
<field name="message_unread" invisible="1"/>
|
||||
<field name="partner_ref" optional="hide"/>
|
||||
<field name="name" string="Reference" readonly="1" decoration-bf="1"/>
|
||||
<field name="date_approve" widget="date" invisible="context.get('quotation_only', False)" optional="show"/>
|
||||
|
||||
@@ -16,12 +16,7 @@ class AccountMove(models.Model):
|
||||
for line in line_ids:
|
||||
try:
|
||||
line.sale_line_ids.tax_id = line.tax_ids
|
||||
if all(line.tax_ids.mapped('price_include')):
|
||||
line.sale_line_ids.price_unit = line.price_unit
|
||||
else:
|
||||
#To keep positive amount on the sale order and to have the right price for the invoice
|
||||
#We need the - before our untaxed_amount_to_invoice
|
||||
line.sale_line_ids.price_unit = -line.sale_line_ids.untaxed_amount_to_invoice
|
||||
line.sale_line_ids.price_unit = line.price_unit
|
||||
except UserError:
|
||||
# a UserError here means the SO was locked, which prevents changing the taxes
|
||||
# just ignore the error - this is a nice to have feature and should not be blocking
|
||||
|
||||
@@ -133,6 +133,52 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
self.assertEqual(len(invoice.invoice_line_ids.filtered(lambda l: l.display_type == 'line_section' and l.name == "Down Payments")), 1, 'A single section for downpayments should be present')
|
||||
self.assertEqual(invoice.amount_total, self.sale_order.amount_total - sum(downpayment_line.mapped('price_unit')), 'Downpayment should be applied')
|
||||
|
||||
def test_downpayment_line_remains_on_SO(self):
|
||||
""" Test downpayment's SO line is created and remains unchanged even if everything is invoiced
|
||||
"""
|
||||
# Create the SO with one line
|
||||
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
sale_order_line = self.env['sale.order.line'].with_context(tracking_disable=True).create({
|
||||
'name': self.company_data['product_order_no'].name,
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': self.company_data['product_order_no'].uom_id.id,
|
||||
'price_unit': self.company_data['product_order_no'].list_price,
|
||||
'order_id': sale_order.id,
|
||||
'tax_id': False,
|
||||
})
|
||||
# Confirm the SO
|
||||
sale_order.action_confirm()
|
||||
# Update delivered quantity of SO line
|
||||
sale_order_line.write({'qty_delivered': 5.0})
|
||||
context = {
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': [sale_order.id],
|
||||
'active_id': sale_order.id,
|
||||
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||||
}
|
||||
# Let's do an invoice for a down payment of 50
|
||||
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'fixed',
|
||||
'fixed_amount': 50,
|
||||
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||||
})
|
||||
downpayment.create_invoices()
|
||||
# Let's do the invoice
|
||||
payment = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||||
})
|
||||
payment.create_invoices()
|
||||
# Confirm all invoices
|
||||
for invoice in sale_order.invoice_ids:
|
||||
invoice.action_post()
|
||||
downpayment_line = sale_order.order_line.filtered(lambda l: l.is_downpayment)
|
||||
self.assertEqual(downpayment_line[0].price_unit, 50, 'The down payment unit price should not change on SO')
|
||||
|
||||
def test_downpayment_percentage_tax_icl(self):
|
||||
""" Test invoice with a percentage downpayment and an included tax
|
||||
Check the total amount of invoice is correct and equal to a respective sale order's total amount
|
||||
|
||||
@@ -15,8 +15,12 @@ class AccountMoveLine(models.Model):
|
||||
if bom:
|
||||
is_line_reversing = self.move_id.move_type == 'out_refund'
|
||||
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
|
||||
posted_invoice_lines = so_line.invoice_lines.filtered(lambda l: l.move_id.state == 'posted' and bool(l.move_id.reversed_entry_id) == is_line_reversing)
|
||||
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
|
||||
posted_invoice_lines = account_moves.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
|
||||
qty_invoiced = sum([x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in posted_invoice_lines])
|
||||
reversal_cogs = posted_invoice_lines.move_id.reversal_move_id.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
|
||||
qty_invoiced -= sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in reversal_cogs])
|
||||
|
||||
moves = so_line.move_ids
|
||||
average_price_unit = 0
|
||||
components_qty = so_line._get_bom_component_qty(bom)
|
||||
|
||||
@@ -2358,3 +2358,76 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
|
||||
|
||||
price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
|
||||
self.assertEqual(price, 10)
|
||||
|
||||
def test_fifo_reverse_and_create_new_invoice(self):
|
||||
"""
|
||||
FIFO automated
|
||||
Kit with one component
|
||||
Receive the component: 1@10, 1@50
|
||||
Deliver 1 kit
|
||||
Post the invoice, add a credit note with option 'new draft inv'
|
||||
Post the second invoice
|
||||
COGS should be based on the delivered kit
|
||||
"""
|
||||
kit = self._create_product('Simple Kit', self.uom_unit)
|
||||
categ_form = Form(self.env['product.category'])
|
||||
categ_form.name = 'Super Fifo'
|
||||
categ_form.property_cost_method = 'fifo'
|
||||
categ_form.property_valuation = 'real_time'
|
||||
categ = categ_form.save()
|
||||
(kit + self.component_a).categ_id = categ
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [(0, 0, {'product_id': self.component_a.id, 'product_qty': 1.0})]
|
||||
})
|
||||
|
||||
in_moves = self.env['stock.move'].create([{
|
||||
'name': 'IN move @%s' % p,
|
||||
'product_id': self.component_a.id,
|
||||
'location_id': self.env.ref('stock.stock_location_suppliers').id,
|
||||
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
|
||||
'product_uom': self.component_a.uom_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': p,
|
||||
} for p in [10, 50]])
|
||||
in_moves._action_confirm()
|
||||
in_moves.quantity_done = 1
|
||||
in_moves._action_done()
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_1').id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': kit.name,
|
||||
'product_id': kit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': kit.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'tax_id': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
picking = so.picking_ids
|
||||
picking.move_lines.quantity_done = 1.0
|
||||
picking.button_validate()
|
||||
|
||||
invoice01 = so._create_invoices()
|
||||
invoice01.action_post()
|
||||
|
||||
wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
|
||||
'refund_method': 'modify',
|
||||
})
|
||||
invoice02 = self.env['account.move'].browse(wizard.reverse_moves()['res_id'])
|
||||
invoice02.action_post()
|
||||
|
||||
amls = invoice02.line_ids
|
||||
stock_out_aml = amls.filtered(lambda aml: aml.account_id == categ.property_stock_account_output_categ_id)
|
||||
self.assertEqual(stock_out_aml.debit, 0)
|
||||
self.assertEqual(stock_out_aml.credit, 10)
|
||||
cogs_aml = amls.filtered(lambda aml: aml.account_id == categ.property_account_expense_categ_id)
|
||||
self.assertEqual(cogs_aml.debit, 10)
|
||||
self.assertEqual(cogs_aml.credit, 0)
|
||||
|
||||
@@ -120,10 +120,15 @@ class AccountMoveLine(models.Model):
|
||||
is_line_reversing = self.move_id.move_type == 'out_refund'
|
||||
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
|
||||
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
|
||||
|
||||
posted_cogs = account_moves.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
|
||||
qty_invoiced = sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in posted_cogs])
|
||||
value_invoiced = sum(posted_cogs.mapped('balance'))
|
||||
|
||||
reversal_cogs = posted_cogs.move_id.reversal_move_id.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
|
||||
qty_invoiced -= sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in reversal_cogs])
|
||||
value_invoiced -= sum(reversal_cogs.mapped('balance'))
|
||||
|
||||
product = self.product_id.with_company(self.company_id).with_context(is_returned=is_line_reversing, value_invoiced=value_invoiced)
|
||||
average_price_unit = product._compute_average_price(qty_invoiced, qty_to_invoice, so_line.move_ids)
|
||||
if average_price_unit:
|
||||
|
||||
@@ -1561,3 +1561,62 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
|
||||
(invoice01 | invoice03).action_post()
|
||||
cogs = invoices.line_ids.filtered(lambda l: l.account_id == out_account)
|
||||
self.assertEqual(sum(cogs.mapped('credit')), total_value)
|
||||
|
||||
def test_fifo_reverse_and_create_new_invoice(self):
|
||||
"""
|
||||
FIFO automated
|
||||
Receive 1@10, 1@50
|
||||
Deliver 1
|
||||
Post the invoice, add a credit note with option 'new draft inv'
|
||||
Post the second invoice
|
||||
COGS should be based on the delivered product
|
||||
"""
|
||||
self.product.categ_id.property_cost_method = 'fifo'
|
||||
|
||||
in_moves = self.env['stock.move'].create([{
|
||||
'name': 'IN move @%s' % p,
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.env.ref('stock.stock_location_suppliers').id,
|
||||
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': p,
|
||||
} for p in [10, 50]])
|
||||
in_moves._action_confirm()
|
||||
in_moves.quantity_done = 1
|
||||
in_moves._action_done()
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': self.product.name,
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'tax_id': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
picking = so.picking_ids
|
||||
picking.move_lines.quantity_done = 1.0
|
||||
picking.button_validate()
|
||||
|
||||
invoice01 = so._create_invoices()
|
||||
invoice01.action_post()
|
||||
|
||||
wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
|
||||
'refund_method': 'modify',
|
||||
})
|
||||
invoice02 = self.env['account.move'].browse(wizard.reverse_moves()['res_id'])
|
||||
invoice02.action_post()
|
||||
|
||||
amls = invoice02.line_ids
|
||||
stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
|
||||
self.assertEqual(stock_out_aml.debit, 0)
|
||||
self.assertEqual(stock_out_aml.credit, 10)
|
||||
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
|
||||
self.assertEqual(cogs_aml.debit, 10)
|
||||
self.assertEqual(cogs_aml.credit, 0)
|
||||
|
||||
@@ -78,7 +78,10 @@ class AccountMoveLine(models.Model):
|
||||
return [
|
||||
('so_line', 'in', sale_line_delivery.ids),
|
||||
('project_id', '!=', False),
|
||||
'|', ('timesheet_invoice_id', '=', False), ('timesheet_invoice_id.state', '=', 'cancel')
|
||||
'|', '|',
|
||||
('timesheet_invoice_id', '=', False),
|
||||
('timesheet_invoice_id.state', '=', 'cancel'),
|
||||
('timesheet_invoice_id.payment_state', '=', 'reversed')
|
||||
]
|
||||
|
||||
def unlink(self):
|
||||
|
||||
@@ -192,8 +192,10 @@ class ProductProduct(models.Model):
|
||||
fifo_vals = self._run_fifo(abs(quantity), company)
|
||||
vals['remaining_qty'] = fifo_vals.get('remaining_qty')
|
||||
# In case of AVCO, fix rounding issue of standard price when needed.
|
||||
if self.product_tmpl_id.cost_method == 'average':
|
||||
rounding_error = currency.round(self.standard_price * self.quantity_svl - self.value_svl)
|
||||
if self.product_tmpl_id.cost_method == 'average' and not float_is_zero(self.quantity_svl, precision_rounding=self.uom_id.rounding):
|
||||
rounding_error = currency.round(
|
||||
(self.standard_price * self.quantity_svl - self.value_svl) * abs(quantity / self.quantity_svl)
|
||||
)
|
||||
if rounding_error:
|
||||
# If it is bigger than the (smallest number of the currency * quantity) / 2,
|
||||
# then it isn't a rounding error but a stock valuation error, we shouldn't fix it under the hood ...
|
||||
|
||||
@@ -2087,7 +2087,7 @@ class TestStockValuation(SavepointCase):
|
||||
move5.move_line_ids.qty_done = 30.0
|
||||
move5._action_done()
|
||||
|
||||
self.assertEqual(move5.stock_valuation_layer_ids.value, -477.5)
|
||||
self.assertEqual(move5.stock_valuation_layer_ids.value, -477.56)
|
||||
|
||||
# Receives 10 units but assign them to an owner, the valuation should not be impacted.
|
||||
move6 = self.env['stock.move'].create({
|
||||
@@ -2121,7 +2121,7 @@ class TestStockValuation(SavepointCase):
|
||||
move7.move_line_ids.qty_done = 50.0
|
||||
move7._action_done()
|
||||
|
||||
self.assertEqual(move7.stock_valuation_layer_ids.value, -796.0)
|
||||
self.assertEqual(move7.stock_valuation_layer_ids.value, -795.94)
|
||||
self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
|
||||
self.assertAlmostEqual(self.product1.value_svl, 0.0)
|
||||
|
||||
|
||||
@@ -543,6 +543,17 @@ class TestStockValuationAVCO(TestStockValuationCommon):
|
||||
self.assertEqual(self.product1.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.standard_price, 1.01)
|
||||
|
||||
def test_rounding_svl_3(self):
|
||||
self._make_in_move(self.product1, 1000, unit_cost=0.17)
|
||||
self._make_in_move(self.product1, 800, unit_cost=0.23)
|
||||
|
||||
self.assertEqual(self.product1.standard_price, 0.20)
|
||||
|
||||
self._make_out_move(self.product1, 1000, create_picking=True)
|
||||
self._make_out_move(self.product1, 800, create_picking=True)
|
||||
|
||||
self.assertEqual(self.product1.value_svl, 0)
|
||||
|
||||
def test_return_delivery_2(self):
|
||||
self.product1.write({"standard_price": 1})
|
||||
move1 = self._make_out_move(self.product1, 10, create_picking=True, force_assign=True)
|
||||
|
||||
@@ -202,3 +202,86 @@ class TestStockLandedCostsRounding(TestStockLandedCostsCommon):
|
||||
lc.compute_landed_cost()
|
||||
|
||||
self.assertEqual(sum(lc.valuation_adjustment_lines.mapped('additional_landed_cost')), 1000.0)
|
||||
|
||||
def test_stock_landed_costs_rounding_03(self):
|
||||
"""
|
||||
Storable AVCO product
|
||||
Receive:
|
||||
5 @ 5
|
||||
5 @ 8
|
||||
5 @ 7
|
||||
20 @ 7.33
|
||||
Add landed cost of $5 to each receipt (except the first one)
|
||||
Deliver:
|
||||
23
|
||||
2
|
||||
10
|
||||
At the end, the SVL value should be zero
|
||||
"""
|
||||
self.product_a.type = 'product'
|
||||
self.product_a.categ_id.property_cost_method = 'average'
|
||||
|
||||
stock_location = self.warehouse.lot_stock_id
|
||||
supplier_location_id = self.ref('stock.stock_location_suppliers')
|
||||
customer_location_id = self.ref('stock.stock_location_customers')
|
||||
|
||||
receipts = self.env['stock.picking'].create([{
|
||||
'picking_type_id': self.warehouse.in_type_id.id,
|
||||
'location_id': supplier_location_id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'move_lines': [(0, 0, {
|
||||
'name': self.product_a.name,
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': price,
|
||||
'product_uom': self.product_a.uom_id.id,
|
||||
'product_uom_qty': qty,
|
||||
'location_id': supplier_location_id,
|
||||
'location_dest_id': stock_location.id,
|
||||
})]
|
||||
} for qty, price in [
|
||||
(5, 5.0),
|
||||
(5, 8.0),
|
||||
(5, 7.0),
|
||||
(20, 7.33),
|
||||
]])
|
||||
|
||||
receipts.action_confirm()
|
||||
for m in receipts.move_lines:
|
||||
m.quantity_done = m.product_uom_qty
|
||||
receipts.button_validate()
|
||||
|
||||
landed_costs = self.env['stock.landed.cost'].create([{
|
||||
'picking_ids': [(6, 0, picking.ids)],
|
||||
'account_journal_id': self.expenses_journal.id,
|
||||
'cost_lines': [(0, 0, {
|
||||
'name': 'equal split',
|
||||
'split_method': 'equal',
|
||||
'price_unit': 5.0,
|
||||
'product_id': self.landed_cost.id
|
||||
})],
|
||||
} for picking in receipts[1:]])
|
||||
landed_costs.compute_landed_cost()
|
||||
landed_costs.button_validate()
|
||||
|
||||
self.assertEqual(self.product_a.standard_price, 7.47)
|
||||
|
||||
deliveries = self.env['stock.picking'].create([{
|
||||
'picking_type_id': self.warehouse.out_type_id.id,
|
||||
'location_id': stock_location.id,
|
||||
'location_dest_id': customer_location_id,
|
||||
'move_lines': [(0, 0, {
|
||||
'name': self.product_a.name,
|
||||
'product_id': self.product_a.id,
|
||||
'product_uom': self.product_a.uom_id.id,
|
||||
'product_uom_qty': qty,
|
||||
'location_id': stock_location.id,
|
||||
'location_dest_id': customer_location_id,
|
||||
})]
|
||||
} for qty in [23, 2, 10]])
|
||||
|
||||
deliveries.action_confirm()
|
||||
for m in deliveries.move_lines:
|
||||
m.quantity_done = m.product_uom_qty
|
||||
deliveries.button_validate()
|
||||
|
||||
self.assertEqual(self.product_a.value_svl, 0)
|
||||
|
||||
@@ -537,10 +537,15 @@ var dom = {
|
||||
return size;
|
||||
},
|
||||
/**
|
||||
* @param {HTMLElement} el - the element to stroll to (limitation: if the
|
||||
* element is using a fixed position, this function cannot work except
|
||||
* if is the header (with the "top" id) or the footer (with the
|
||||
* "bottom" id) for which exceptions have been made)
|
||||
* @param {HTMLElement|string} el - the element to scroll to. If "el" is a
|
||||
* string, it must be a valid selector of an element in the DOM or
|
||||
* '#top' or '#bottom'. If it is an HTML element, it must be present
|
||||
* in the DOM.
|
||||
* Limitation: if the element is using a fixed position, this
|
||||
* function cannot work except if is the header (el is then either a
|
||||
* string set to '#top' or an HTML element with the "top" id) or the
|
||||
* footer (el is then a string set to '#bottom' or an HTML element
|
||||
* with the "bottom" id) for which exceptions have been made.
|
||||
* @param {number} [options] - same as animate of jQuery
|
||||
* @param {number} [options.extraOffset=0]
|
||||
* extra offset to add on top of the automatic one (the automatic one
|
||||
@@ -552,15 +557,19 @@ var dom = {
|
||||
*/
|
||||
scrollTo(el, options = {}) {
|
||||
const $el = $(el);
|
||||
const $scrollable = $el.parent().closestScrollable();
|
||||
if (typeof(el) === 'string' && $el[0]) {
|
||||
el = $el[0];
|
||||
}
|
||||
const isTopOrBottomHidden = (el === '#top' || el === '#bottom');
|
||||
const $topLevelScrollable = $().getScrollingElement();
|
||||
const $scrollable = isTopOrBottomHidden ? $topLevelScrollable : $el.parent().closestScrollable();
|
||||
const isTopScroll = $scrollable.is($topLevelScrollable);
|
||||
|
||||
function _computeScrollTop() {
|
||||
if (el.id === 'top') {
|
||||
if (el === '#top' || el.id === 'top') {
|
||||
return 0;
|
||||
}
|
||||
if (el.id === 'bottom') {
|
||||
if (el === '#bottom' || el.id === 'bottom') {
|
||||
return $scrollable[0].scrollHeight - $scrollable[0].clientHeight;
|
||||
}
|
||||
|
||||
@@ -595,7 +604,8 @@ var dom = {
|
||||
options.progress.apply(this, ...arguments);
|
||||
}
|
||||
const newScrollTop = _computeScrollTop();
|
||||
if (Math.abs(newScrollTop - originalScrollTop) <= 1.0 && !(el.classList.contains('o_transitioning'))) {
|
||||
if (Math.abs(newScrollTop - originalScrollTop) <= 1.0
|
||||
&& (isTopOrBottomHidden || !(el.classList.contains('o_transitioning')))) {
|
||||
return;
|
||||
}
|
||||
$scrollable.stop();
|
||||
|
||||
@@ -986,6 +986,19 @@ registry.anchorSlide = publicWidget.Widget.extend({
|
||||
return;
|
||||
}
|
||||
var hash = this.$target[0].hash;
|
||||
if (hash === '#top' || hash === '#bottom') {
|
||||
// If the anchor targets #top or #bottom, directly call the
|
||||
// "scrollTo" function. The reason is that the header or the footer
|
||||
// could have been removed from the DOM. By receiving a string as
|
||||
// parameter, the "scrollTo" function handles the scroll to the top
|
||||
// or to the bottom of the document even if the header or the
|
||||
// footer is removed from the DOM.
|
||||
dom.scrollTo(hash, {
|
||||
duration: 500,
|
||||
extraOffset: this._computeExtraOffset(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!utils.isValidAnchor(hash)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,9 +22,20 @@ function loadAnchors(url) {
|
||||
resolve();
|
||||
}
|
||||
}).then(function (response) {
|
||||
return _.map($(response).find('[id][data-anchor=true]'), function (el) {
|
||||
const anchors = _.map($(response).find('[id][data-anchor=true]'), function (el) {
|
||||
return '#' + el.id;
|
||||
});
|
||||
// Always suggest the top and the bottom of the page as internal link
|
||||
// anchor even if the header and the footer are not in the DOM. Indeed,
|
||||
// the "scrollTo" function handles the scroll towards those elements
|
||||
// even when they are not in the DOM.
|
||||
if (!anchors.includes('#top')) {
|
||||
anchors.unshift('#top');
|
||||
}
|
||||
if (!anchors.includes('#bottom')) {
|
||||
anchors.push('#bottom');
|
||||
}
|
||||
return anchors;
|
||||
}).catch(error => {
|
||||
console.debug(error);
|
||||
return [];
|
||||
|
||||
@@ -422,7 +422,7 @@ font[class*='bg-'] {
|
||||
|
||||
// Probably outdated
|
||||
// Disable fixed height
|
||||
@media (max-width: 400px) {
|
||||
@include media-breakpoint-down(sm) {
|
||||
section,
|
||||
.parallax,
|
||||
.row,
|
||||
|
||||
@@ -23,7 +23,7 @@ const TableOfContent = publicWidget.Widget.extend({
|
||||
*/
|
||||
destroy() {
|
||||
this.$target.css('top', '');
|
||||
this.$target.find('.s_table_of_content_navbar').css('top', '');
|
||||
this.$target.find('.s_table_of_content_navbar').css({top: '', maxHeight: ''});
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
@@ -42,8 +42,10 @@ const TableOfContent = publicWidget.Widget.extend({
|
||||
this.$target.css('top', isHorizontalNavbar ? position : '');
|
||||
this.$target.find('.s_table_of_content_navbar').css('top', isHorizontalNavbar ? '' : position + 20);
|
||||
const $mainNavBar = $('#oe_main_menu_navbar');
|
||||
position += $mainNavBar.length ? $mainNavBar.outerHeight() : 0;
|
||||
const mainNavBarHidden = document.body.classList.contains('o_fullscreen') || this.editableMode;
|
||||
position += !mainNavBarHidden && $mainNavBar.length ? $mainNavBar.outerHeight() : 0;
|
||||
position += isHorizontalNavbar ? this.$target.outerHeight() : 0;
|
||||
this.$target.find('.s_table_of_content_navbar').css('maxHeight', isHorizontalNavbar ? '' : `calc(100vh - ${position + 40}px)`);
|
||||
if (this.previousPosition !== position) {
|
||||
// The scrollSpy must be destroyed before calling it again.
|
||||
// Otherwise the call has no effect.
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
&.s_table_of_content_horizontal_navbar, &.s_table_of_content_vertical_navbar .s_table_of_content_navbar {
|
||||
@include o-position-sticky($top: 0px);
|
||||
}
|
||||
&.s_table_of_content_vertical_navbar .s_table_of_content_navbar {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
&:not(.s_table_of_content_navbar_sticky) {
|
||||
&, .s_table_of_content_navbar {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
overflow-y: auto;
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
@@ -22,12 +21,19 @@
|
||||
font-family: $o-we-sidebar-content-field-input-font-family;
|
||||
}
|
||||
tr {
|
||||
// Since the sortable list's <tr> loses its connection with
|
||||
// the table when dragged, the <td> with the input no longer
|
||||
// takes up the full width, causing a visual issue. To solve
|
||||
// this problem, we added the 'flex' display property.
|
||||
display: flex;
|
||||
border: 1px solid rgba(white, 0.1);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
td {
|
||||
flex-grow: 1;
|
||||
&:first-child, &:last-child {
|
||||
flex-grow: 0;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user