oca-ocb-accounting/odoo-bringout-oca-ocb-stock_account/stock_account/models/res_company.py
Ernad Husremovic 768b70e05e 19.0 vanilla
2026-03-09 09:30:07 +01:00

360 lines
15 KiB
Python

from collections import defaultdict
from dateutil.relativedelta import relativedelta
from odoo import Command, _, api, fields, models
from odoo.fields import Domain
from odoo.exceptions import UserError
class ResCompany(models.Model):
_inherit = "res.company"
account_stock_journal_id = fields.Many2one('account.journal', string='Stock Journal', check_company=True)
account_stock_valuation_id = fields.Many2one('account.account', string='Stock Valuation Account', check_company=True)
account_production_wip_account_id = fields.Many2one('account.account', string='Production WIP Account', check_company=True)
account_production_wip_overhead_account_id = fields.Many2one('account.account', string='Production WIP Overhead Account', check_company=True)
inventory_period = fields.Selection(
string='Inventory Period',
selection=[
('manual', 'Manual'),
('daily', 'Daily'),
('monthly', 'Monthly'),
],
default='manual',
required=True)
inventory_valuation = fields.Selection(
string='Valuation',
selection=[
('periodic', 'Periodic (at closing)'),
('real_time', 'Perpetual (at invoicing)'),
],
default='periodic',
)
cost_method = fields.Selection(
string="Cost Method",
selection=[
('standard', "Standard Price"),
('fifo', "First In First Out (FIFO)"),
('average', "Average Cost (AVCO)"),
],
default='standard',
required=True,
)
def action_close_stock_valuation(self, at_date=None, auto_post=False):
self.ensure_one()
if at_date and isinstance(at_date, str):
at_date = fields.Date.from_string(at_date)
last_closing_date = self._get_last_closing_date()
if at_date and last_closing_date and at_date < fields.Date.to_date(last_closing_date):
raise UserError(self.env._('It exists closing entries after the selected date. Cancel them before generate an entry prior to them'))
aml_vals_list = self._action_close_stock_valuation(at_date=at_date)
if not aml_vals_list:
# No account moves to create, so nothing to display.
raise UserError(_("Everything is correctly closed"))
if not self.account_stock_journal_id:
raise UserError(self.env._("Please set the Journal for Inventory Valuation in the settings."))
if not self.account_stock_valuation_id:
raise UserError(self.env._("Please set the Valuation Account for Inventory Valuation in the settings."))
moves_vals = {
'journal_id': self.account_stock_journal_id.id,
'date': at_date or fields.Date.today(),
'ref': _('Stock Closing'),
'line_ids': [Command.create(aml_vals) for aml_vals in aml_vals_list],
}
account_move = self.env['account.move'].create(moves_vals)
self._save_closing_id(account_move.id)
if auto_post:
account_move._post()
return {
'type': 'ir.actions.act_window',
'name': _("Journal Items"),
'res_model': 'account.move',
'res_id': account_move.id,
'views': [(False, 'form')],
}
def stock_value(self, accounts_by_product=None, at_date=None):
self.ensure_one()
value_by_account: dict = defaultdict(float)
if not accounts_by_product:
accounts_by_product = self._get_accounts_by_product()
for product, accounts in accounts_by_product.items():
account = accounts['valuation']
product_value = product.with_context(to_date=at_date).total_value
value_by_account[account] += product_value
return value_by_account
def stock_accounting_value(self, accounts_by_product=None, at_date=None):
self.ensure_one()
if not accounts_by_product:
accounts_by_product = self._get_accounts_by_product()
account_data = defaultdict(float)
stock_valuation_accounts_ids = set()
for dummy, accounts in accounts_by_product.items():
stock_valuation_accounts_ids.add(accounts['valuation'].id)
stock_valuation_accounts = self.env['account.account'].browse(stock_valuation_accounts_ids)
domain = Domain([
('account_id', 'in', stock_valuation_accounts.ids),
('company_id', '=', self.id),
('parent_state', '=', 'posted'),
])
if at_date:
domain = domain & Domain([('date', '<=', at_date)])
amls_group = self.env['account.move.line']._read_group(domain, ['account_id'], ['balance:sum'])
for account, balance in amls_group:
account_data[account] += balance
return account_data
def _action_close_stock_valuation(self, at_date=None):
aml_vals_list = []
accounts_by_product = self._get_accounts_by_product()
vals_list = self._get_location_valuation_vals(at_date)
if vals_list:
# Needed directly since it will impact the accounting stock valuation.
aml_vals_list += vals_list
vals_list = self._get_stock_valuation_account_vals(accounts_by_product, at_date, aml_vals_list)
if vals_list:
aml_vals_list += vals_list
vals_list = self._get_continental_realtime_variation_vals(accounts_by_product, at_date, aml_vals_list)
if vals_list:
aml_vals_list += vals_list
return aml_vals_list
@api.model
def _cron_post_stock_valuation(self):
domain = Domain([('inventory_period', '=', 'daily'), ('inventory_valuation', '!=', 'real_time')])
if fields.Date.today() == fields.Date.today() + relativedelta(day=31):
domain = domain & Domain([('inventory_period', '=', 'monthly')])
companies = self.env['res.company'].search(domain)
for company in companies:
company.action_close_stock_valuation(auto_post=True)
def _get_accounts_by_product(self, products=None):
if not products:
products = self.env['product.product'].with_company(self).search([('is_storable', '=', True)])
accounts_by_product = {}
for product in products:
accounts = product._get_product_accounts()
accounts_by_product[product] = {
'valuation': accounts['stock_valuation'],
'variation': accounts['stock_variation'],
'expense': accounts['expense'],
}
return accounts_by_product
@api.model
def _get_extra_balance(self, vals_list=None):
extra_balance = defaultdict(float)
if not vals_list:
return extra_balance
for vals in vals_list:
extra_balance[vals['account_id']] += (vals['debit'] - vals['credit'])
return extra_balance
def _get_location_valuation_vals(self, at_date=None, location_domain=False):
location_domain = Domain.AND([
location_domain or [],
[('valuation_account_id', '!=', False)],
[('company_id', '=', self.id)],
])
amls_vals_list = []
valued_location = self.env['stock.location'].search(location_domain)
last_closing_date = self._get_last_closing_date()
moves_base_domain = Domain([
('product_id.is_storable', '=', True),
('product_id.valuation', '=', 'periodic')
])
if last_closing_date:
moves_base_domain &= Domain([('date', '>', last_closing_date)])
if at_date:
moves_base_domain &= Domain([('date', '<=', at_date)])
moves_in_domain = Domain([
('is_out', '=', True),
('company_id', '=', self.id),
('location_dest_id', 'in', valued_location.ids),
]) & moves_base_domain
moves_in_by_location = self.env['stock.move']._read_group(
moves_in_domain,
['location_dest_id', 'product_category_id'],
['value:sum'],
)
moves_out_domain = Domain([
('is_in', '=', True),
('company_id', '=', self.id),
('location_id', 'in', valued_location.ids),
]) & moves_base_domain
moves_out_by_location = self.env['stock.move']._read_group(
moves_out_domain,
['location_id', 'product_category_id'],
['value:sum'],
)
account_balance = defaultdict(float)
for location, category, value in moves_in_by_location:
stock_valuation_acc = category.property_stock_valuation_account_id or self.account_stock_valuation_id
account_balance[location.valuation_account_id, stock_valuation_acc] += value
for location, category, value in moves_out_by_location:
stock_valuation_acc = category.property_stock_valuation_account_id or self.account_stock_valuation_id
account_balance[location.valuation_account_id, stock_valuation_acc] -= value
for (location_account, stock_account), balance in account_balance.items():
if balance == 0:
continue
amls_vals = self._prepare_inventory_aml_vals(
location_account,
stock_account,
balance,
_('Closing: Location Reclassification - [%(account)s]', account=location_account.display_name),
)
amls_vals_list += amls_vals
return amls_vals_list
def _get_stock_valuation_account_vals(self, accounts_by_product, at_date=None, extra_aml_vals_list=None):
amls_vals_list = []
if not accounts_by_product:
return amls_vals_list
extra_balance = self._get_extra_balance(extra_aml_vals_list)
if 'inventory_data' in self.env.context:
inventory_data = self.env.context.get('inventory_data')
else:
inventory_data = self.stock_value(accounts_by_product, at_date)
accounting_data = self.stock_accounting_value(accounts_by_product, at_date)
accounts = inventory_data.keys() | accounting_data.keys()
for account in accounts:
account_variation = account.account_stock_variation_id
if not account_variation:
account_variation = self.expense_account_id
if not account_variation:
continue
balance = inventory_data.get(account, 0) - accounting_data.get(account, 0)
balance -= extra_balance.get(account.id, 0)
if self.currency_id.is_zero(balance):
continue
amls_vals = self._prepare_inventory_aml_vals(
account,
account_variation,
balance,
_('Closing: Stock Variation Global for company [%(company)s]', company=self.display_name),
)
amls_vals_list += amls_vals
return amls_vals_list
def _get_continental_realtime_variation_vals(self, accounts_by_product, at_date=None, extra_aml_vals_list=None):
""" In continental perpetual the inventory variation is never posted.
This method compute the variation for a period and post it.
"""
extra_balance = self._get_extra_balance(extra_aml_vals_list)
fiscal_year_date_from = self.compute_fiscalyear_dates(fields.Date.today())['date_from']
amls_vals_list = []
accounting_data_today = self.stock_accounting_value(accounts_by_product)
accounting_data_last_period = self.stock_accounting_value(accounts_by_product, at_date=fiscal_year_date_from)
accounts = accounting_data_today.keys() | accounting_data_last_period.keys()
for account in accounts:
variation_acc = account.account_stock_variation_id
expense_acc = account.account_stock_expense_id
if not variation_acc or not expense_acc:
continue
balance_today = accounting_data_today.get(account, 0) - extra_balance[account]
balance_last_period = accounting_data_last_period.get(account, 0)
balance_over_period = balance_today - balance_last_period
current_balance_domain = Domain([
('account_id', '=', variation_acc.id),
('company_id', '=', self.id),
('parent_state', '=', 'posted'),
])
if at_date:
current_balance_domain &= Domain([('date', '<=', at_date)])
existing_balance = sum(self.env['account.move.line'].search(current_balance_domain).mapped('balance'))
balance_over_period += existing_balance
if self.currency_id.is_zero(balance_over_period):
continue
amls_vals = self._prepare_inventory_aml_vals(
expense_acc,
variation_acc,
balance_over_period,
_('Closing: Stock Variation Over Period'),
)
amls_vals_list += amls_vals
return amls_vals_list
def _prepare_inventory_aml_vals(self, debit_acc, credit_acc, balance, ref, product_id=False):
if balance < 0:
temp = credit_acc
credit_acc = debit_acc
debit_acc = temp
balance = abs(balance)
return [{
'account_id': credit_acc.id,
'name': ref,
'debit': 0,
'credit': balance,
'product_id': product_id,
}, {
'account_id': debit_acc.id,
'name': ref,
'debit': balance,
'credit': 0,
'product_id': product_id,
}]
def _get_last_closing_date(self):
self.ensure_one()
key = f'{self.id}.stock_valuation_closing_ids'
closing_ids = self.env['ir.config_parameter'].sudo().get_param(key)
closing_ids = closing_ids.split(',') if closing_ids else []
closing = self.env['account.move']
while not closing and closing_ids:
closing_id = closing_ids.pop(-1)
closing_id = int(closing_id)
closing = self.env['account.move'].browse(closing_id).exists().filtered(lambda am: am.state == 'posted')
if not closing:
return False
am_state_field = self.env['ir.model.fields'].search([('model', '=', 'account.move'), ('name', '=', 'state')], limit=1)
state_tracking = closing.message_ids.sudo().tracking_value_ids.filtered(lambda t: t.field_id == am_state_field).sorted('id')
return state_tracking[-1:].create_date or fields.Datetime.to_datetime(closing.date)
def _save_closing_id(self, move_id):
self.ensure_one()
key = f'{self.id}.stock_valuation_closing_ids'
closing_ids = self.env['ir.config_parameter'].sudo().get_param(key)
ids = closing_ids.split(',') if closing_ids else []
ids.append(str(move_id))
if len(ids) > 10:
ids = ids[1:]
self.env['ir.config_parameter'].sudo().set_param(key, ','.join(ids))
def _set_category_defaults(self):
for company in self:
self.env['ir.default'].set('product.category', 'property_valuation', company.inventory_valuation, company_id=company.id)
self.env['ir.default'].set('product.category', 'property_cost_method', company.cost_method, company_id=company.id)
self.env['ir.default'].set('product.category', 'property_stock_journal', company.account_stock_journal_id.id, company_id=company.id)
self.env['ir.default'].set('product.category', 'property_stock_valuation_account_id', company.account_stock_valuation_id.id, company_id=company.id)