У меня возникли проблемы при создании счета в коммерческих предложениях. Я только добавил новое поле дополнительной скидки на единицу товара в заказе на продажу, но создал учет, чтобы включить его, но затем возникла ошибка, когда я собирался создать счет.
Это моя скидка.py
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from functools import partial
from odoo.tools.misc import formatLang
class Discount(models.Model):
_inherit = 'sale.order.line'
def _compute_amount_undiscounted(self):
for order in self:
total = 0.0
for line in order.order_line:
total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * (
(line.add_discount or 0.0) / 100.0) * line.product_uom_qty
order.amount_undiscounted = total
def _amount_by_group(self):
for order in self:
currency = order.currency_id or order.company_id.currency_id
fmt = partial(formatLang, self.with_context(lang=order.partner_id.lang).env, currency_obj=currency)
res = {}
for line in order.order_line:
price_reduce = line.price_unit * (1.0 - line.discount / 100.0) * (1.0 - line.add_discount / 100.0)
taxes = line.tax_id.compute_all(price_reduce, quantity=line.product_uom_qty, product=line.product_id,
partner=order.partner_shipping_id)['taxes']
for tax in line.tax_id:
group = tax.tax_group_id
res.setdefault(group, {'amount': 0.0, 'base': 0.0})
for t in taxes:
if t['id'] == tax.id or t['id'] in tax.children_tax_ids.ids:
res[group]['amount'] += t['amount']
res[group]['base'] += t['base']
res = sorted(res.items(), key=lambda l: l[0].sequence)
order.amount_by_group = [(
l[0].name, l[1]['amount'], l[1]['base'],
fmt(l[1]['amount']), fmt(l[1]['base']),
len(res),
) for l in res]
@api.depends('product_uom_qty', 'discount', 'add_discount', 'price_unit', 'tax_id')
def _compute_amount(self):
"""
Compute the amounts of the SO line.
"""
for line in self:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) * (1 - (line.add_discount or 0.0) / 100.0)
taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty,
product=line.product_id,
partner=line.order_id.partner_shipping_id)
line.update({
'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])),
'price_total': taxes['total_included'],
'price_subtotal': taxes['total_excluded'],
})
@api.depends('price_unit', 'discount', 'add_discount')
def _get_price_reduce(self):
for line in self:
line.price_reduce = line.price_unit * (1.0 - line.discount / 100.0) * (1.0 - line.add_discount / 100.0)
def _prepare_invoice_line(self):
"""
Prepare the dict of values to create the new invoice line for a sales order line.
:param qty: float quantity to invoice
"""
self.ensure_one()
res = {
'display_type': self.display_type,
'sequence': self.sequence,
'name': self.name,
'product_id': self.product_id.id,
'product_uom_id': self.product_uom.id,
'quantity': self.qty_to_invoice,
'discount': self.discount,
'add_discount': self.add_discount,
'price_unit': self.price_unit,
'tax_ids': [(6, 0, self.tax_id.ids)],
'analytic_account_id': self.order_id.analytic_account_id.id,
'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
'sale_line_ids': [(4, self.id)],
}
if self.display_type:
res['account_id'] = False
return res
@api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id')
def _onchange_discount(self):
if not (self.product_id and self.product_uom and
self.order_id.partner_id and self.order_id.pricelist_id and
self.order_id.pricelist_id.discount_policy == 'without_discount' and
self.env.user.has_group('product.group_discount_per_so_line')):
return
self.discount = 0.0
self.add_discount = 0.0
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id,
quantity=self.product_uom_qty,
date=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id,
fiscal_position=self.env.context.get('fiscal_position')
)
product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order,
uom=self.product_uom.id)
price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule(
self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id)
new_list_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id,
self.product_uom_qty,
self.product_uom,
self.order_id.pricelist_id.id)
if new_list_price != 0:
if self.order_id.pricelist_id.currency_id != currency:
# we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
new_list_price = currency._convert(
new_list_price, self.order_id.pricelist_id.currency_id,
self.order_id.company_id or self.env.company, self.order_id.date_order or fields.Date.today())
discount = (new_list_price - price) / new_list_price * 100
if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0):
self.discount = discount
add_discount = (new_list_price - price) / new_list_price * 100
if (add_discount > 0 and new_list_price > 0) or (add_discount < 0 and new_list_price < 0):
self.add_discount = add_discount
add_discount = fields.Float(string='Add. Disc (%)', digits='Discount', default=0.0)
class SaleOrder(models.Model):
_inherit = 'sale.order'
add_discount = fields.Float(string='Add. Disc (%)', digits='Discount', default=0.0)
def _prepare_invoice(self, ):
invoice_vals = super(SaleOrder, self)._prepare_invoice()
invoice_vals.update({
'add_discount': self.add_discount,
})
return invoice_vals
Это мой аккаунт.py
from odoo import api, fields, models, _
from odoo.exceptions import RedirectWarning, UserError, ValidationError, AccessError
# forbidden fields
INTEGRITY_HASH_MOVE_FIELDS = ('date', 'journal_id', 'company_id')
INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id')
class AccountInvoiceLine(models.Model):
_inherit = "account.move.line"
def _get_price_total_and_subtotal(self, price_unit=None, quantity=None, discount=None, currency=None,
product=None, partner=None, taxes=None, move_type=None, add_discount=None, ):
self.ensure_one()
return self._get_price_total_and_subtotal_model(
price_unit=price_unit or self.price_unit,
quantity=quantity or self.quantity,
discount=discount or self.discount,
currency=currency or self.currency_id,
product=product or self.product_id,
partner=partner or self.partner_id,
taxes=taxes or self.tax_ids,
move_type=move_type or self.move_id.type,
add_discount=add_discount or self.add_discount,
)
@api.model
def _get_price_total_and_subtotal_model(self, price_unit, quantity, discount, add_discount, currency, product,
partner, taxes, move_type):
''' This method is used to compute 'price_total' & 'price_subtotal'.
:param price_unit: The current price unit.
:param quantity: The current quantity.
:param discount: The current discount.
:param add_discount: The current additional discount
:param currency: The line's currency.
:param product: The line's product.
:param partner: The line's partner.
:param taxes: The applied taxes.
:param move_type: The type of the move.
:return: A dictionary containing 'price_subtotal' & 'price_total'.
'''
res = {}
# Compute 'price_subtotal'.
price_unit_wo_discount = price_unit * (1 - (discount / 100.0)) * (1 - (add_discount / 100.0))
subtotal = quantity * price_unit_wo_discount
# Compute 'price_total'.
if taxes:
taxes_res = taxes._origin.compute_all(price_unit_wo_discount,
quantity=quantity, currency=currency, product=product,
partner=partner, is_refund=move_type in ('out_refund', 'in_refund'))
res['price_subtotal'] = taxes_res['total_excluded']
res['price_total'] = taxes_res['total_included']
else:
res['price_total'] = res['price_subtotal'] = subtotal
# In case of multi currency, round before it's use for computing debit credit
if currency:
res = {k: currency.round(v) for k, v in res.items()}
return res
def _get_fields_onchange_balance(self, quantity=None, discount=None, add_discount=None, balance=None,
move_type=None, currency=None, taxes=None, price_subtotal=None):
self.ensure_one()
return self._get_fields_onchange_balance_model(
quantity=quantity or self.quantity,
discount=discount or self.discount,
add_discount=add_discount or self.add_discount,
balance=balance or self.balance,
move_type=move_type or self.move_id.type,
currency=currency or self.currency_id or self.move_id.currency_id,
taxes=taxes or self.tax_ids,
price_subtotal=price_subtotal or self.price_subtotal,
)
@api.model
def _get_fields_onchange_balance_model(self, quantity, discount, add_discount, balance, move_type, currency, taxes, price_subtotal):
if move_type in self.move_id.get_outbound_types():
sign = 1
elif move_type in self.move_id.get_inbound_types():
sign = -1
else:
sign = 1
balance *= sign
# Avoid rounding issue when dealing with price included taxes. For example, when the price_unit is 2300.0 and
# a 5.5% price included tax is applied on it, a balance of 2300.0 / 1.055 = 2180.094 ~ 2180.09 is computed.
# However, when triggering the inverse, 2180.09 + (2180.09 * 0.055) = 2180.09 + 119.90 = 2299.99 is computed.
# To avoid that, set the price_subtotal at the balance if the difference between them looks like a rounding
# issue.
if currency.is_zero(balance - price_subtotal):
return {}
taxes = taxes.flatten_taxes_hierarchy()
if taxes and any(tax.price_include for tax in taxes):
taxes_res = taxes._origin.compute_all(balance, currency=currency, handle_price_include=False)
for tax_res in taxes_res['taxes']:
tax = self.env['account.tax'].browse(tax_res['id'])
if tax.price_include:
balance += tax_res['amount']
discount_factor = 1 - (discount / 100.0) - (add_discount / 100)
if balance and discount_factor:
# discount != 100%
vals = {
'quantity': quantity or 1.0,
'price_unit': balance / discount_factor / (quantity or 1.0),
}
elif balance and not discount_factor:
# discount == 100%
vals = {
'quantity': quantity or 1.0,
'discount': 0.0,
'price_unit': balance / (quantity or 1.0),
}
elif not discount_factor:
# balance of line is 0, but discount == 100% so we display the normal unit_price
vals = {}
else:
# balance is 0, so unit price is 0 as well
vals = {'price_unit': 0.0}
return vals
@api.onchange('quantity', 'discount', 'add_discount', 'price_unit', 'tax_ids')
def _onchange_price_subtotal(self):
for line in self:
if not line.move_id.is_invoice(include_receipts=True):
continue
line.update(line._get_price_total_and_subtotal())
line.update(line._get_fields_onchange_subtotal())
@api.model_create_multi
def create(self, vals_list):
# OVERRIDE
ACCOUNTING_FIELDS = ('debit', 'credit', 'amount_currency')
BUSINESS_FIELDS = ('price_unit', 'quantity', 'discount', 'add_discount', 'tax_ids')
for vals in vals_list:
move = self.env['account.move'].browse(vals['move_id'])
vals.setdefault('company_currency_id',
move.company_id.currency_id.id) # important to bypass the ORM limitation where monetary fields are not rounded; more info in the commit message
if move.is_invoice(include_receipts=True):
currency = move.currency_id
partner = self.env['res.partner'].browse(vals.get('partner_id'))
taxes = self.resolve_2many_commands('tax_ids', vals.get('tax_ids', []), fields=['id'])
tax_ids = set(tax['id'] for tax in taxes)
taxes = self.env['account.tax'].browse(tax_ids)
# Ensure consistency between accounting & business fields.
# As we can't express such synchronization as computed fields without cycling, we need to do it both
# in onchange and in create/write. So, if something changed in accounting [resp. business] fields,
# business [resp. accounting] fields are recomputed.
if any(vals.get(field) for field in ACCOUNTING_FIELDS):
if vals.get('currency_id'):
balance = vals.get('amount_currency', 0.0)
else:
balance = vals.get('debit', 0.0) - vals.get('credit', 0.0)
price_subtotal = self._get_price_total_and_subtotal_model(
vals.get('price_unit', 0.0),
vals.get('quantity', 0.0),
vals.get('discount', 0.0),
currency,
self.env['product.product'].browse(vals.get('product_id')),
partner,
taxes,
move.type,
vals.get('add_discount', 0.0)
).get('price_subtotal', 0.0)
vals.update(self._get_fields_onchange_balance_model(
vals.get('quantity', 0.0),
vals.get('discount', 0.0),
balance,
move.type,
currency,
taxes,
price_subtotal,
vals.get('add_discount', 0.0)
))
vals.update(self._get_price_total_and_subtotal_model(
vals.get('price_unit', 0.0),
vals.get('quantity', 0.0),
vals.get('discount', 0.0),
currency,
self.env['product.product'].browse(vals.get('product_id')),
partner,
taxes,
move.type,
vals.get('add_discount', 0.0)
))
elif any(vals.get(field) for field in BUSINESS_FIELDS):
vals.update(self._get_price_total_and_subtotal_model(
vals.get('price_unit', 0.0),
vals.get('quantity', 0.0),
vals.get('discount', 0.0),
vals.get('add_discount', 0.0),
currency,
self.env['product.product'].browse(vals.get('product_id')),
partner,
taxes,
move.type,
))
vals.update(self._get_fields_onchange_subtotal_model(
vals['price_subtotal'],
move.type,
currency,
move.company_id,
move.date,
))
# Ensure consistency between taxes & tax exigibility fields.
if 'tax_exigible' in vals:
continue
if vals.get('tax_repartition_line_id'):
repartition_line = self.env['account.tax.repartition.line'].browse(vals['tax_repartition_line_id'])
tax = repartition_line.invoice_tax_id or repartition_line.refund_tax_id
vals['tax_exigible'] = tax.tax_exigibility == 'on_invoice'
elif vals.get('tax_ids'):
tax_ids = [v['id'] for v in self.resolve_2many_commands('tax_ids', vals['tax_ids'], fields=['id'])]
taxes = self.env['account.tax'].browse(tax_ids).flatten_taxes_hierarchy()
vals['tax_exigible'] = not any(tax.tax_exigibility == 'on_payment' for tax in taxes)
lines = super(AccountInvoiceLine, self).create(vals_list)
moves = lines.mapped('move_id')
if self._context.get('check_move_validity', True):
moves._check_balanced()
moves._check_fiscalyear_lock_date()
lines._check_tax_lock_date()
return lines
def write(self, vals):
# OVERRIDE
def field_will_change(line, field_name):
if field_name not in vals:
return False
field = line._fields[field_name]
if field.type == 'many2one':
return line[field_name].id != vals[field_name]
if field.type in ('one2many', 'many2many'):
current_ids = set(line[field_name].ids)
after_write_ids = set(
r['id'] for r in line.resolve_2many_commands(field_name, vals[field_name], fields=['id']))
return current_ids != after_write_ids
if field.type == 'monetary' and line[field.currency_field]:
return not line[field.currency_field].is_zero(line[field_name] - vals[field_name])
return line[field_name] != vals[field_name]
ACCOUNTING_FIELDS = ('debit', 'credit', 'amount_currency')
BUSINESS_FIELDS = ('price_unit', 'quantity', 'discount', 'add_discount', 'tax_ids')
PROTECTED_FIELDS_TAX_LOCK_DATE = ['debit', 'credit', 'tax_line_id', 'tax_ids', 'tag_ids']
PROTECTED_FIELDS_LOCK_DATE = PROTECTED_FIELDS_TAX_LOCK_DATE + ['account_id', 'journal_id',
'amount_currency', 'currency_id',
'partner_id']
PROTECTED_FIELDS_RECONCILIATION = (
'account_id', 'date', 'debit', 'credit', 'amount_currency', 'currency_id')
account_to_write = self.env['account.account'].browse(vals['account_id']) if 'account_id' in vals else None
# Check writing a deprecated account.
if account_to_write and account_to_write.deprecated:
raise UserError(_('You cannot use a deprecated account.'))
# when making a reconciliation on an existing liquidity journal item, mark the payment as reconciled
for line in self:
if line.parent_state == 'posted':
if line.move_id.restrict_mode_hash_table and set(vals).intersection(INTEGRITY_HASH_LINE_FIELDS):
raise UserError(_(
"You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(
INTEGRITY_HASH_LINE_FIELDS))
if any(key in vals for key in ('tax_ids', 'tax_line_ids')):
raise UserError(_(
'You cannot modify the taxes related to a posted journal item, you should reset the journal entry to draft to do so.'))
if 'statement_line_id' in vals and line.payment_id:
# In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement
if all(line.statement_id for line in line.payment_id.move_line_ids.filtered(
lambda r: r.id != line.id and r.account_id.internal_type == 'liquidity')):
line.payment_id.state = 'reconciled'
# Check the lock date.
if any(field_will_change(line, field_name) for field_name in PROTECTED_FIELDS_LOCK_DATE):
line.move_id._check_fiscalyear_lock_date()
# Check the tax lock date.
if any(field_will_change(line, field_name) for field_name in PROTECTED_FIELDS_TAX_LOCK_DATE):
line._check_tax_lock_date()
# Check the reconciliation.
if any(field_will_change(line, field_name) for field_name in PROTECTED_FIELDS_RECONCILIATION):
line._check_reconciliation()
# Check switching receivable / payable accounts.
if account_to_write:
account_type = line.account_id.user_type_id.type
if line.move_id.is_sale_document(include_receipts=True):
if (account_type == 'receivable' and account_to_write.user_type_id.type != account_type) \
or (
account_type != 'receivable' and account_to_write.user_type_id.type == 'receivable'):
raise UserError(_(
"You can only set an account having the receivable type on payment terms lines for customer invoice."))
if line.move_id.is_purchase_document(include_receipts=True):
if (account_type == 'payable' and account_to_write.user_type_id.type != account_type) \
or (account_type != 'payable' and account_to_write.user_type_id.type == 'payable'):
raise UserError(_(
"You can only set an account having the payable type on payment terms lines for vendor bill."))
result = super(AccountInvoiceLine, self).write(vals)
for line in self:
if not line.move_id.is_invoice(include_receipts=True):
continue
# Ensure consistency between accounting & business fields.
# As we can't express such synchronization as computed fields without cycling, we need to do it both
# in onchange and in create/write. So, if something changed in accounting [resp. business] fields,
# business [resp. accounting] fields are recomputed.
if any(field in vals for field in ACCOUNTING_FIELDS):
balance = line.currency_id and line.amount_currency or line.debit - line.credit
price_subtotal = line._get_price_total_and_subtotal().get('price_subtotal', 0.0)
to_write = line._get_fields_onchange_balance(
balance=balance,
price_subtotal=price_subtotal,
)
to_write.update(line._get_price_total_and_subtotal(
price_unit=to_write.get('price_unit', line.price_unit),
quantity=to_write.get('quantity', line.quantity),
discount=to_write.get('discount', line.discount),
add_discount=to_write.get('add_discount', line.add_discount),
))
super(AccountInvoiceLine, line).write(to_write)
elif any(field in vals for field in BUSINESS_FIELDS):
to_write = line._get_price_total_and_subtotal()
to_write.update(line._get_fields_onchange_subtotal(
price_subtotal=to_write['price_subtotal'],
))
super(AccountInvoiceLine, line).write(to_write)
# Check total_debit == total_credit in the related moves.
if self._context.get('check_move_validity', True):
self.mapped('move_id')._check_balanced()
return result
add_discount = fields.Float(string='Discount (%)', digits=(16, 20), default=0.0)
Это мой аккаунт. xml
<odoo>
<record id="discount_view" model="ir.ui.view">
<field name="name">Additional Discount</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='discount']" position="after">
<field name="add_discount" string="Add. Disc.%" groups="base.group_no_one" optional="show"/>
</xpath>
<xpath expr="//field[@name='invoice_line_ids']/form/sheet/group[1]/field[@name='discount']" position="after">
<field name="add_discount" string="Add. Disc.%" groups="base.group_no_one" optional="show"/>
</xpath>
<xpath expr="//field[@name='line_ids']/tree/field[@name='discount']" position="after">
<field name="add_discount" invisible="1"/>
</xpath>
</field>
</record>
</odoo>
Это ошибка, когда я собираюсь создать счет в заказе на продажу.
Error:
Odoo Server Error
raise ValueError("Invalid field %r on model %r" % (e.args[0], self._name))
ValueError: Invalid field 'add_discount' on model 'account.move'
Я не понимаю, как эта штука работает.