mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-23 06:22:05 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
|
|
@ -1,10 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import api, fields, models, _, Command
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.fields import Domain
|
||||
from odoo.tools import format_amount
|
||||
from odoo.tools.misc import split_every
|
||||
|
||||
|
||||
ACCOUNT_DOMAIN = "[('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card','off_balance'))]"
|
||||
|
||||
ACCOUNT_DOMAIN = "['&', '&', '&', ('deprecated', '=', False), ('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card')), ('company_id', '=', current_company_id), ('is_off_balance', '=', False)]"
|
||||
|
||||
class ProductCategory(models.Model):
|
||||
_inherit = "product.category"
|
||||
|
|
@ -12,28 +14,44 @@ class ProductCategory(models.Model):
|
|||
property_account_income_categ_id = fields.Many2one('account.account', company_dependent=True,
|
||||
string="Income Account",
|
||||
domain=ACCOUNT_DOMAIN,
|
||||
help="This account will be used when validating a customer invoice.")
|
||||
help="This account will be used when validating a customer invoice.",
|
||||
tracking=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
property_account_expense_categ_id = fields.Many2one('account.account', company_dependent=True,
|
||||
string="Expense Account",
|
||||
domain=ACCOUNT_DOMAIN,
|
||||
help="The expense is accounted for when a vendor bill is validated, except in anglo-saxon accounting with perpetual inventory valuation in which case the expense (Cost of Goods Sold account) is recognized at the customer invoice validation.")
|
||||
help="The expense is accounted for when a vendor bill is validated, except in anglo-saxon accounting with perpetual inventory valuation in which case the expense (Cost of Goods Sold account) is recognized at the customer invoice validation.",
|
||||
tracking=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Products
|
||||
#----------------------------------------------------------
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
taxes_id = fields.Many2many('account.tax', 'product_taxes_rel', 'prod_id', 'tax_id', help="Default taxes used when selling the product.", string='Customer Taxes',
|
||||
domain=[('type_tax_use', '=', 'sale')], default=lambda self: self.env.company.account_sale_tax_id)
|
||||
taxes_id = fields.Many2many('account.tax', 'product_taxes_rel', 'prod_id', 'tax_id',
|
||||
string="Sales Taxes",
|
||||
help="Default taxes used when selling the product",
|
||||
domain=[('type_tax_use', '=', 'sale')],
|
||||
default=lambda self: self.env.companies.account_sale_tax_id or self.env.companies.root_id.sudo().account_sale_tax_id,
|
||||
)
|
||||
tax_string = fields.Char(compute='_compute_tax_string')
|
||||
supplier_taxes_id = fields.Many2many('account.tax', 'product_supplier_taxes_rel', 'prod_id', 'tax_id', string='Vendor Taxes', help='Default taxes used when buying the product.',
|
||||
domain=[('type_tax_use', '=', 'purchase')], default=lambda self: self.env.company.account_purchase_tax_id)
|
||||
property_account_income_id = fields.Many2one('account.account', company_dependent=True,
|
||||
supplier_taxes_id = fields.Many2many('account.tax', 'product_supplier_taxes_rel', 'prod_id', 'tax_id',
|
||||
string="Purchase Taxes",
|
||||
help="Default taxes used when buying the product",
|
||||
domain=[('type_tax_use', '=', 'purchase')],
|
||||
default=lambda self: self.env.companies.account_purchase_tax_id or self.env.companies.root_id.sudo().account_purchase_tax_id,
|
||||
)
|
||||
property_account_income_id = fields.Many2one('account.account', company_dependent=True, ondelete='restrict',
|
||||
string="Income Account",
|
||||
domain=ACCOUNT_DOMAIN,
|
||||
help="Keep this field empty to use the default value from the product category.")
|
||||
property_account_expense_id = fields.Many2one('account.account', company_dependent=True,
|
||||
property_account_expense_id = fields.Many2one('account.account', company_dependent=True, ondelete='restrict',
|
||||
string="Expense Account",
|
||||
domain=ACCOUNT_DOMAIN,
|
||||
help="Keep this field empty to use the default value from the product category. If anglo-saxon accounting with automated valuation method is configured, the expense account on the product category will be used.")
|
||||
|
|
@ -42,24 +60,46 @@ class ProductTemplate(models.Model):
|
|||
comodel_name='account.account.tag',
|
||||
domain="[('applicability', '=', 'products')]",
|
||||
help="Tags to be set on the base and tax journal items created for this product.")
|
||||
fiscal_country_codes = fields.Char(compute='_compute_fiscal_country_codes')
|
||||
|
||||
def _get_product_accounts(self):
|
||||
return {
|
||||
'income': self.property_account_income_id or self.categ_id.property_account_income_categ_id,
|
||||
'expense': self.property_account_expense_id or self.categ_id.property_account_expense_categ_id
|
||||
'income': (
|
||||
self.property_account_income_id
|
||||
or self._get_category_account('property_account_income_categ_id')
|
||||
or (self.company_id or self.env.company).income_account_id
|
||||
), 'expense': (
|
||||
self.property_account_expense_id
|
||||
or self._get_category_account('property_account_expense_categ_id')
|
||||
or (self.company_id or self.env.company).expense_account_id
|
||||
),
|
||||
}
|
||||
|
||||
def _get_asset_accounts(self):
|
||||
res = {}
|
||||
res['stock_input'] = False
|
||||
res['stock_output'] = False
|
||||
return res
|
||||
def _get_category_account(self, field_name):
|
||||
"""
|
||||
Return the first account defined on the product category hierarchy
|
||||
for the given field.
|
||||
"""
|
||||
categ = self.categ_id
|
||||
while categ:
|
||||
account = categ[field_name]
|
||||
if account:
|
||||
return account
|
||||
categ = categ.parent_id
|
||||
return self.env['account.account']
|
||||
|
||||
def get_product_accounts(self, fiscal_pos=None):
|
||||
accounts = self._get_product_accounts()
|
||||
if not fiscal_pos:
|
||||
fiscal_pos = self.env['account.fiscal.position']
|
||||
return fiscal_pos.map_accounts(accounts)
|
||||
return {
|
||||
key: (fiscal_pos or self.env['account.fiscal.position']).map_account(account)
|
||||
for key, account in self._get_product_accounts().items()
|
||||
}
|
||||
|
||||
@api.depends('company_id')
|
||||
@api.depends_context('allowed_company_ids')
|
||||
def _compute_fiscal_country_codes(self):
|
||||
for record in self:
|
||||
allowed_companies = record.company_id or self.env.companies
|
||||
record.fiscal_country_codes = ",".join(allowed_companies.mapped('account_fiscal_country_id.code'))
|
||||
|
||||
@api.depends('taxes_id', 'list_price')
|
||||
@api.depends_context('company')
|
||||
|
|
@ -69,16 +109,16 @@ class ProductTemplate(models.Model):
|
|||
|
||||
def _construct_tax_string(self, price):
|
||||
currency = self.currency_id
|
||||
res = self.taxes_id.filtered(lambda t: t.company_id == self.env.company).compute_all(
|
||||
res = self.taxes_id._filter_taxes_by_company(self.env.company).compute_all(
|
||||
price, product=self, partner=self.env['res.partner']
|
||||
)
|
||||
joined = []
|
||||
included = res['total_included']
|
||||
if currency.compare_amounts(included, price):
|
||||
joined.append(_('%s Incl. Taxes', format_amount(self.env, included, currency)))
|
||||
joined.append(_('%(amount)s Incl. Taxes', amount=format_amount(self.env, included, currency)))
|
||||
excluded = res['total_excluded']
|
||||
if currency.compare_amounts(excluded, price):
|
||||
joined.append(_('%s Excl. Taxes', format_amount(self.env, excluded, currency)))
|
||||
joined.append(_('%(amount)s Excl. Taxes', amount=format_amount(self.env, excluded, currency)))
|
||||
if joined:
|
||||
tax_string = f"(= {', '.join(joined)})"
|
||||
else:
|
||||
|
|
@ -88,26 +128,82 @@ class ProductTemplate(models.Model):
|
|||
@api.constrains('uom_id')
|
||||
def _check_uom_not_in_invoice(self):
|
||||
self.env['product.template'].flush_model(['uom_id'])
|
||||
self._cr.execute("""
|
||||
self.env.cr.execute("""
|
||||
SELECT prod_template.id
|
||||
FROM account_move_line line
|
||||
JOIN product_product prod_variant ON line.product_id = prod_variant.id
|
||||
JOIN product_template prod_template ON prod_variant.product_tmpl_id = prod_template.id
|
||||
JOIN uom_uom template_uom ON prod_template.uom_id = template_uom.id
|
||||
JOIN uom_category template_uom_cat ON template_uom.category_id = template_uom_cat.id
|
||||
JOIN uom_uom line_uom ON line.product_uom_id = line_uom.id
|
||||
JOIN uom_category line_uom_cat ON line_uom.category_id = line_uom_cat.id
|
||||
WHERE prod_template.id IN %s
|
||||
AND line.parent_state = 'posted'
|
||||
AND template_uom_cat.id != line_uom_cat.id
|
||||
AND template_uom.id != line_uom.id
|
||||
LIMIT 1
|
||||
""", [tuple(self.ids)])
|
||||
if self._cr.fetchall():
|
||||
if self.env.cr.fetchall():
|
||||
raise ValidationError(_(
|
||||
"This product is already being used in posted Journal Entries.\n"
|
||||
"If you want to change its Unit of Measure, please archive this product and create a new one."
|
||||
))
|
||||
|
||||
@api.onchange('type')
|
||||
def _onchange_type(self):
|
||||
if self.type == 'combo':
|
||||
self.taxes_id = False
|
||||
self.supplier_taxes_id = False
|
||||
return super()._onchange_type()
|
||||
|
||||
def _force_default_sale_tax(self, companies):
|
||||
default_customer_taxes = companies.filtered('account_sale_tax_id').account_sale_tax_id
|
||||
if not default_customer_taxes:
|
||||
return
|
||||
links = [Command.link(t.id) for t in default_customer_taxes]
|
||||
for sub_ids in split_every(self.env.cr.IN_MAX, self.ids):
|
||||
chunk = self.browse(sub_ids)
|
||||
chunk.write({'taxes_id': links})
|
||||
chunk.invalidate_recordset(['taxes_id'])
|
||||
|
||||
def _force_default_purchase_tax(self, companies):
|
||||
default_supplier_taxes = companies.filtered('account_purchase_tax_id').account_purchase_tax_id
|
||||
if not default_supplier_taxes:
|
||||
return
|
||||
links = [Command.link(t.id) for t in default_supplier_taxes]
|
||||
for sub_ids in split_every(self.env.cr.IN_MAX, self.ids):
|
||||
chunk = self.browse(sub_ids)
|
||||
chunk.write({'supplier_taxes_id': links})
|
||||
chunk.invalidate_recordset(['supplier_taxes_id'])
|
||||
|
||||
def _force_default_tax(self, companies):
|
||||
self._force_default_sale_tax(companies)
|
||||
self._force_default_purchase_tax(companies)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
products = super().create(vals_list)
|
||||
# If no company was set for the product, the product will be available for all companies and therefore should
|
||||
# have the default taxes of the other companies as well. sudo() is used since we're going to need to fetch all
|
||||
# the other companies default taxes which the user may not have access to.
|
||||
other_companies = self.env['res.company'].sudo().search(['!', ('id', 'child_of', self.env.companies.ids)])
|
||||
if other_companies and products:
|
||||
products_without_company = products.filtered(lambda p: not p.company_id).sudo()
|
||||
products_without_company._force_default_tax(other_companies)
|
||||
return products
|
||||
|
||||
def _get_list_price(self, price):
|
||||
""" Get the product sales price from a public price based on taxes defined on the product """
|
||||
self.ensure_one()
|
||||
if not self.taxes_id:
|
||||
return super()._get_list_price(price)
|
||||
computed_price = self.taxes_id.compute_all(price, self.currency_id)
|
||||
total_included = computed_price["total_included"]
|
||||
|
||||
if price == total_included:
|
||||
# Tax is configured as price included
|
||||
return total_included
|
||||
# calculate base from tax
|
||||
included_computed_price = self.taxes_id.with_context(force_price_include=True).compute_all(price, self.currency_id)
|
||||
return included_computed_price['total_excluded']
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
|
@ -124,6 +220,8 @@ class ProductProduct(models.Model):
|
|||
""" Helper to get the price unit from different models.
|
||||
This is needed to compute the same unit price in different models (sale order, account move, etc.) with same parameters.
|
||||
"""
|
||||
self.ensure_one()
|
||||
company.ensure_one()
|
||||
|
||||
product = self
|
||||
|
||||
|
|
@ -156,10 +254,8 @@ class ProductProduct(models.Model):
|
|||
if product_taxes and fiscal_position:
|
||||
product_price_unit = self._get_tax_included_unit_price_from_price(
|
||||
product_price_unit,
|
||||
currency,
|
||||
product_taxes,
|
||||
fiscal_position=fiscal_position,
|
||||
is_refund_document=is_refund_document,
|
||||
)
|
||||
|
||||
# Apply currency rate.
|
||||
|
|
@ -168,12 +264,10 @@ class ProductProduct(models.Model):
|
|||
|
||||
return product_price_unit
|
||||
|
||||
@api.model # the product is optional for `compute_all`
|
||||
def _get_tax_included_unit_price_from_price(
|
||||
self, product_price_unit, currency, product_taxes,
|
||||
self, product_price_unit, product_taxes,
|
||||
fiscal_position=None,
|
||||
product_taxes_after_fp=None,
|
||||
is_refund_document=False,
|
||||
):
|
||||
if not product_taxes:
|
||||
return product_price_unit
|
||||
|
|
@ -184,38 +278,62 @@ class ProductProduct(models.Model):
|
|||
|
||||
product_taxes_after_fp = fiscal_position.map_tax(product_taxes)
|
||||
|
||||
flattened_taxes_after_fp = product_taxes_after_fp._origin.flatten_taxes_hierarchy()
|
||||
flattened_taxes_before_fp = product_taxes._origin.flatten_taxes_hierarchy()
|
||||
taxes_before_included = all(tax.price_include for tax in flattened_taxes_before_fp)
|
||||
|
||||
if set(product_taxes.ids) != set(product_taxes_after_fp.ids) and taxes_before_included:
|
||||
taxes_res = flattened_taxes_before_fp.with_context(round=False, round_base=False).compute_all(
|
||||
product_price_unit,
|
||||
quantity=1.0,
|
||||
currency=currency,
|
||||
product=self,
|
||||
is_refund=is_refund_document,
|
||||
)
|
||||
product_price_unit = taxes_res['total_excluded']
|
||||
|
||||
if any(tax.price_include for tax in flattened_taxes_after_fp):
|
||||
taxes_res = flattened_taxes_after_fp.with_context(round=False, round_base=False).compute_all(
|
||||
product_price_unit,
|
||||
quantity=1.0,
|
||||
currency=currency,
|
||||
product=self,
|
||||
is_refund=is_refund_document,
|
||||
handle_price_include=False,
|
||||
)
|
||||
for tax_res in taxes_res['taxes']:
|
||||
tax = self.env['account.tax'].browse(tax_res['id'])
|
||||
if tax.price_include:
|
||||
product_price_unit += tax_res['amount']
|
||||
|
||||
return product_price_unit
|
||||
return product_taxes._adapt_price_unit_to_another_taxes(
|
||||
price_unit=product_price_unit,
|
||||
product=self,
|
||||
original_taxes=product_taxes,
|
||||
new_taxes=product_taxes_after_fp,
|
||||
)
|
||||
|
||||
@api.depends('lst_price', 'product_tmpl_id', 'taxes_id')
|
||||
@api.depends_context('company')
|
||||
def _compute_tax_string(self):
|
||||
for record in self:
|
||||
record.tax_string = record.product_tmpl_id._construct_tax_string(record.lst_price)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# EDI
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _retrieve_product(self, company=None, extra_domain=None, **product_vals):
|
||||
'''Search all products and find one that matches one of the parameters.
|
||||
|
||||
:param company: The company of the product.
|
||||
:param extra_domain: Any extra domain to add to the search.
|
||||
:param product_vals: Values the product should match.
|
||||
:returns: A product or an empty recordset if not found.
|
||||
'''
|
||||
domains = self._get_product_domain_search_order(**product_vals)
|
||||
company = company or self.env.company
|
||||
for _priority, domain in domains:
|
||||
for company_domain in (
|
||||
[*self.env['res.partner']._check_company_domain(company), ('company_id', '!=', False)],
|
||||
[('company_id', '=', False)],
|
||||
):
|
||||
if product := self.env['product.product'].search(
|
||||
Domain.AND([domain, company_domain, extra_domain or Domain.TRUE]), limit=1,
|
||||
):
|
||||
return product
|
||||
return self.env['product.product']
|
||||
|
||||
def _get_product_domain_search_order(self, **vals):
|
||||
"""Gives the order of search for a product given the parameters.
|
||||
|
||||
:param name: The name of the product.
|
||||
:param default_code: The default_code of the product.
|
||||
:param barcode: The barcode of the product.
|
||||
:returns: An ordered list of product domains and their associated priority.
|
||||
:rtype: list[tuple[int, Domain]]
|
||||
"""
|
||||
sorted_domains = []
|
||||
if barcode := vals.get('barcode'):
|
||||
sorted_domains.append((5, Domain('barcode', '=', barcode)))
|
||||
if default_code := vals.get('default_code'):
|
||||
sorted_domains.append((10, Domain('default_code', '=', default_code)))
|
||||
if name := vals.get('name'):
|
||||
name = name.split('\n', 1)[0] # Cut sales description from the name
|
||||
sorted_domains.append((15, Domain('name', '=', name)))
|
||||
# avoid matching unrelated products whose names merely contain that short string
|
||||
if len(name) > 4:
|
||||
sorted_domains.append((20, Domain('name', 'ilike', name)))
|
||||
return sorted_domains
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue