Files
enterprise-addons/l10n_ro_saft_stock/models/account_general_ledger.py
T

323 lines
16 KiB
Python

from collections import defaultdict
from odoo import api, models
from odoo.tools import float_round
from odoo.exceptions import RedirectWarning
from odoo.addons.account_edi_ubl_cii.models.account_edi_common import UOM_TO_UNECE_CODE
def safe_division(numerator, denominator):
if not numerator or not denominator:
return 0
return numerator / denominator
class AccountGeneralLedgerReportHandler(models.AbstractModel):
_inherit = 'account.general.ledger.report.handler'
def _custom_options_initializer(self, report, options, previous_options):
# EXTENDS l10n_ro_saft
super()._custom_options_initializer(report, options, previous_options)
if self.env.company.account_fiscal_country_id.code != 'RO':
return
options['buttons'].append({
'name': self.env._("SAF-T (D406 Stocks Declaration)"),
'sequence': 52,
'action': 'export_file',
'action_param': 'l10n_ro_export_saft_to_xml_stocks',
'file_export_type': self.env._("XML")
})
@api.model
def l10n_ro_export_saft_to_xml_stocks(self, options):
options['l10n_ro_saft_type'] = 'stocks'
options['l10n_ro_saft_required_sections'] = self._set_l10n_ro_saft_stock_required_sections()
return self.l10n_ro_export_saft_to_xml(options)
@api.model
def _set_l10n_ro_saft_stock_required_sections(self):
return {
'master_files': {
'general_ledger_accounts': True,
'customers': False,
'suppliers': False,
'tax_table': True,
'uom_table': True,
'analysis_type_table': True,
'movement_type_table': True,
'products': True,
'physical_stocks': True,
'owners': True,
'assets': False,
},
'general_ledger_entries': False,
'source_documents': {
'sales_invoices': False,
'purchase_invoices': False,
'payments': False,
'movement_of_goods': True,
'asset_transactions': False,
}
}
@api.model
def _saft_prepare_report_initial_values(self, options, values):
# EXTENDS account_saft
# It runs the methods that fill l10n_ro_saft_stock_encountered_values before
# the rest of the methods because the encountered values are used in the other
# saft methods (e.g., encountered accounts are used in _saft_fill_report_general_ledger_accounts)
super()._saft_prepare_report_initial_values(options, values)
if options.get('l10n_ro_saft_type') == 'stocks':
# the dict l10n_ro_saft_stock_encountered_values is used to collect
# encountered l10n_ro_saft_stock relevant values. It also signals
# that we are in a stock XML export
values['l10n_ro_saft_stock_encountered_values'] = defaultdict(set)
self._l10n_ro_saft_stock_fill_physical_stocks_values(options, values)
self._l10n_ro_saft_stock_fill_movement_of_goods_values(options, values)
self._l10n_ro_saft_stock_fill_movement_types_values(options, values)
self._l10n_ro_saft_stock_fill_owners_values(options, values)
@api.model
def _l10n_ro_saft_fill_header_values(self, options, values):
# EXTENDS l10n_ro_saft
super()._l10n_ro_saft_fill_header_values(options, values)
if values['company'].country_code == 'RO' and options.get('l10n_ro_saft_type') == 'stocks':
values['declaration_type'] = 'C'
@api.model
def _saft_fill_report_general_ledger_accounts(self, report, options, values):
# EXTENDS account_saft
super()._saft_fill_report_general_ledger_accounts(report, options, values)
if encountered_account_ids := values.get('l10n_ro_saft_stock_encountered_values', {}).get('account.account'):
existing_account_ids = {vals['account'].id for vals in values['account_vals_list']}
encountered_account_ids -= existing_account_ids
encountered_accounts = self.env['account.account'].browse(encountered_account_ids)
account_types_dict = dict(self.env['account.account']._fields['account_type']._description_selection(self.env))
for account in encountered_accounts:
values['account_vals_list'].append({
'account': account,
'account_type': account_types_dict[account.account_type],
'saft_account_type': self._saft_get_account_type(account.account_type),
'opening_balance': 0,
'closing_balance': 0,
})
def _get_encountered_product_uom_ids(self, values):
# EXTENDS l10n_ro_saft
result = super()._get_encountered_product_uom_ids(values)
if 'l10n_ro_saft_stock_encountered_values' in values:
result |= values['l10n_ro_saft_stock_encountered_values']['uom.uom']
return result
def _get_encountered_product_ids(self, values):
# EXTENDS l10n_ro_saft
result = super()._get_encountered_product_ids(values)
if 'l10n_ro_saft_stock_encountered_values' in values:
result |= values['l10n_ro_saft_stock_encountered_values']['product.product']
return result
def _validate_product_categories_accounts(self, categories):
if invalid_categories := categories.filtered(lambda c: not c.property_stock_valuation_account_id):
raise RedirectWarning(
self.env._("Each product category must have a Stock Valuation Account assigned."),
invalid_categories._get_records_action(),
self.env._("View Invalid Categories"),
)
@api.model
def _l10n_ro_saft_stock_fill_physical_stocks_values(self, options, values):
if not options['l10n_ro_saft_required_sections']['master_files']['physical_stocks']:
return
values['physical_stocks'] = []
self.env['stock.quant']._quant_tasks()
encountered_partner_ids = values['l10n_ro_saft_stock_encountered_values']['res.partner']
encountered_account_ids = values['l10n_ro_saft_stock_encountered_values']['account.account']
encountered_owner_account_codes = values['l10n_ro_saft_stock_encountered_values']['owner_account_codes']
quantities_dict = defaultdict(lambda: {'before': 0, 'after': 0})
domains = {
'before': [('date', '<', options['date']['date_from'])],
'after': [('date', '>=', options['date']['date_from']), ('date', '<=', options['date']['date_to'])],
}
for quantities_key, domain in domains.items():
lines = self.env['stock.move.line']._read_group(
domain=[
('company_id', '=', values['company'].id),
('state', '=', 'done'),
'|',
('location_id.usage', 'in', ('internal', 'transit')),
('location_dest_id.usage', 'in', ('internal', 'transit')),
*domain,
],
aggregates=['quantity_product_uom:sum'],
groupby=['product_id', 'location_id', 'location_dest_id', 'lot_id', 'owner_id'],
)
for product_id, location_id, location_dest_id, lot_id, owner_id, qty in lines:
if location_id.usage in ('internal', 'transit'):
quantities_dict[product_id, location_id, lot_id, owner_id][quantities_key] -= qty
if location_dest_id.usage in ('internal', 'transit'):
quantities_dict[product_id, location_dest_id, lot_id, owner_id][quantities_key] += qty
for (product_id, location_id, lot_id, owner_id), qty in sorted(quantities_dict.items(), key=lambda kv: tuple(r.id for r in kv[0])):
quantity_from = qty['before']
quantity_to = qty['after'] + qty['before']
if not quantity_from and not quantity_to:
continue
value_from = quantity_from * product_id._with_valuation_context().with_context(to_date=options['date']['date_from']).avg_cost
value_to = quantity_to * product_id._with_valuation_context().with_context(to_date=options['date']['date_to']).avg_cost
owner_id = owner_id or values['company'].partner_id
values['physical_stocks'].append({
'warehouse_id': location_id.warehouse_id.id,
'location_id': location_id.id,
'product_code': product_id.default_code or product_id.code,
'lot': lot_id.name,
'product_type': product_id.type,
'commodity_code': self._get_commodity_code(product_id),
'owner_id': self._l10n_ro_saft_get_registration_number(owner_id),
'uom_physical_stock': UOM_TO_UNECE_CODE.get(product_id.uom_id.get_external_id()[product_id.uom_id.id], 'C62'),
'uom_conversion_factor': 1, # stock.quant is always in the product UOM
'unit_price': float_round(product_id.standard_price or 0.0, 2),
'opening_stock_quantity': float_round(quantity_from, 2),
'opening_stock_value': float_round(value_from, 2),
'closing_stock_quantity': float_round(quantity_to, 2),
'closing_stock_value': float_round(value_to, 2),
'stock_characteristic': product_id.name,
})
encountered_partner_ids.add(owner_id.id)
encountered_account_ids.add(product_id.categ_id.property_stock_valuation_account_id.id)
encountered_owner_account_codes.add((owner_id.id, product_id.categ_id.property_stock_valuation_account_id.code))
@api.model
def _l10n_ro_saft_stock_fill_movement_of_goods_values(self, options, values):
if not options['l10n_ro_saft_required_sections']['source_documents']['movement_of_goods']:
return
values['movement_of_goods'] = {}
values['movement_of_goods']['stock_movements'] = []
values['movement_of_goods']['nb_stock_movement_lines'] = 0
values['movement_of_goods']['total_quantity_received'] = 0
values['movement_of_goods']['total_quantity_issued'] = 0
moves = self.env['stock.move'].search([
('company_id', '=', values['company'].id),
('state', '=', 'done'),
('date', '>=', options['date']['date_from']),
('date', '<=', options['date']['date_to']),
])
self._validate_product_categories_accounts(moves.move_line_ids.product_id.categ_id)
encountered_picking_type_ids = values['l10n_ro_saft_stock_encountered_values']['stock.picking.type']
encountered_uom_ids = values['l10n_ro_saft_stock_encountered_values']['uom.uom']
encountered_product_ids = values['l10n_ro_saft_stock_encountered_values']['product.product']
encountered_account_ids = values['l10n_ro_saft_stock_encountered_values']['account.account']
for move in moves:
movement_type = None
quantity_factor = 0
# Inventory count positive adjustment
if move.location_usage not in ('internal', 'transit') and move.location_dest_usage in ('internal', 'transit'):
movement_type = '110'
quantity_factor = 1
# Inventory count negative adjustment
elif move.location_usage in ('internal', 'transit') and move.location_dest_usage not in ('internal', 'transit'):
movement_type = '120'
quantity_factor = -1
# Internal Transfer
else:
movement_type = '80'
quantity_factor = 0
if move.picking_type_id:
movement_type = move.picking_type_id.l10n_ro_stock_movement_type
encountered_picking_type_ids.add(move.picking_type_id.id)
values['movement_of_goods']['stock_movements'].append({
'movement_reference': move.reference,
'movement_date': move.date.date().isoformat(),
'movement_type': movement_type,
'stock_movement_lines': [],
})
for line_number, line in enumerate(move.move_line_ids, 1):
values['movement_of_goods']['nb_stock_movement_lines'] += 1
if quantity_factor == 1:
values['movement_of_goods']['total_quantity_received'] += line['quantity']
elif quantity_factor == -1:
values['movement_of_goods']['total_quantity_issued'] += line['quantity']
customer = line.picking_partner_id if line.picking_code == 'outgoing' else values['company'].partner_id
supplier = line.picking_partner_id if line.picking_code == 'incoming' else values['company'].partner_id
values['movement_of_goods']['stock_movements'][-1]['stock_movement_lines'].append({
'line_number': line_number,
'account_id': line.product_id.categ_id.property_stock_valuation_account_id.code,
'customer_id': self._l10n_ro_saft_get_registration_number(customer),
'supplier_id': self._l10n_ro_saft_get_registration_number(supplier),
'ship_to': {
'warehouse_id': line.location_dest_id.warehouse_id.id,
'location_id': line.location_dest_id.id,
} if line.location_dest_usage in ('internal', 'transit') else None,
'ship_from': {
'warehouse_id': line.location_id.warehouse_id.id,
'location_id': line.location_id.id,
} if line.location_usage in ('internal', 'transit') else None,
'product_code': line.product_id.default_code or line.product_id.code,
'lot': line.lot_id.id,
'quantity': line['quantity'],
'unit_of_measure': UOM_TO_UNECE_CODE.get(line.product_uom_id.get_external_id()[line.product_uom_id.id], 'C62'),
'uom_conversion_factor': safe_division(line.product_uom_id.factor, line.product_id.uom_id.factor),
'movement_subtype': movement_type,
})
encountered_uom_ids.add(line.product_uom_id.id)
encountered_product_ids.add(line.product_id.id)
encountered_account_ids.add(line.product_id.categ_id.property_stock_valuation_account_id.id)
@api.model
def _l10n_ro_saft_stock_fill_movement_types_values(self, options, values):
if not options['l10n_ro_saft_required_sections']['master_files']['movement_type_table']:
return
values['movement_types'] = []
picking_types = self.env['stock.picking.type'].browse(values['l10n_ro_saft_stock_encountered_values']['stock.picking.type']).sorted()
if picking_types_without_l10n_ro_movement_type := picking_types.filtered(lambda p: not p.l10n_ro_stock_movement_type):
raise RedirectWarning(
self.env._("The Romanian authorities require a movement type for each picking type."),
picking_types_without_l10n_ro_movement_type._get_records_action(),
self.env._("View Picking Types"),
)
for picking_type in picking_types:
values['movement_types'].append({
'code': picking_type.l10n_ro_stock_movement_type,
'name': picking_type.display_name,
})
def _get_all_partners(self, values, balance_result):
# EXTENDS account_saft
result = super()._get_all_partners(values, balance_result)
if 'l10n_ro_saft_stock_encountered_values' in values:
result |= self.env['res.partner'].browse(values['l10n_ro_saft_stock_encountered_values']['res.partner'])
return result
@api.model
def _l10n_ro_saft_stock_fill_owners_values(self, options, values):
if not options['l10n_ro_saft_required_sections']['master_files']['owners']:
return
values['owners'] = []
owners = self.env['res.partner'].browse(values['l10n_ro_saft_stock_encountered_values']['res.partner']).sorted()
owner_account_code = dict(values['l10n_ro_saft_stock_encountered_values']['owner_account_codes'])
for owner in owners:
values['owners'].append({
'partner_id': owner.id,
'owner_id': self._l10n_ro_saft_get_registration_number(owner),
'account_id': owner_account_code[owner.id],
})