308 lines
14 KiB
Python
308 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from odoo import api, Command, fields, models, _
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.osv import expression
|
|
from odoo.tools import format_amount
|
|
|
|
ACCOUNT_DOMAIN = "['&', ('deprecated', '=', False), ('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card','off_balance'))]"
|
|
|
|
class ProductCategory(models.Model):
|
|
_inherit = "product.category"
|
|
|
|
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.",
|
|
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.",
|
|
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',
|
|
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="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, 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.")
|
|
account_tag_ids = fields.Many2many(
|
|
string="Account Tags",
|
|
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
|
|
}
|
|
|
|
def _get_asset_accounts(self):
|
|
res = {}
|
|
res['stock_input'] = False
|
|
res['stock_output'] = False
|
|
return res
|
|
|
|
def get_product_accounts(self, fiscal_pos=None):
|
|
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')
|
|
def _compute_tax_string(self):
|
|
for record in self:
|
|
record.tax_string = record._construct_tax_string(record.list_price)
|
|
|
|
def _construct_tax_string(self, price):
|
|
currency = self.currency_id
|
|
res = self.taxes_id.compute_all(price, product=self, partner=self.env['res.partner'])
|
|
joined = []
|
|
included = res['total_included']
|
|
if currency.compare_amounts(included, price):
|
|
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(_('%(amount)s Excl. Taxes', amount=format_amount(self.env, excluded, currency)))
|
|
if joined:
|
|
tax_string = f"(= {', '.join(joined)})"
|
|
else:
|
|
tax_string = " "
|
|
return tax_string
|
|
|
|
@api.constrains('uom_id')
|
|
def _check_uom_not_in_invoice(self):
|
|
self.env['product.template'].flush_model(['uom_id'])
|
|
self._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
|
|
LIMIT 1
|
|
""", [tuple(self.ids)])
|
|
if self._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
|
|
for product_grouped_by_tax in self.grouped('taxes_id').values():
|
|
product_grouped_by_tax.taxes_id += default_customer_taxes
|
|
self.invalidate_recordset(['taxes_id'])
|
|
|
|
def _force_default_purchase_tax(self, companies):
|
|
default_supplier_taxes = companies.filtered('account_purchase_tax_id').account_purchase_tax_id
|
|
for product_grouped_by_tax in self.grouped('supplier_taxes_id').values():
|
|
product_grouped_by_tax.supplier_taxes_id += default_supplier_taxes
|
|
self.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', 'not in', 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"
|
|
|
|
tax_string = fields.Char(compute='_compute_tax_string')
|
|
|
|
def _get_product_accounts(self):
|
|
return self.product_tmpl_id._get_product_accounts()
|
|
|
|
def _get_tax_included_unit_price(self, company, currency, document_date, document_type,
|
|
is_refund_document=False, product_uom=None, product_currency=None,
|
|
product_price_unit=None, product_taxes=None, fiscal_position=None
|
|
):
|
|
""" 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
|
|
|
|
assert document_type
|
|
|
|
if product_uom is None:
|
|
product_uom = product.uom_id
|
|
if not product_currency:
|
|
if document_type == 'sale':
|
|
product_currency = product.currency_id
|
|
elif document_type == 'purchase':
|
|
product_currency = company.currency_id
|
|
if product_price_unit is None:
|
|
if document_type == 'sale':
|
|
product_price_unit = product.with_company(company).lst_price
|
|
elif document_type == 'purchase':
|
|
product_price_unit = product.with_company(company).standard_price
|
|
else:
|
|
return 0.0
|
|
if product_taxes is None:
|
|
if document_type == 'sale':
|
|
product_taxes = product.taxes_id.filtered(lambda x: x.company_id == company)
|
|
elif document_type == 'purchase':
|
|
product_taxes = product.supplier_taxes_id.filtered(lambda x: x.company_id == company)
|
|
# Apply unit of measure.
|
|
if product_uom and product.uom_id != product_uom:
|
|
product_price_unit = product.uom_id._compute_price(product_price_unit, product_uom)
|
|
|
|
# Apply fiscal position.
|
|
if product_taxes and fiscal_position:
|
|
product_price_unit = self._get_tax_included_unit_price_from_price(
|
|
product_price_unit,
|
|
product_taxes,
|
|
fiscal_position=fiscal_position,
|
|
)
|
|
|
|
# Apply currency rate.
|
|
if currency != product_currency:
|
|
product_price_unit = product_currency._convert(product_price_unit, currency, company, document_date, round=False)
|
|
|
|
return product_price_unit
|
|
|
|
def _get_tax_included_unit_price_from_price(
|
|
self, product_price_unit, product_taxes,
|
|
fiscal_position=None,
|
|
product_taxes_after_fp=None,
|
|
):
|
|
if not product_taxes:
|
|
return product_price_unit
|
|
|
|
if product_taxes_after_fp is None:
|
|
if not fiscal_position:
|
|
return product_price_unit
|
|
|
|
product_taxes_after_fp = fiscal_position.map_tax(product_taxes)
|
|
|
|
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')
|
|
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, name=None, default_code=None, barcode=None, company=None, extra_domain=None):
|
|
'''Search all products and find one that matches one of 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.
|
|
:param company: The company of the product.
|
|
:param extra_domain: Any extra domain to add to the search.
|
|
:returns: A product or an empty recordset if not found.
|
|
'''
|
|
if name and '\n' in name:
|
|
# cut Sales Description from the name
|
|
name = name.split('\n')[0]
|
|
domains = []
|
|
if default_code:
|
|
domains.append([('default_code', '=', default_code)])
|
|
if barcode:
|
|
domains.append([('barcode', '=', barcode)])
|
|
|
|
# Search for the product with the exact name, then ilike the name
|
|
name_domains = [('name', '=', name)], [('name', 'ilike', name)] if name else []
|
|
company = company or self.env.company
|
|
for name_domain in name_domains:
|
|
for extra_domain in (
|
|
[*self.env['res.partner']._check_company_domain(company), ('company_id', '!=', False)],
|
|
[('company_id', '=', False)],
|
|
):
|
|
product = self.env['product.product'].search(
|
|
expression.AND([
|
|
expression.OR(domains + [name_domain]),
|
|
extra_domain,
|
|
]),
|
|
limit=1,
|
|
)
|
|
if product:
|
|
return product
|
|
return self.env['product.product']
|