[13.0][IMP] account_move_template, add options to overwirte output lines
This commit is contained in:
parent
36b5238751
commit
9dedd88bf1
@ -17,7 +17,7 @@ class AccountMoveTemplate(models.Model):
|
||||
string="Company",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
default=lambda self: self.env["res.company"]._company_default_get(),
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
journal_id = fields.Many2one("account.journal", string="Journal", required=True)
|
||||
ref = fields.Char(string="Reference", copy=False)
|
||||
@ -159,6 +159,12 @@ class AccountMoveTemplateLine(models.Model):
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
opt_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
string="Account Opt.",
|
||||
domain=[("deprecated", "=", False)],
|
||||
help="When amount is negative, use this account in stead",
|
||||
)
|
||||
|
||||
@api.depends("is_refund", "account_id", "tax_line_id")
|
||||
def _compute_tax_repartition_line_id(self):
|
||||
|
@ -19,3 +19,7 @@ Module Contributors
|
||||
|
||||
* Harald Panten <harald.panten@sygel.es>
|
||||
* Valentin Vinagre <valentin.vinagre@sygel.es>
|
||||
|
||||
* `Ecosoft <http://ecosoft.co.th>`_:
|
||||
|
||||
* Kitti U. <kittiu@ecosoft.co.th> (Add context overwrite)
|
||||
|
@ -7,3 +7,23 @@ the amount of every input lines.
|
||||
|
||||
The journal entry form allows lo load, through a wizard,
|
||||
the template to use and the amounts to fill.
|
||||
|
||||
**Notable features:**
|
||||
|
||||
This module enhance the capability of module account_move_template with following features,
|
||||
|
||||
#. Optional account for negative amount.
|
||||
|
||||
When the Journal entry is created, and credit/debit is negative value, change debit/credit
|
||||
side and use the opt_account_id
|
||||
|
||||
#. Allow overwrite move line values with overwrite dict.
|
||||
|
||||
Normally, the journal items created by the template will require user input on wizard.
|
||||
This feature allow passing the overwrite values with a dictionary.
|
||||
This is particularly useful when the wizard is called by code.
|
||||
|
||||
Sample of dictionary to overwrite move lines::
|
||||
|
||||
{'L1': {'partner_id': 1, 'amount': 100, 'name': 'some label'},
|
||||
'L2': {'partner_id': 2, 'amount': 200, 'name': 'some label 2'}, }
|
||||
|
@ -10,4 +10,5 @@ To use an existing template:
|
||||
|
||||
#. Go to *Invoicing / Accounting / Miscellaneous / Create Entry from Template*
|
||||
#. Select one of the available templates.
|
||||
#. As option, you can overwrite output lines with dict, i.e., {"L1": {"partner": 1}}
|
||||
#. Complete the entries according to the template and click on the button *Generate Journal Entry*.
|
||||
|
@ -1,3 +1,4 @@
|
||||
# Needs to be re-written because wizard.multi.charts.accounts
|
||||
# doesn't exist any more on v12
|
||||
# from . import test_account_move_template
|
||||
from . import test_account_move_template_options
|
||||
|
@ -0,0 +1,186 @@
|
||||
# Copyright 2020 Ecosoft (http://ecosoft.co.th)
|
||||
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import Form, TransactionCase
|
||||
|
||||
|
||||
class TestAccountMoveTemplateEnhanced(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestAccountMoveTemplateEnhanced, self).setUp()
|
||||
self.Move = self.env["account.move"]
|
||||
self.Journal = self.env["account.journal"]
|
||||
self.Account = self.env["account.account"]
|
||||
self.Template = self.env["account.move.template"]
|
||||
self.Partner = self.env["res.partner"]
|
||||
|
||||
self.journal = self.Journal.search([("type", "=", "general")], limit=1)
|
||||
self.ar_account_id = self.Account.search(
|
||||
[("user_type_id.type", "=", "receivable")], limit=1
|
||||
)
|
||||
self.ap_account_id = self.Account.search(
|
||||
[("user_type_id.type", "=", "payable")], limit=1
|
||||
)
|
||||
self.income_account_id = self.Account.search(
|
||||
[
|
||||
("user_type_id.type", "=", "other"),
|
||||
("user_type_id.internal_group", "=", "income"),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
self.expense_account_id = self.Account.search(
|
||||
[
|
||||
("user_type_id.type", "=", "other"),
|
||||
("user_type_id.internal_group", "=", "expense"),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
self.partners = self.Partner.search([], limit=3)
|
||||
|
||||
# Create a simple move tempalte
|
||||
ar_line = {
|
||||
"sequence": 0,
|
||||
"name": "AR Line 1",
|
||||
"account_id": self.ar_account_id.id,
|
||||
"opt_account_id": self.ap_account_id.id,
|
||||
"move_line_type": "dr",
|
||||
"type": "input",
|
||||
}
|
||||
income_line1 = {
|
||||
"sequence": 1,
|
||||
"name": "Income Line 2",
|
||||
"account_id": self.income_account_id.id,
|
||||
"opt_account_id": self.expense_account_id.id,
|
||||
"move_line_type": "cr",
|
||||
"type": "computed",
|
||||
"python_code": "L0*1/3",
|
||||
}
|
||||
income_line2 = {
|
||||
"sequence": 2,
|
||||
"name": "Income Line 2",
|
||||
"account_id": self.income_account_id.id,
|
||||
"opt_account_id": self.expense_account_id.id,
|
||||
"move_line_type": "cr",
|
||||
"type": "computed",
|
||||
"python_code": "L0*2/3",
|
||||
}
|
||||
|
||||
self.move_template = self.Template.create(
|
||||
{
|
||||
"name": "Test Template",
|
||||
"journal_id": self.journal.id,
|
||||
"line_ids": [
|
||||
(0, 0, ar_line),
|
||||
(0, 0, income_line1),
|
||||
(0, 0, income_line2),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def test_move_template_normal(self):
|
||||
""" Test normal case, input amount 300 """
|
||||
with Form(self.env["account.move.template.run"]) as f:
|
||||
f.template_id = self.move_template
|
||||
template_run = f.save()
|
||||
template_run.load_lines()
|
||||
template_run.line_ids[0].amount = 300
|
||||
res = template_run.generate_move()
|
||||
move = self.Move.browse(res["res_id"])
|
||||
self.assertRecordValues(
|
||||
move.line_ids.sorted("credit"),
|
||||
[
|
||||
{"account_id": self.ar_account_id.id, "credit": 0.0, "debit": 300.0},
|
||||
{
|
||||
"account_id": self.income_account_id.id,
|
||||
"credit": 100.0,
|
||||
"debit": 0.0,
|
||||
},
|
||||
{
|
||||
"account_id": self.income_account_id.id,
|
||||
"credit": 200.0,
|
||||
"debit": 0.0,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_move_template_optional(self):
|
||||
""" Test optional case, input amount -300, expect optional account """
|
||||
with Form(self.env["account.move.template.run"]) as f:
|
||||
f.template_id = self.move_template
|
||||
template_run = f.save()
|
||||
template_run.load_lines()
|
||||
template_run.line_ids[0].amount = -300 # Negative amount
|
||||
res = template_run.generate_move()
|
||||
move = self.Move.browse(res["res_id"])
|
||||
self.assertRecordValues(
|
||||
move.line_ids.sorted("debit"),
|
||||
[
|
||||
{"account_id": self.ap_account_id.id, "credit": 300.0, "debit": 0.0},
|
||||
{
|
||||
"account_id": self.expense_account_id.id,
|
||||
"credit": 0.0,
|
||||
"debit": 100.0,
|
||||
},
|
||||
{
|
||||
"account_id": self.expense_account_id.id,
|
||||
"credit": 0.0,
|
||||
"debit": 200.0,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_move_template_overwrite(self):
|
||||
""" Test case overwrite, amount = 3000, no need to manual input """
|
||||
# Test for error when debit is not a valid field
|
||||
with Form(self.env["account.move.template.run"]) as f:
|
||||
f.template_id = self.move_template
|
||||
f.overwrite = str(
|
||||
{
|
||||
"L0": {
|
||||
"partner_id": self.partners[0].id,
|
||||
"amount": 3000,
|
||||
"debit": 3000,
|
||||
},
|
||||
}
|
||||
)
|
||||
template_run = f.save()
|
||||
with self.assertRaises(ValidationError):
|
||||
template_run.load_lines()
|
||||
# Assign only on valid fields, and load_lines again
|
||||
with Form(self.env["account.move.template.run"]) as f:
|
||||
f.template_id = self.move_template
|
||||
f.overwrite = str(
|
||||
{
|
||||
"L0": {"partner_id": self.partners[0].id, "amount": 3000},
|
||||
"L1": {"partner_id": self.partners[1].id},
|
||||
"L2": {"partner_id": self.partners[2].id},
|
||||
}
|
||||
)
|
||||
template_run = f.save()
|
||||
res = template_run.load_lines()
|
||||
self.assertEqual(template_run.line_ids[0].partner_id, self.partners[0])
|
||||
self.assertEqual(template_run.line_ids[0].amount, 3000)
|
||||
res = template_run.with_context(res["context"]).generate_move()
|
||||
move = self.Move.browse(res["res_id"])
|
||||
self.assertRecordValues(
|
||||
move.line_ids.sorted("credit"),
|
||||
[
|
||||
{
|
||||
"partner_id": self.partners[0].id,
|
||||
"account_id": self.ar_account_id.id,
|
||||
"credit": 0.0,
|
||||
"debit": 3000.0,
|
||||
},
|
||||
{
|
||||
"partner_id": self.partners[1].id,
|
||||
"account_id": self.income_account_id.id,
|
||||
"credit": 1000.0,
|
||||
"debit": 0.0,
|
||||
},
|
||||
{
|
||||
"partner_id": self.partners[2].id,
|
||||
"account_id": self.income_account_id.id,
|
||||
"credit": 2000.0,
|
||||
"debit": 0.0,
|
||||
},
|
||||
],
|
||||
)
|
@ -7,6 +7,10 @@
|
||||
<tree>
|
||||
<field name="sequence" />
|
||||
<field name="account_id" domain="[('company_id', '=', company_id)]" />
|
||||
<field
|
||||
name="opt_account_id"
|
||||
domain="[('company_id', '=', company_id)]"
|
||||
/>
|
||||
<field name="partner_id" />
|
||||
<field name="name" />
|
||||
<field
|
||||
@ -47,6 +51,10 @@
|
||||
name="account_id"
|
||||
domain="[('company_id', '=', company_id)]"
|
||||
/>
|
||||
<field
|
||||
name="opt_account_id"
|
||||
domain="[('company_id', '=', company_id)]"
|
||||
/>
|
||||
<field name="partner_id" />
|
||||
<field name="payment_term_id" />
|
||||
<field name="company_id" invisible="1" />
|
||||
|
@ -1,8 +1,9 @@
|
||||
# Copyright 2015-2019 See manifest
|
||||
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
@ -15,7 +16,7 @@ class AccountMoveTemplateRun(models.TransientModel):
|
||||
"res.company",
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=lambda self: self.env["res.company"]._company_default_get(),
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
"res.partner",
|
||||
@ -33,6 +34,13 @@ class AccountMoveTemplateRun(models.TransientModel):
|
||||
readonly=True,
|
||||
default="select_template",
|
||||
)
|
||||
overwrite = fields.Text(
|
||||
help="""
|
||||
Valid dictionary to overwrite template lines:
|
||||
{'L1': {'partner_id': 1, 'amount': 100, 'name': 'some label'},
|
||||
'L2': {'partner_id': 2, 'amount': 200, 'name': 'some label 2'}, }
|
||||
"""
|
||||
)
|
||||
|
||||
def _prepare_wizard_line(self, tmpl_line):
|
||||
vals = {
|
||||
@ -57,6 +65,8 @@ class AccountMoveTemplateRun(models.TransientModel):
|
||||
# STEP 1
|
||||
def load_lines(self):
|
||||
self.ensure_one()
|
||||
# Verify and get overwrite dict
|
||||
overwrite_vals = self._get_overwrite_vals()
|
||||
amtlro = self.env["account.move.template.line.run"]
|
||||
if self.company_id != self.template_id.company_id:
|
||||
raise UserError(
|
||||
@ -86,8 +96,75 @@ class AccountMoveTemplateRun(models.TransientModel):
|
||||
action = self.env.ref("account_move_template.account_move_template_run_action")
|
||||
result = action.read()[0]
|
||||
result.update({"res_id": self.id, "context": self.env.context})
|
||||
|
||||
# Overwrite self.line_ids to show overwrite values
|
||||
self._overwrite_line(overwrite_vals)
|
||||
# Pass context furtner to generate_move function, only readonly field
|
||||
for key in overwrite_vals.keys():
|
||||
overwrite_vals[key].pop("amount", None)
|
||||
context = result.get("context", {}).copy()
|
||||
context.update({"overwrite": overwrite_vals})
|
||||
result["context"] = context
|
||||
return result
|
||||
|
||||
def _get_valid_keys(self):
|
||||
return ["partner_id", "amount", "name", "date_maturity"]
|
||||
|
||||
def _get_overwrite_vals(self):
|
||||
""" valid_dict = {
|
||||
'L1': {'partner_id': 1, 'amount': 10},
|
||||
'L2': {'partner_id': 2, 'amount': 20},
|
||||
}
|
||||
"""
|
||||
self.ensure_one()
|
||||
valid_keys = self._get_valid_keys()
|
||||
overwrite_vals = self.overwrite or "{}"
|
||||
try:
|
||||
overwrite_vals = literal_eval(overwrite_vals)
|
||||
assert isinstance(overwrite_vals, dict)
|
||||
except (SyntaxError, ValueError, AssertionError):
|
||||
raise ValidationError(_("Overwrite value must be a valid python dict"))
|
||||
# First level keys must be L1, L2, ...
|
||||
keys = overwrite_vals.keys()
|
||||
if list(filter(lambda x: x[:1] != "L" or not x[1:].isdigit(), keys)):
|
||||
raise ValidationError(_("Keys must be line sequence, i..e, L1, L2, ..."))
|
||||
# Second level keys must be a valid keys
|
||||
try:
|
||||
if dict(
|
||||
filter(lambda x: set(overwrite_vals[x].keys()) - set(valid_keys), keys)
|
||||
):
|
||||
raise ValidationError(
|
||||
_("Valid fields to overwrite are %s") % valid_keys
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
msg = """
|
||||
valid_dict = {
|
||||
'L1': {'partner_id': 1, 'amount': 10},
|
||||
'L2': {'partner_id': 2, 'amount': 20},
|
||||
}
|
||||
"""
|
||||
raise ValidationError(_("Invalid dictionary: {}\n{}".format(e, msg)))
|
||||
return overwrite_vals
|
||||
|
||||
def _safe_vals(self, model, vals):
|
||||
obj = self.env[model]
|
||||
copy_vals = vals.copy()
|
||||
invalid_keys = list(
|
||||
set(list(vals.keys())) - set(list(dict(obj._fields).keys()))
|
||||
)
|
||||
for key in invalid_keys:
|
||||
copy_vals.pop(key)
|
||||
return copy_vals
|
||||
|
||||
def _overwrite_line(self, overwrite_vals):
|
||||
self.ensure_one()
|
||||
for line in self.line_ids:
|
||||
vals = overwrite_vals.get("L{}".format(line.sequence), {})
|
||||
safe_vals = self._safe_vals(line._name, vals)
|
||||
line.write(safe_vals)
|
||||
|
||||
# STEP 2
|
||||
def generate_move(self):
|
||||
self.ensure_one()
|
||||
@ -138,7 +215,7 @@ class AccountMoveTemplateRun(models.TransientModel):
|
||||
def _prepare_move_line(self, line, amount):
|
||||
date_maturity = False
|
||||
if line.payment_term_id:
|
||||
pterm_list = line.payment_term_id.compute(value=1, date_ref=self.date)[0]
|
||||
pterm_list = line.payment_term_id.compute(value=1, date_ref=self.date)
|
||||
date_maturity = max(l[0] for l in pterm_list)
|
||||
debit = line.move_line_type == "dr"
|
||||
values = {
|
||||
@ -165,8 +242,24 @@ class AccountMoveTemplateRun(models.TransientModel):
|
||||
values["tag_ids"] = [(6, 0, atrl_ids.mapped("tag_ids").ids)]
|
||||
if line.tax_repartition_line_id:
|
||||
values["tag_ids"] = [(6, 0, line.tax_repartition_line_id.tag_ids.ids)]
|
||||
# With overwrite options
|
||||
overwrite = self._context.get("overwrite", {})
|
||||
move_line_vals = overwrite.get("L{}".format(line.sequence), {})
|
||||
values.update(move_line_vals)
|
||||
# Use optional account, when amount is negative
|
||||
self._update_account_on_negative(line, values)
|
||||
return values
|
||||
|
||||
def _update_account_on_negative(self, line, vals):
|
||||
if not line.opt_account_id:
|
||||
return
|
||||
for key in ["debit", "credit"]:
|
||||
if vals[key] < 0:
|
||||
ikey = (key == "debit") and "credit" or "debit"
|
||||
vals["account_id"] = line.opt_account_id.id
|
||||
vals[ikey] = abs(vals[key])
|
||||
vals[key] = 0
|
||||
|
||||
|
||||
class AccountMoveTemplateLineRun(models.TransientModel):
|
||||
_name = "account.move.template.line.run"
|
||||
|
@ -12,6 +12,13 @@
|
||||
domain="[('company_id', '=', company_id)]"
|
||||
attrs="{'readonly': [('state', '=', 'set_lines')]}"
|
||||
/>
|
||||
<field
|
||||
name="overwrite"
|
||||
widget="ace"
|
||||
options="{'mode': 'python'}"
|
||||
attrs="{'invisible': [('state', '=', 'set_lines')]}"
|
||||
placeholder="Add an internal note here..."
|
||||
/>
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="date" states="set_lines" />
|
||||
<field name="journal_id" states="set_lines" />
|
||||
|
Loading…
Reference in New Issue
Block a user