mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-26 21:02:01 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue