[MIG] stock_valuation_xlsx from v14 to v16

This commit is contained in:
Alexis de Lattre
2024-10-21 14:46:24 +02:00
parent 97cf376e90
commit 02c082f966
9 changed files with 76 additions and 152 deletions

View File

@@ -1,11 +1,11 @@
# Copyright 2020-2021 Akretion France (http://www.akretion.com)
# Copyright 2020-2024 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Stock Valuation XLSX',
'version': '14.0.1.0.0',
'version': '16.0.1.0.0',
'category': 'Tools',
'license': 'AGPL-3',
'summary': 'Generate XLSX reports for past or present stock levels',
@@ -42,8 +42,7 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
'security/ir.model.access.csv',
'wizard/stock_valuation_xlsx_view.xml',
'wizard/stock_variation_xlsx_view.xml',
'views/stock_inventory.xml',
'views/stock_expiry_depreciation_rule.xml',
],
'installable': False,
'installable': True,
}

View File

@@ -1,4 +1,4 @@
# Copyright 2021 Akretion France (http://www.akretion.com/)
# Copyright 2021-2024 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

View File

@@ -1,5 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_expiry_depreciation_rule_full,Full access on stock.expiry.depreciation.rule to account manager,model_stock_expiry_depreciation_rule,account.group_account_manager,1,1,1,1
access_stock_expiry_depreciation_rule_read,Read access on stock.expiry.depreciation.rule to stock manager,model_stock_expiry_depreciation_rule,stock.group_stock_manager,1,0,0,0
access_stock_expiry_depreciation_rule_read,Read access on stock.expiry.depreciation.rule to stock user,model_stock_expiry_depreciation_rule,stock.group_stock_user,1,0,0,0
access_stock_valuation_xlsx,stock.valuation.xlsx wizard,model_stock_valuation_xlsx,stock.group_stock_user,1,1,1,0
access_stock_variation_xlsx,stock.variation.xlsx wizard,model_stock_variation_xlsx,stock.group_stock_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_expiry_depreciation_rule_full Full access on stock.expiry.depreciation.rule to account manager model_stock_expiry_depreciation_rule account.group_account_manager 1 1 1 1
3 access_stock_expiry_depreciation_rule_read Read access on stock.expiry.depreciation.rule to stock manager Read access on stock.expiry.depreciation.rule to stock user model_stock_expiry_depreciation_rule stock.group_stock_manager stock.group_stock_user 1 0 0 0
4 access_stock_valuation_xlsx stock.valuation.xlsx wizard model_stock_valuation_xlsx stock.group_stock_user 1 1 1 0
5 access_stock_variation_xlsx stock.variation.xlsx wizard model_stock_variation_xlsx stock.group_stock_user 1 1 1 0

View File

