前言
在前一章中,我们使用继承来修改模块的行为。在我们的房地产场景中,我们希望更进一步,能够为我们的客户生成发票。Odoo提供了一个发票(Invoicing)模块,所以直接从我们的房地产模块创建一个发票会很简洁,也就是说,一旦一个属性被设置为“已售出”,发票就会在发票应用程序中被创建。
具体示例:账户移动
目标:
- 应该创建一个新的模块estate_account
- 房产出售时,应给买方开具发票
- 无论何时,我们与其他模块交互时,都需要牢记模块化。如果我们打算将我们的应用程序出售给房地产中介,一些人可能想要发票功能,但其他人可能不想要。
链接模块
此类用例的常见方法是创建一个“链接”模块。在我们的例子中,该模块将依赖于房地产和账户,并将包括房地产的发票创建逻辑。这样,房地产和会计模块可以独立安装。当两者都安装时,链接模块提供新功能。
锻炼
- 创建链接模块。
- 创建estate_account模块,该模块依赖于 estate和account模块。 现在,它将是一个空壳。
提示:您在本教程的开头已经这样做了。这个过程非常 类似。《odoo17 | 创建一个新应用程序》
当estate_account模块出现在列表中时,继续安装它! 你会注意到发票(Invoicing)应用程序也已安装。这是意料之中的,因为您的模块依赖于它。如果您卸载了发票(Invoicing)应用程序,您的estate_account模块也将被卸载。
创建发票
现在是生成发票的时候了。我们想为房地产模型添加功能,即我们想为房产出售时添加一些额外的逻辑。这听起来很熟悉吗?如果没有,最好回到上一章,因为你可能错过了什么!
作为第一步,我们需要扩展在按下房产上的“出售”按钮时调用的动作。为此,我们需要在 estate_account 模块中为 estate.property 模型创建一个模型继承。目前,重写的动作将简单地返回 super 调用。也许一个例子会使事情变得更清楚。
示例代码
from odoo import models
class InheritedModel(models.Model):
_inherit = "estate.property"
def inherited_action(self):
return super().inherited_action()
这里是一个完整代码
# -*- coding: utf-8 -*-
from odoo import api, models
class AccountMove(models.Model):
_inherit = 'account.move'
def action_invoice_paid(self):
""" 当发票链接到销售订单时,销售注册是
付费确认与会者。与会者确实不应该事先确认
完整的付款。 """
res = super(AccountMove, self).action_invoice_paid()
self.mapped('line_ids.sale_line_ids')._update_registrations(confirm=True, mark_as_paid=True)
return res
锻炼
添加发票创建的第一步。
- 在 estate_account 模块的models文件夹中创建 estate_property.py 文件。
- _inherit 继承房地产estate.property模型。
- 重写 action_sold 方法(您可能已将其重命名)以返回 super 调用。
提示:为了确保它工作正常,在重写的方法中添加一个打印或调试器断点。
启动项目是否报错?如果不是,请检查是否正确导入了所有Python文件。
如果覆盖有效,我们可以继续前进并创建发票。不幸的是,没有简单的方法知道如何在Odoo中创建任何给定的对象。大多数时候,有必要查看其模型以找到所需的字段并提供适当的值。
学习的一个好方法是看看其他模块是如何完成你想要做的事情的。例如,销售的基本流程之一是从销售订单中创建发票。这看起来是一个很好的起点,因为它正是我们想做的事情。花一些时间阅读和理解**_create_invoices**方法。
def _create_invoices(self, grouped=False, final=False, date=None):
"""
创建与销售订单关联的发票。
:param grouped: 如果为 True,则按 SO ID 对发票进行分组。如果为 False,则按
(合作伙伴发票ID,货币)
:param final: 如果为True,则必要时将生成退款
:returns: 创建的发票列表
"""
if not self.env['account.move'].check_access_rights('create', False):
try:
self.check_access_rights('write')
self.check_access_rule('write')
except AccessError:
return self.env['account.move']
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
# 1) 创建发票.
invoice_vals_list = []
invoice_item_sequence = 0
for order in self:
order = order.with_company(order.company_id)
current_section_vals = None
down_payments = order.env['sale.order.line']
# 发票金额
invoice_vals = order._prepare_invoice()
# 发票行值(仅保留必要的部分)
invoice_lines_vals = []
for line in order.order_line:
if line.display_type == 'line_section':
current_section_vals = line._prepare_invoice_line(sequence=invoice_item_sequence + 1)
continue
if line.display_type != 'line_note' and float_is_zero(line.qty_to_invoice, precision_digits=precision):
continue
if line.qty_to_invoice > 0 or (line.qty_to_invoice < 0 and final) or line.display_type == 'line_note':
if line.is_downpayment:
down_payments += line
continue
if current_section_vals:
invoice_item_sequence += 1
invoice_lines_vals.append(current_section_vals)
current_section_vals = None
invoice_item_sequence += 1
prepared_line = line._prepare_invoice_line(sequence=invoice_item_sequence)
invoice_lines_vals.append(prepared_line)
# 如果销售订单中有预付款,请将它们分组到共同部分下
if down_payments:
invoice_item_sequence += 1
down_payments_section = order._prepare_down_payment_section_line(sequence=invoice_item_sequence)
invoice_lines_vals.append(down_payments_section)
for down_payment in down_payments:
invoice_item_sequence += 1
invoice_down_payment_vals = down_payment._prepare_invoice_line(sequence=invoice_item_sequence)
invoice_lines_vals.append(invoice_down_payment_vals)
if not any(new_line['display_type'] is False for new_line in invoice_lines_vals):
raise self._nothing_to_invoice_error()
invoice_vals['invoice_line_ids'] = [(0, 0, invoice_line_id) for invoice_line_id in invoice_lines_vals]
invoice_vals_list.append(invoice_vals)
if not invoice_vals_list:
raise self._nothing_to_invoice_error()
# 2) 管理“grouped”参数:按(partner_id, currency_id)分组。
if not grouped:
new_invoice_vals_list = []
invoice_grouping_keys = self._get_invoice_grouping_keys()
for grouping_keys, invoices in groupby(invoice_vals_list, key=lambda x: [x.get(grouping_key) for grouping_key in invoice_grouping_keys]):
origins = set()
payment_refs = set()
refs = set()
ref_invoice_vals = None
for invoice_vals in invoices:
if not ref_invoice_vals:
ref_invoice_vals = invoice_vals
else:
ref_invoice_vals['invoice_line_ids'] += invoice_vals['invoice_line_ids']
origins.add(invoice_vals['invoice_origin'])
payment_refs.add(invoice_vals['payment_reference'])
refs.add(invoice_vals['ref'])
ref_invoice_vals.update({
'ref': ', '.join(refs)[:2000],
'invoice_origin': ', '.join(origins),
'payment_reference': len(payment_refs) == 1 and payment_refs.pop() or False,
})
new_invoice_vals_list.append(ref_invoice_vals)
invoice_vals_list = new_invoice_vals_list
# 3)创建发票。
# 使用sudo管理发票的创建,因为销售人员必须能够在没有“计费”访问权限的情况下从销售订单生成发票。然而,他不应该能够从头开始创建发票。
moves = self.env['account.move'].sudo().with_context(default_move_type='out_invoice').create(invoice_vals_list)
# 4) 有些动作实际上可能是退款:如果总金额为负,则转换它们
# 我们会在创建交易后进行此操作,因为我们需要税收等数据来了解总金额实际上是否为负数
if final:
moves.sudo().filtered(lambda m: m.amount_total < 0).action_switch_invoice_into_refund_credit_note()
for move in moves:
move.message_post_with_view('mail.message_origin_link',
values={'self': move, 'origin': move.line_ids.mapped('sale_line_ids.order_id')},
subtype_id=self.env.ref('mail.mt_note').id
)
return moves
要创建发票,我们需要以下信息:
-
一个 partner_id:客户
-
一个 move_type:它有几个可能的值
move_type = fields.Selection(selection=[
('entry', 'Journal Entry'),
('out_invoice', 'Customer Invoice'),
('out_refund', 'Customer Credit Note'),
('in_invoice', 'Vendor Bill'),
('in_refund', 'Vendor Credit Note'),
('out_receipt', 'Sales Receipt'),
('in_receipt', 'Purchase Receipt'),
], string='Type', required=True, store=True, index=True, readonly=True, tracking=True,
default="entry", change_default=True)
- 一个 journal_id:会计账簿
锻炼
添加发票创建的第二步。
在 action_sold 方法的覆盖中创建一个空的 account.move:
-
partner_id 来自当前的 estate.property
-
move_type 应与“客户发票”相对应
提示:
-
要创建对象,请使用 self.env[model_name].create(values),其中 values 是一个 dict。
-
create 方法不接受记录集作为字段值。
当某个房产被设置为“已售出”时,您现在应该在“开票/客户/发票”中创建一张新的客户发票。
显然,到目前为止我们还没有任何发票行。要创建发票行,我们需要以下信息:
- name:行描述
- quantity:数量
- price_unit: 价格单位
此外,发票行需要与发票关联。将行与发票关联的最简单和最有效的方法是在创建发票时包含所有行。为此,在创建帐户时包含 invoice_line_ids 字段。One2many 和 Many2many 使用特殊的“命令”,这些命令已通过 Command 命名空间转换为人类可读。此命名空间表示一组记录上执行的三元组命令。三元组最初是执行这些命令的唯一选项,但现在使用命名空间是标准。格式是将它们放在一个按顺序执行的列表中。这里是一个简单的例子,在创建 test_model 时包含 One2many 字段 line_ids。
from odoo import Command
def inherited_action(self):
self.env["test_model"].create(
{
"name": "Test",
"line_ids": [
Command.create({
"field_1": "value_1",
"field_2": "value_2",
})
],
}
)
return super().inherited_action()
锻炼
添加发票创建的第三步。
在创建account.move时添加两条发票行。每个出售的物业都将根据以下条件开具发票:
-
售价的6%
-
行政费用另加100.00
提示:按照上面的示例在创建时添加 invoice_line_ids。对于每一行,我们需要一个名称、数量和价格单位。
这一章可能是迄今为止所涵盖的最难的一章,但它与Odoo开发实践最为接近。在下一章中,我们将介绍Odoo中使用的模板机制