19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:07 +01:00
parent ba20ce7443
commit 768b70e05e
2357 changed files with 1057103 additions and 712486 deletions

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import stock_avco_audit_report
from . import stock_forecasted
from . import stock_valuation_report

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="filter_invoice_inventory_valuation" model="ir.filters">
<field name="name">Inventory Valuation</field>
<field name="model_id">account.invoice.report</field>
<field name="domain">[('product_id.is_storable', '=', True)]</field>
<field name="user_ids" eval="False"/>
<field name="context">{'group_by': ['product_id'], 'pivot_column_groupby': ['invoice_date:month'], 'pivot_measures': ['inventory_value'], 'graph_measure': 'inventory_value'}</field>
</record>
</odoo>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="stock_account_report_product_product_replenishment" inherit_id="stock.report_replenishment_header">
</template>
</odoo>

View file

@ -0,0 +1,135 @@
from odoo import fields, models, tools
from odoo.tools import float_is_zero
class StockAverageCostReport(models.AbstractModel):
_auto = False
_name = 'stock.avco.report'
_description = 'Stock AVCO Justifier'
_order = 'date desc, id desc'
date = fields.Date(string='Date', required=True)
user_id = fields.Many2one('res.users', string='User', required=True)
company_id = fields.Many2one('res.company', string='Company', required=True)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id', string='Currency')
product_id = fields.Many2one('product.product', string='Product', required=True)
reference = fields.Char(string='Reference', required=True)
description = fields.Text(string='Description', required=True)
res_model_name = fields.Selection([
('stock.move', 'Stock Move'),
('product.value', 'Product Value'),
], string='Resource Model Name', required=True)
quantity = fields.Float(string='Added Quantity', required=True)
value = fields.Float(string='Value', required=True)
added_value = fields.Float(string='Added Value', compute='_compute_cumulative_fields')
total_quantity = fields.Float(string='Total Quantity', compute='_compute_cumulative_fields')
total_value = fields.Float(string='Total Value', compute='_compute_cumulative_fields')
avco_value = fields.Float(string='AVCO Value', compute='_compute_cumulative_fields')
justification = fields.Text(string='Justification', compute='_compute_justification')
def init(self):
tools.drop_view_if_exists(self.env.cr, 'stock_avco_report')
query = """
CREATE OR REPLACE VIEW stock_avco_report AS (
SELECT
sm.id AS id,
sm.product_id,
sm.date,
picking.user_id,
sm.company_id,
sm.reference,
CASE WHEN sm.is_in THEN sm.value ELSE -sm.value END AS value,
CASE WHEN sm.is_in THEN sm.quantity ELSE -sm.quantity END AS quantity,
'stock.move' AS res_model_name,
'Operation' AS description
FROM
stock_move sm
LEFT JOIN
stock_picking picking ON sm.picking_id = picking.id
LEFT JOIN
product_product pp ON sm.product_id = pp.id
LEFT JOIN
product_template pt ON pp.product_tmpl_id = pt.id
LEFT JOIN
product_category pc ON pt.categ_id = pc.id
LEFT JOIN
res_company company ON sm.company_id = company.id
WHERE
sm.state = 'done'
AND (sm.is_in = TRUE OR sm.is_out = TRUE)
-- Ignore moves for standard cost method. Only display the list of cost updates
AND (
(pt.categ_id IS NOT NULL AND pc.property_cost_method ->> company.id::text IN ('fifo', 'average'))
OR (pt.categ_id IS NULL OR (pc.property_cost_method IS NULL OR pc.property_cost_method ->> company.id::text IS NULL) AND company.cost_method IN ('fifo', 'average'))
)
UNION ALL
SELECT
-pv.id,
pv.product_id,
pv.date,
pv.user_id,
pv.company_id,
'Adjustment' AS reference, -- Set a fixed string for the reference
pv.value,
0 AS quantity, -- Set quantity to 0 as requested,
'product.value' AS res_model_name,
pv.description
FROM
product_value pv
WHERE
pv.move_id IS NULL
);
"""
self.env.cr.execute(query)
def _compute_cumulative_fields(self):
total_records_grouped = self.env['stock.avco.report'].search(
[('product_id', 'in', self.product_id.mapped('id')), ('company_id', 'in', self.company_id.mapped('id'))]
).grouped(lambda m: (m.product_id, m.company_id))
for records in self.grouped(lambda m: (m.product_id, m.company_id)).values():
current_page_records = records.sorted('date, id')
total_records = total_records_grouped.get((records.product_id, records.company_id)).sorted('date, id')
added_value = 0.0
total_value = 0.0
total_quantity = 0.0
avco = 0.0
for record in total_records:
in_qty = record.quantity
in_value = record.value
if record.res_model_name == 'stock.move':
previous_qty = total_quantity
total_quantity += in_qty
# Regular case, value from accumulation
if previous_qty > 0:
total_value += in_value
avco = total_value / total_quantity if not float_is_zero(total_quantity, precision_digits=self.env['decimal.precision'].precision_get('Product Unit')) else avco
# From negative quantity case, value from last_in
elif previous_qty <= 0:
avco = in_value / in_qty if in_qty else avco
total_value = avco * total_quantity
added_value = avco * in_qty
elif record.res_model_name == 'product.value':
avco = in_value
added_value = (avco * total_quantity) - total_value
total_value = avco * total_quantity
if record in current_page_records:
record.added_value = added_value
record.total_value = total_value
record.total_quantity = total_quantity
record.avco_value = avco
def _compute_justification(self):
self.justification = False
for record in self:
if record.res_model_name == 'stock.move':
record.justification = self.env['stock.move'].browse(record.id).value_justification

View file

@ -0,0 +1,29 @@
<odoo>
<record id="stock_avco_report_view_list" model="ir.ui.view">
<field name="name">stock.avco.report.view.list</field>
<field name="model">stock.avco.report</field>
<field name="arch" type="xml">
<list>
<field name="currency_id" column_invisible="1"/>
<field name="reference"/>
<field name="product_id"/>
<field name="date"/>
<field name="description"/>
<field name="quantity" column_invisible="context.get('cost_method') == 'standard'"/>
<field name="added_value" widget="monetary" options="{'currency_field': 'currency_id'}" column_invisible="context.get('cost_method') == 'standard'"/>
<field name="value" string="Unit Cost" widget="monetary" options="{'currency_field': 'currency_id'}" column_invisible="context.get('cost_method') != 'standard'"/>
<field name="total_value" widget="monetary" options="{'currency_field': 'currency_id'}" column_invisible="context.get('cost_method') == 'standard'"/>
<field name="total_quantity" column_invisible="context.get('cost_method') == 'standard'"/>
<field name="avco_value" string="Unit Cost" widget="monetary" options="{'currency_field': 'currency_id'}" column_invisible="context.get('cost_method') == 'standard'"/>
<field name="justification" column_invisible="context.get('cost_method') != 'average'" optional="hide"/>
</list>
</field>
</record>
<record id="stock_avco_report_action" model="ir.actions.act_window">
<field name="name">Unit Cost History</field>
<field name="res_model">stock.avco.report</field>
<field name="view_mode">list</field>
<field name="domain">[("product_id", "=", active_id)]</field>
</record>
</odoo>

View file