@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 Akretion France (http://www.akretion.com/)
Copyright 2021-2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<data>
<record id="stock_expiry_depreciation_rule_tree" model="ir.ui.view">
<field name="model">stock.expiry.depreciation.rule</field>
@@ -31,5 +30,4 @@
parent="account.account_management_menu"
sequence="100"/>
</data>
</odoo>

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_inventory_form" model="ir.ui.view">
<field name="name">xlsx.stock.inventory.form</field>
<field name="model">stock.inventory</field>
<field name="inherit_id" ref="stock.view_inventory_form"/>
<field name="arch" type="xml">
<button name="action_validate" position="after">
<button name="%(stock_valuation_xlsx_action)d" type="action"
states="done" string="XLSX Valuation Report"
context="{'default_source': 'inventory', 'default_inventory_id': active_id}"/>
</button>
</field>
</record>
</odoo>

View File

@@ -1,4 +1,4 @@
# Copyright 2020 Akretion France (http://www.akretion.com/)
# Copyright 2020-2024 Akretion France (http://www.akretion.com/)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
@@ -29,33 +29,26 @@ class StockValuationXlsx(models.TransientModel):
'stock.warehouse', string='Warehouse', check_company=True,
domain="[('company_id', '=', company_id)]")
location_id = fields.Many2one(
'stock.location', string='Root Stock Location', required=True,
domain="[('usage', 'in', ('view', 'internal')), ('company_id', '=', company_id)]",
default=lambda self: self._default_location(), check_company=True,
'stock.location', string='Root Stock Location', required=True, check_company=True,
compute='_compute_location_id', readonly=False, precompute=True, store=True,
domain="[('usage', 'in', ('view', 'internal')), ('company_id', 'in', [False, company_id])]",
help="The childen locations of the selected locations will "
"be taken in the valuation.")
categ_ids = fields.Many2many(
'product.category', string='Product Category Filter',
help="Leave this field empty to have a stock valuation for all your products.",
)
source = fields.Selection([
('inventory', 'Physical Inventory'),
('stock', 'Stock Levels'),
], string='Source data', default='stock', required=True)
inventory_id = fields.Many2one(
'stock.inventory', string='Inventory', check_company=True,
domain="[('state', '=', 'done'), ('company_id', '=', company_id)]")
stock_date_type = fields.Selection([
('present', 'Present'),
('past', 'Past'),
], string='Present or Past', default='present')
], string='Present or Past', default='present', required=True)
past_date = fields.Datetime(
string='Past Date', default=fields.Datetime.now)
categ_subtotal = fields.Boolean(
string='Subtotals per Categories', default=True,
help="Show a subtotal per product category.")
standard_price_date = fields.Selection([
('past', 'Past Date or Inventory Date'),
('past', 'Past Date'),
('present', 'Current'),
], default='past', string='Cost Price Date')
has_expiry_date = fields.Boolean(
@@ -67,38 +60,28 @@ class StockValuationXlsx(models.TransientModel):
@api.model
def _default_has_expiry_date(self):
splo = self.env['stock.production.lot']
has_expiry_date = False
if hasattr(splo, 'expiry_date'):
if hasattr(self.env['stock.lot'], 'expiry_date'):
has_expiry_date = True
return has_expiry_date
@api.model
def _default_location(self):
wh = self.env.ref('stock.warehouse0')
return wh.lot_stock_id
@api.depends('warehouse_id', 'company_id')
def _compute_location_id(self):
for wiz in self:
wh = wiz.warehouse_id
if not wh:
wh = self.env["stock.warehouse"].search([('company_id', '=', wiz.company_id.id)], limit=1)
if wh:
wiz.location_id = wh.view_location_id.id
@api.onchange('warehouse_id')
def warehouse_id_change(self):
if self.warehouse_id:
self.location_id = self.warehouse_id.view_location_id.id
def _check_config(self, company_id):
def _check_config(self):
self.ensure_one()
if (
self.source == 'stock' and
self.stock_date_type == 'past' and
self.past_date > fields.Datetime.now()):
raise UserError(_("The 'Past Date' must be in the past !"))
if self.source == 'inventory':
if not self.inventory_id:
raise UserError(_("You must select an inventory."))
elif self.inventory_id.state != 'done':
raise UserError(_(
"The selected inventory (%s) is not in done state.")
% self.inventory_id.display_name)
cost_method_real_count = self.env['ir.property'].sudo().search([
('company_id', '=', company_id),
('company_id', '=', self.company_id.id),
('name', '=', 'property_cost_method'),
('value_text', '=', 'real'),
('type', '=', 'selection'),
@@ -159,7 +142,7 @@ class StockValuationXlsx(models.TransientModel):
if not std_price_date: # present
product_id2data[p['id']][std_price_field_name] = p['standard_price']
else:
layer_rg = svlo.read_group(
layer_rg = svlo._read_group(
[
('product_id', '=', p['id']),
('company_id', '=', company_id),
@@ -201,13 +184,13 @@ class StockValuationXlsx(models.TransientModel):
@api.model
def prodlot_id2data(self, product_ids, has_expiry_date, depreciation_rules):
splo = self.env['stock.production.lot']
slo = self.env['stock.lot']
lot_id2data = {}
lot_fields = ['name']
if has_expiry_date:
lot_fields.append('expiry_date')
lots = splo.search_read(
lots = slo.search_read(
[('product_id', 'in', product_ids)], lot_fields)
for lot in lots:
lot_id2data[lot['id']] = lot
@@ -230,30 +213,6 @@ class StockValuationXlsx(models.TransientModel):
loc_id2name[loc['id']] = loc['display_name']
return loc_id2name
def compute_data_from_inventory(self, product_ids, prec_qty):
self.ensure_one()
logger.debug('Start compute_data_from_inventory')
# Can he modify UoM ?
inv_lines = self.env['stock.inventory.line'].search_read([
('inventory_id', '=', self.inventory_id.id),
('location_id', 'child_of', self.location_id.id),
('product_id', 'in', product_ids),
('product_qty', '>', 0),
], ['product_id', 'location_id', 'prod_lot_id', 'product_qty'])
res = []
in_stock_products = {}
for l in inv_lines:
if not float_is_zero(l['product_qty'], precision_digits=prec_qty):
res.append({
'product_id': l['product_id'][0],
'lot_id': l['prod_lot_id'] and l['prod_lot_id'][0] or False,
'qty': l['product_qty'],
'location_id': l['location_id'][0],
})
in_stock_products[l['product_id'][0]] = True
logger.debug('End compute_data_from_inventory')
return res, in_stock_products
def compute_data_from_present_stock(self, company_id, product_ids, prec_qty):
self.ensure_one()
logger.debug('Start compute_data_from_present_stock')
@@ -361,40 +320,34 @@ class StockValuationXlsx(models.TransientModel):
company = self.company_id
company_id = company.id
prec_cur_rounding = company.currency_id.rounding
self._check_config(company_id)
self._check_config()
apply_depreciation = self.apply_depreciation
if (
(self.source == 'stock' and self.stock_date_type == 'past') or
self.stock_date_type == 'past' or
not self.split_by_lot or
not self.has_expiry_date):
apply_depreciation = False
else:
apply_depreciation = self.apply_depreciation
product_ids = self.get_product_ids()
if not product_ids:
raise UserError(_("There are no products to analyse."))
split_by_lot = self.split_by_lot
split_by_location = self.split_by_location
if self.source == 'stock':
if self.stock_date_type == 'present':
past_date = False
data, in_stock_products = self.compute_data_from_present_stock(
company_id, product_ids, prec_qty)
elif self.stock_date_type == 'past':
split_by_lot = False
split_by_location = False
past_date = self.past_date
data, in_stock_products = self.compute_data_from_past_stock(
product_ids, prec_qty, past_date)
elif self.source == 'inventory':
past_date = self.inventory_id.date
data, in_stock_products = self.compute_data_from_inventory(product_ids, prec_qty)
if self.source == 'stock' and self.stock_date_type == 'present':
if self.stock_date_type == 'present':
split_by_lot = self.split_by_lot
split_by_location = self.split_by_location
past_date = False
standard_price_past_date = False
else: # field standard_price_date is shown on screen
if self.standard_price_date == 'present':
standard_price_past_date = False
else:
standard_price_past_date = past_date
data, in_stock_products = self.compute_data_from_present_stock(
company_id, product_ids, prec_qty)
elif self.stock_date_type == 'past':
split_by_lot = False
split_by_location = False
past_date = self.past_date
standard_price_past_date = past_date
data, in_stock_products = self.compute_data_from_past_stock(
product_ids, prec_qty, past_date)
else:
raise
depreciation_rules = []
if apply_depreciation:
depreciation_rules = self._prepare_expiry_depreciation_rules(company_id, past_date)

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 Akretion France (http://www.akretion.com/)
Copyright 2020-2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
@@ -12,25 +12,24 @@
<field name="name">stock.valuation.xlsx.form</field>
<field name="model">stock.valuation.xlsx</field>
<field name="arch" type="xml">
<form string="Stock valuation XLSX">
<form>
<div name="help">
<p>The generated XLSX report has the valuation of stockable products located on the selected stock locations (and their childrens).</p>
</div>
<group name="setup">
<field name="company_id" groups="base.group_multi_company"/>
<field name="company_id" invisible="1"/>
<field name="categ_ids" widget="many2many_tags"/>
<field name="warehouse_id"/>
<field name="location_id"/>
<field name="source" widget="radio"/>
<field name="inventory_id" attrs="{'invisible': [('source', '!=', 'inventory')], 'required': [('source', '=', 'inventory')]}"/>
<field name="stock_date_type" attrs="{'invisible': [('source', '!=', 'stock')], 'required': [('source', '=', 'stock')]}" widget="radio"/>
<field name="past_date" attrs="{'invisible': ['|', ('source', '!=', 'stock'), ('stock_date_type', '!=', 'past')], 'required': [('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}"/>
<field name="standard_price_date" attrs="{'invisible': [('source', '=', 'stock'), ('stock_date_type', '=', 'present')]}" widget="radio"/>
<field name="stock_date_type" />
<field name="past_date" attrs="{'invisible': [('stock_date_type', '!=', 'past')], 'required': [('stock_date_type', '=', 'past')]}"/>
<field name="standard_price_date" attrs="{'invisible': [('stock_date_type', '=', 'present')]}" widget="radio"/>
<field name="categ_subtotal" />
<field name="has_expiry_date" invisible="1"/>
<field name="split_by_lot" attrs="{'invisible': [('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}" groups="stock.group_production_lot"/>
<field name="split_by_location" attrs="{'invisible': [('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}"/>
<field name="apply_depreciation" groups="stock.group_production_lot" attrs="{'invisible': ['|', '|', ('split_by_lot', '=', False), ('has_expiry_date', '=', False), '&amp;', ('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}"/>
<field name="split_by_lot" attrs="{'invisible': [('stock_date_type', '=', 'past')]}" groups="stock.group_production_lot"/>
<field name="split_by_location" attrs="{'invisible': [('stock_date_type', '=', 'past')]}"/>
<field name="apply_depreciation" groups="stock.group_production_lot" attrs="{'invisible': ['|', '|', ('split_by_lot', '=', False), ('has_expiry_date', '=', False), ('stock_date_type', '=', 'past')]}"/>
</group>
<footer>
<button name="generate" type="object" class="btn-primary" string="Generate"/>

View File

@@ -1,4 +1,4 @@
# Copyright 2020-2021 Akretion France (http://www.akretion.com/)
# Copyright 2020-2024 Akretion France (http://www.akretion.com/)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
@@ -27,9 +27,9 @@ class StockVariationXlsx(models.TransientModel):
'stock.warehouse', string='Warehouse', check_company=True,
domain="[('company_id', '=', company_id)]")
location_id = fields.Many2one(
'stock.location', string='Root Stock Location', required=True,
domain="[('usage', 'in', ('view', 'internal')), ('company_id', '=', company_id)]",
default=lambda self: self._default_location(), check_company=True,
'stock.location', string='Root Stock Location', required=True, check_company=True,
compute='_compute_location_id', readonly=False, precompute=True, store=True,
domain="[('usage', 'in', ('view', 'internal')), ('company_id', 'in', [False, company_id])]",
help="The childen locations of the selected locations will "
"be taken in the valuation.")
categ_ids = fields.Many2many(
@@ -56,17 +56,16 @@ class StockVariationXlsx(models.TransientModel):
string='Subtotals per Categories', default=True,
help="Show a subtotal per product category.")
@api.model
def _default_location(self):
wh = self.env.ref('stock.warehouse0')
return wh.lot_stock_id
@api.depends('warehouse_id', 'company_id')
def _compute_location_id(self):
for wiz in self:
wh = wiz.warehouse_id
if not wh:
wh = self.env["stock.warehouse"].search([('company_id', '=', wiz.company_id.id)], limit=1)
if wh:
wiz.location_id = wh.view_location_id.id
@api.onchange('warehouse_id')
def warehouse_id_change(self):
if self.warehouse_id:
self.location_id = self.warehouse_id.view_location_id.id
def _check_config(self, company_id):
def _check_config(self):
self.ensure_one()
present = fields.Datetime.now()
if self.end_date_type == 'past':
@@ -80,7 +79,7 @@ class StockVariationXlsx(models.TransientModel):
if self.start_date >= present:
raise UserError(_("The start date must be in the past."))
cost_method_real_count = self.env['ir.property'].sudo().search([
('company_id', '=', company_id),
('company_id', '=', self.company_id.id),
('name', '=', 'property_cost_method'),
('value_text', '=', 'real'),
('type', '=', 'selection'),
@@ -119,21 +118,21 @@ class StockVariationXlsx(models.TransientModel):
domain_quant = [('product_id', 'in', product_ids)] + domain_quant_loc
domain_move_in = [('product_id', 'in', product_ids), ('state', '=', 'done')] + domain_move_in_loc
domain_move_out = [('product_id', 'in', product_ids), ('state', '=', 'done')] + domain_move_out_loc
quants_res = dict((item['product_id'][0], item['quantity']) for item in sqo.read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id'))
quants_res = dict((item['product_id'][0], item['quantity']) for item in sqo._read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id'))
domain_move_in_start_to_end = [('date', '>', start_date)] + domain_move_in
domain_move_out_start_to_end = [('date', '>', start_date)] + domain_move_out
if end_date_type == 'past':
domain_move_in_end_to_present = [('date', '>', end_date)] + domain_move_in
domain_move_out_end_to_present = [('date', '>', end_date)] + domain_move_out
moves_in_res_end_to_present = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_in_end_to_present, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res_end_to_present = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_out_end_to_present, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_in_res_end_to_present = dict((item['product_id'][0], item['product_qty']) for item in smo._read_group(domain_move_in_end_to_present, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res_end_to_present = dict((item['product_id'][0], item['product_qty']) for item in smo._read_group(domain_move_out_end_to_present, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
domain_move_in_start_to_end += [('date', '<', end_date)]
domain_move_out_start_to_end += [('date', '<', end_date)]
moves_in_res_start_to_end = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_in_start_to_end, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res_start_to_end = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_out_start_to_end, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_in_res_start_to_end = dict((item['product_id'][0], item['product_qty']) for item in smo._read_group(domain_move_in_start_to_end, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res_start_to_end = dict((item['product_id'][0], item['product_qty']) for item in smo._read_group(domain_move_out_start_to_end, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
product_data = {} # key = product_id , value = dict
for product in ppo.browse(product_ids):
@@ -208,7 +207,7 @@ class StockVariationXlsx(models.TransientModel):
company = self.company_id
company_id = company.id
prec_cur_rounding = company.currency_id.rounding
self._check_config(company_id)
self._check_config()
product_ids = self.get_product_ids()
if not product_ids:

View File

@@ -12,12 +12,13 @@
<field name="name">stock.variation.xlsx.form</field>
<field name="model">stock.variation.xlsx</field>
<field name="arch" type="xml">
<form string="Stock variation XLSX">
<form>
<div name="help">
<p>The generated XLSX report has the valuation of stockable products located on the selected stock locations (and their childrens).</p>
</div>
<group name="setup">
<field name="company_id" groups="base.group_multi_company"/>
<field name="company_id" invisible="1"/>
<field name="categ_ids" widget="many2many_tags"/>
<field name="warehouse_id"/>
<field name="location_id"/>