@ -2,20 +2,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.tools.float_utils import float_is_zero, float_repr
from odoo.tools.float_utils import float_repr
class ReplenishmentReport(models.AbstractModel):
_inherit = 'report.stock.report_product_product_replenishment'
class StockForecasted_Product_Product(models.AbstractModel):
_inherit = 'stock.forecasted_product_product'
def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids):
def _get_report_header(self, product_template_ids, product_ids, wh_location_ids):
""" Overrides to computes the valuations of the stock. """
res = super()._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids)
if not self.user_has_groups('stock.group_stock_manager'):
res = super()._get_report_header(product_template_ids, product_ids, wh_location_ids)
if not self.env.user.has_group('stock.group_stock_manager') or not wh_location_ids:
return res
domain = self._product_domain(product_template_ids, product_variant_ids)
company = self.env['stock.location'].browse(wh_location_ids[0]).company_id
svl = self.env['stock.valuation.layer'].search(domain + [('company_id', '=', company.id)])
domain_quants = [
('company_id', '=', company.id),
('location_id', 'in', wh_location_ids)
@ -23,15 +21,11 @@ class ReplenishmentReport(models.AbstractModel):
if product_template_ids:
domain_quants += [('product_id.product_tmpl_id', 'in', product_template_ids)]
else:
domain_quants += [('product_id', 'in', product_variant_ids)]
domain_quants += [('product_id', 'in', product_ids)]
quants = self.env['stock.quant'].search(domain_quants)
currency = svl.currency_id or self.env.company.currency_id
total_quantity = sum(svl.mapped('quantity'))
# Because we can have negative quantities, `total_quantity` may be equal to zero even if the warehouse's `quantity` is positive.
if svl and not float_is_zero(total_quantity, precision_rounding=svl.product_id.uom_id.rounding):
value = sum(svl.mapped('value')) * (sum(quants.mapped('quantity')) / total_quantity)
else:
value = 0
currency = self.env.company.currency_id
value = sum(quants.mapped('value'))
value = float_repr(value, precision_digits=currency.decimal_places)
if currency.position == 'after':
value = '%s %s' % (value, currency.symbol)

View file

@ -0,0 +1,152 @@
from collections import defaultdict
from odoo import _, api, fields, models
class StockValuationReport(models.AbstractModel):
_name = 'stock_account.stock.valuation.report'
_description = 'Stock Valuation'
@api.model
def get_report_values(self, date=False):
return {
'data': self.with_context(allowed_company_ids=self.env.company.ids)._get_report_data(date=date),
'context': {},
}
@api.model
def _get_report_values(self, docids, data=None):
docs = []
doc = self._get_report_data()
docs.append(self._include_pdf_specifics(doc, data))
return {
'doc_ids': docids,
'doc_model': 'stock.valuation.report',
'docs': docs,
}
def _get_report_data(self, date=False, product_category=False, warehouse=False):
company = self.env.company
# Check if date is a string instance
if isinstance(date, str):
date = fields.Date.from_string(date)
if date == fields.Date.today():
date = False
if not date:
inventory_data = company.stock_value()
accounting_data = company.stock_accounting_value()
else:
inventory_data = company.stock_value(at_date=date)
accounting_data = company.stock_accounting_value(at_date=date)
accounts = inventory_data.keys() | accounting_data.keys()
account_ids = {acc.id for acc in accounts}
initial_balance = {
'label': _("Initial Balance"),
'value': 0,
'lines_by_account_id': defaultdict(lambda: {
'value': 0,
'accounts': [],
}),
}
ending_stock = {
'label': _("Ending Stock"),
'value': 0,
'lines_by_account_id': defaultdict(lambda: {
'value': 0,
'accounts': [],
}),
}
# Compute Opening Balance values and Ending Stock values.
for account in accounts:
opening_balance = accounting_data.get(account, 0)
ending_balance = inventory_data.get(account, 0)
account_ids.add(account.id)
if opening_balance:
initial_balance['value'] += opening_balance
initial_balance['lines_by_account_id'][account.id]['value'] += opening_balance
if ending_balance:
ending_stock['value'] += ending_balance
ending_stock['lines_by_account_id'][account.id]['value'] += ending_balance
# Get accounting data.
accounts_by_product = company._get_accounts_by_product()
location_valuation_vals = company._get_location_valuation_vals(
date, location_domain=[('usage', '=', 'inventory')],
)
stock_valuation_account_vals = company.with_context(inventory_data=inventory_data)._get_stock_valuation_account_vals(
accounts_by_product, date, company._get_location_valuation_vals(date))
report_data = {
'company_id': company.id,
'currency_id': company.currency_id.id,
'ending_stock': ending_stock,
'initial_balance': initial_balance,
}
if self._must_include_inventory_loss():
# Compute Inventory Loss values.
inventory_loss = {
'label': _("Inventory Loss"),
'value': 0,
}
lines_by_account_id = defaultdict(lambda: {
'debit': 0,
'credit': 0,
})
for vals in location_valuation_vals:
account_ids.add(vals['account_id'])
inventory_loss['value'] -= vals['debit']
lines_by_account_id[vals['account_id']]['debit'] += vals['debit']
lines_by_account_id[vals['account_id']]['credit'] += vals['credit']
inventory_loss['lines'] = [{
'account_id': account_id,
'debit': vals['debit'],
'credit': vals['credit'],
} for (account_id, vals) in lines_by_account_id.items()]
report_data['inventory_loss'] = inventory_loss
# Compute Stock Variation values.
stock_variation = {
'label': _("Stock Variation"),
'value': 0,
}
lines_by_account_id = defaultdict(lambda: {
'debit': 0,
'credit': 0,
'lines': [],
})
for vals in stock_valuation_account_vals:
account_ids.add(vals['account_id'])
stock_variation['value'] += vals['debit']
lines_by_account_id[vals['account_id']]['debit'] += vals['debit']
lines_by_account_id[vals['account_id']]['credit'] += vals['credit']
stock_variation['lines'] = [{
'account_id': account_id,
'debit': vals['debit'],
'credit': vals['credit'],
} for (account_id, vals) in lines_by_account_id.items()]
accounts_read_data = self.env['account.account'].search_read(
[('id', 'in', account_ids)],
['id', 'name', 'code', 'display_name']
)
report_data.update(
accounts_by_id={acc_data['id']: acc_data for acc_data in accounts_read_data},
stock_variation=stock_variation,
)
return report_data
def action_print_as_pdf(self):
return
def action_print_as_xlsx(self):
return
def _must_include_inventory_loss(self):
return bool(self.env['stock.location'].search_count([
('usage', '=', 'inventory'),
('valuation_account_id', '!=', False),
], limit=1))

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_report_stock_valuation" model="ir.actions.client">
<field name="name">Inventory Valuation</field>
<field name="tag">stock_valuation_report</field>
<field name="res_model">stock_account.stock.valuation.report</field>
<field name="path">stock-valuation-closing</field>
<field name="context">{'model': 'stock_account.stock.valuation.report'}</field>
</record>
</odoo>