line: quantity_balance, validate,

reconciliation: start/end-quantity
todo: tests
This commit is contained in:
Frederik Jaeckel 2022-12-31 16:15:23 +01:00
parent e94a869166
commit f86db6dea3
11 changed files with 484 additions and 140 deletions

View file

@ -7,10 +7,12 @@ from trytond.pool import Pool
from .types import Type
from .book import Book
from .line import Line
from .reconciliation import Reconciliation
def register():
Pool.register(
Type,
Book,
Line,
Reconciliation,
module='cashbook_investment', type_='model')

70
line.py
View file

@ -7,6 +7,8 @@ from decimal import Decimal
from trytond.model import fields
from trytond.pool import PoolMeta
from trytond.pyson import Eval, Or, If
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.modules.cashbook.line import STATES, DEPENDS
STATESQ = {
@ -34,11 +36,17 @@ class Line(metaclass=PoolMeta):
digits=(16, Eval('quantity_digits', 4)),
states=STATESQ, depends=DEPENDSQ)
quantity_credit = fields.Numeric(string='Quantity Credit',
digits=(16, Eval('quantity_digits', 4)),
states=STATESQ, depends=DEPENDSQ)
digits=(16, Eval('quantity_digits', 4)), readonly=True,
states={
'invisible': STATESQ['invisible'],
'required': STATESQ['required'],
}, depends=DEPENDSQ)
quantity_debit = fields.Numeric(string='Quantity Debit',
digits=(16, Eval('quantity_digits', 4)),
states=STATESQ, depends=DEPENDSQ)
digits=(16, Eval('quantity_digits', 4)), readonly=True,
states={
'invisible': STATESQ['invisible'],
'required': STATESQ['required'],
}, depends=DEPENDSQ)
quantity_digits = fields.Function(fields.Integer(string='Digits',
readonly=True, states={'invisible': True}),
@ -55,6 +63,11 @@ class Line(metaclass=PoolMeta):
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'quantity_digits', 'feature']),
'on_change_with_asset_rate')
quantity_balance = fields.Function(fields.Numeric(string='Quantity',
digits=(16, Eval('quantity_digits', 4)),
help='Number of shares in the cashbook up to the current row if the default sort applies.',
readonly=True, depends=['quantity_digits']),
'on_change_with_quantity_balance')
@classmethod
def get_debit_credit(cls, values):
@ -123,4 +136,53 @@ class Line(metaclass=PoolMeta):
return self.cashbook.quantity_digits
return 4
@classmethod
def validate(cls, lines):
""" deny pos/neg mismatch
"""
super(Line, cls).validate(lines)
for line in lines:
# ignore non-asset-lines
if line.cashbook.feature != 'asset':
continue
# quantity must be set
if (line.quantity is None) or \
(line.quantity_credit is None) or \
(line.quantity_debit is None):
raise UserError(gettext(
'cashbook_investment.msg_line_quantity_not_set',
linetxt = line.rec_name,
))
# quantity and amount must with same sign
(amount_sign, a_dig, a_exp) = line.amount.as_tuple()
(quantity_sign, q_dig, q_exp) = line.quantity.as_tuple()
if amount_sign != quantity_sign:
raise UserError(gettext(
'cashbook_investment.msg_line_sign_mismatch',
linetxt = line.rec_name,
))
@classmethod
def write(cls, *args):
""" add or update quanity_debit/credit
"""
actions = iter(args)
to_write = []
for lines, values in zip(actions, actions):
# update debit / credit
if len(set(values.keys()).intersection(set({'quantity', 'bookingtype'}))) > 0:
for line in lines:
values2 = {}
values2.update(values)
values2.update(cls.get_debit_credit({
x:values.get(x, getattr(line, x)) for x in ['quantity', 'bookingtype']
}))
to_write.extend([lines, values2])
else :
to_write.extend([lines, values])
super(Line, cls).write(*to_write)
# end LineContext

View file

@ -10,6 +10,14 @@ msgctxt "model:ir.message,text:msg_btype_asset"
msgid "Asset"
msgstr "Vermögenswert"
msgctxt "model:ir.message,text:msg_line_quantity_not_set"
msgid "Quantity must be set for line '%(linetxt)s'."
msgstr "Menge für die Zeile '%(linetxt)s' muß gesetzt werden."
msgctxt "model:ir.message,text:msg_line_sign_mismatch"
msgid "Quantity and Amount must with same sign for line %(linetxt)s."
msgstr "Menge und Betrag müssen für Zeile %(linetxt)s mit demselben Vorzeichen versehen werden."
#################
# cashbook.book #
@ -124,11 +132,11 @@ msgstr "Anzahl"
msgctxt "field:cashbook.line,quantity_credit:"
msgid "Quantity Credit"
msgstr "Anzahlgutschrift"
msgstr "Anzahl Haben"
msgctxt "field:cashbook.line,quantity_debit:"
msgid "Quantity Debit"
msgstr "Anzahllastschrift"
msgstr "Anzahl Soll"
msgctxt "field:cashbook.line,quantity_symbol:"
msgid "Symbol"
@ -137,3 +145,31 @@ msgstr "Symbol"
msgctxt "field:cashbook.line,asset_rate:"
msgid "Rate"
msgstr "Kurs"
msgctxt "field:cashbook.line,quantity_balance:"
msgid "Quantity"
msgstr "Anzahl"
msgctxt "help:cashbook.line,quantity_balance:"
msgid "Number of shares in the cashbook up to the current row if the default sort applies."
msgstr "Anzahl Anteile im Kassenbuch bis zur aktuellen Zeile, wenn die Standardsortierung zutrifft."
##################
# cashbook.recon #
##################
msgctxt "field:cashbook.recon,start_quantity:"
msgid "Start Quantity"
msgstr "Anfangsmenge"
msgctxt "field:cashbook.recon,end_quantity:"
msgid "End Quantity"
msgstr "Endmenge"
msgctxt "field:cashbook.recon,quantity_digits:"
msgid "Quantity Digits"
msgstr "Anzahl-Dezimalstellen"
msgctxt "field:cashbook.recon,quantity_uom:"
msgid "Symbol"
msgstr "Symbol"

View file

@ -6,6 +6,14 @@ msgctxt "model:ir.message,text:msg_btype_asset"
msgid "Asset"
msgstr "Asset"
msgctxt "model:ir.message,text:msg_line_quantity_not_set"
msgid "Quantity must be set for line '%(linetxt)s'."
msgstr "Quantity must be set for line '%(linetxt)s'."
msgctxt "model:ir.message,text:msg_line_sign_mismatch"
msgid "Quantity and Amount must with same sign for line %(linetxt)s."
msgstr "Quantity and Amount must with same sign for line %(linetxt)s."
msgctxt "view:cashbook.book:"
msgid "Asset"
msgstr "Asset"
@ -122,3 +130,27 @@ msgctxt "field:cashbook.line,quantity_symbol:"
msgid "Symbol"
msgstr "Symbol"
msgctxt "field:cashbook.line,asset_rate:"
msgid "Rate"
msgstr "Rate"
msgctxt "field:cashbook.line,quantity_balance:"
msgid "Quantity"
msgstr "Quantity"
msgctxt "help:cashbook.line,quantity_balance:"
msgid "Number of shares in the cashbook up to the current row if the default sort applies."
msgstr "Number of shares in the cashbook up to the current row if the default sort applies."
msgctxt "field:cashbook.recon,start_quantity:"
msgid "Start Quantity"
msgstr "Start Quantity"
msgctxt "field:cashbook.recon,end_quantity:"
msgid "End Quantity"
msgstr "End Quantity"
msgctxt "field:cashbook.recon,quantity_digits:"
msgid "Quantity Digits"
msgstr "Quantity Digits"

View file

@ -8,6 +8,12 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_btype_asset">
<field name="text">Asset</field>
</record>
<record model="ir.message" id="msg_line_quantity_not_set">
<field name="text">Quantity must be set for line '%(linetxt)s'.</field>
</record>
<record model="ir.message" id="msg_line_sign_mismatch">
<field name="text">Quantity and Amount must with same sign for line %(linetxt)s.</field>
</record>
</data>
</tryton>

100
reconciliation.py Normal file
View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.pool import PoolMeta
from trytond.model import fields
from trytond.pyson import Eval
from decimal import Decimal
class Reconciliation(metaclass=PoolMeta):
__name__ = 'cashbook.recon'
start_quantity = fields.Numeric(string='Start Quantity',
readonly=True, digits=(16, Eval('quantity_digits', 4)),
states={
'required': Eval('feature', '') == 'asset',
'invisible': Eval('feature', '') != 'asset',
}, depends=['quantity_digits', 'feature'])
end_quantity = fields.Numeric(string='End Quantity',
readonly=True, digits=(16, Eval('quantity_digits', 4)),
states={
'required': Eval('feature', '') == 'asset',
'invisible': Eval('feature', '') != 'asset',
}, depends=['quantity_digits', 'feature'])
quantity_digits = fields.Function(fields.Integer(string='Quantity Digits'),
'on_change_with_quantity_digits')
quantity_uom = fields.Function(fields.Many2One(string='Symbol',
readonly=True, model_name='product.uom'),
'on_change_with_quantity_uom')
@fields.depends('cashbook', '_parent_cashbook.quantity_uom')
def on_change_with_quantity_uom(self, name=None):
""" get quantity-unit of asset
"""
if self.cashbook:
if self.cashbook.quantity_uom:
return self.cashbook.quantity_uom.id
@fields.depends('cashbook', '_parent_cashbook.quantity_digits')
def on_change_with_quantity_digits(self, name=None):
""" quantity_digits of cashbook
"""
if self.cashbook:
return self.cashbook.currency.digits
else:
return 4
@classmethod
def default_start_quantity(cls):
return Decimal('0.0')
@classmethod
def default_end_quantity(cls):
return Decimal('0.0')
@classmethod
def get_values_wfedit(cls, reconciliation):
""" get values for 'to_write' in wf-edit
"""
values = super(Reconciliation, cls).get_values_wfedit(reconciliation)
values.update({
'start_quantity': Decimal('0.0'),
'end_quantity': Decimal('0.0'),
})
return values
@classmethod
def get_values_wfcheck(cls, reconciliation):
""" get values for 'to_write' in wf-check
"""
values = super(Reconciliation, cls).get_values_wfcheck(reconciliation)
if reconciliation.predecessor:
values['start_quantity'] = reconciliation.predecessor.end_quantity
else :
values['start_quantity'] = Decimal('0.0')
values['end_quantity'] = values['start_quantity']
# add quantities of new lines
if 'lines' in values.keys():
if len(values['lines']) != 1:
raise ValueError('invalid number of values')
values['end_quantity'] += sum([
x.quantity_credit - x.quantity_debit
for x in values['lines'][0][1]
])
# add quantities of already linked lines
values['end_quantity'] += sum([
x.quantity_credit - x.quantity_debit
for x in reconciliation.lines
])
return values
# end Reconciliation

15
reconciliation.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="recon_view_form">
<field name="model">cashbook.recon</field>
<field name="inherit" ref="cashbook.recon_view_form"/>
<field name="name">recon_form</field>
</record>
</data>
</tryton>

View file

@ -531,10 +531,22 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase):
self.assertEqual(book.rec_name, 'Book 1 | -1.00 usd | Open')
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.lines[0].quantity, Decimal('1.5'))
self.assertEqual(book.lines[0].quantity_credit, Decimal('0.0'))
self.assertEqual(book.lines[0].quantity_debit, Decimal('1.5'))
self.assertEqual(len(book2.lines), 0)
self.assertEqual(book.lines[0].rec_name, '05/01/2022|to|-1.00 usd|Transfer Out [Asset-Book | 0.00 usd | Open]')
self.assertEqual(len(book.lines[0].references), 0)
# update quantity
Book.write(*[
[book],
{
'lines': [('write', [book.lines[0]], {'quantity': Decimal('2.5')})],
}])
self.assertEqual(book.lines[0].quantity, Decimal('2.5'))
self.assertEqual(book.lines[0].quantity_credit, Decimal('0.0'))
self.assertEqual(book.lines[0].quantity_debit, Decimal('2.5'))
# check counterpart
self.assertEqual(book.lines[0].booktransf.rec_name, 'Asset-Book | 0.00 usd | Open')
self.assertEqual(book.lines[0].booktransf.btype.feature, 'asset')
@ -547,9 +559,9 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase):
self.assertEqual(book.lines[0].rec_name, '05/01/2022|to|-1.00 usd|Transfer Out [Asset-Book | 1.00 usd | Open]')
self.assertEqual(book.lines[0].state, 'check')
self.assertEqual(book.lines[0].bookingtype, 'mvout')
self.assertEqual(book.lines[0].quantity, Decimal('1.5'))
self.assertEqual(book.lines[0].quantity, Decimal('2.5'))
self.assertEqual(book.lines[0].quantity_credit, Decimal('0.0'))
self.assertEqual(book.lines[0].quantity_debit, Decimal('1.5'))
self.assertEqual(book.lines[0].quantity_debit, Decimal('2.5'))
self.assertEqual(len(book.lines[0].references), 1)
self.assertEqual(book.lines[0].reference, None)
self.assertEqual(book.lines[0].references[0].id, book2.lines[0].id)
@ -557,12 +569,78 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase):
self.assertEqual(len(book2.lines), 1)
self.assertEqual(book2.lines[0].rec_name, '05/01/2022|from|1.00 usd|Transfer Out [Book 1 | -1.00 usd | Open]')
self.assertEqual(book2.lines[0].state, 'check')
self.assertEqual(book2.lines[0].quantity, Decimal('1.5'))
self.assertEqual(book2.lines[0].quantity_credit, Decimal('1.5'))
self.assertEqual(book2.lines[0].quantity, Decimal('2.5'))
self.assertEqual(book2.lines[0].quantity_credit, Decimal('2.5'))
self.assertEqual(book2.lines[0].quantity_debit, Decimal('0.0'))
self.assertEqual(book2.lines[0].bookingtype, 'mvin')
self.assertEqual(book2.lines[0].asset_rate, Decimal('0.6667'))
self.assertEqual(book2.lines[0].asset_rate, Decimal('0.4'))
self.assertEqual(book2.lines[0].reference.rec_name, '05/01/2022|to|-1.00 usd|Transfer Out [Asset-Book | 1.00 usd | Open]')
self.assertEqual(len(book2.lines[0].references), 0)
@with_transaction()
def test_assetbook_check_sign_mismatch(self):
""" create cashbook + line, bookingtype 'in',
check detection of sign mismatch between quantity and amount
"""
pool = Pool()
Book = pool.get('cashbook.book')
Line = pool.get('cashbook.line')
Category = pool.get('cashbook.category')
BType = pool.get('cashbook.type')
type_depot = self.prep_type('Depot', 'D')
BType.write(*[
[type_depot],
{
'feature': 'asset',
}])
category_in = self.prep_category(cattype='in')
company = self.prep_company()
party = self.prep_party()
asset = self.prep_asset_item(
company=company,
product = self.prep_asset_product(name='Product 1'))
self.assertEqual(asset.symbol, 'usd/u')
book, = Book.create([{
'name': 'Asset-Book',
'btype': type_depot.id,
'asset': asset.id,
'quantity_uom': asset.uom.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'buy some',
'category': category_in.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'quantity': Decimal('1.5'),
}])],
}])
self.assertEqual(book.rec_name, 'Asset-Book | 1.00 usd | Open')
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.lines[0].amount, Decimal('1.0'))
self.assertEqual(book.lines[0].credit, Decimal('1.0'))
self.assertEqual(book.lines[0].debit, Decimal('0.0'))
self.assertEqual(book.lines[0].quantity, Decimal('1.5'))
self.assertEqual(book.lines[0].quantity_credit, Decimal('1.5'))
self.assertEqual(book.lines[0].quantity_debit, Decimal('0.0'))
self.assertRaisesRegex(UserError,
"Quantity and Amount must with same sign for line 05/01/2022|Rev|1.00 usd|buy some [Cat1].",
Book.write,
*[
[book],
{
'lines': [('write', [book.lines[0]], {
'quantity': Decimal('-1.5'),
})],
}])
# end CbInvTestCase

View file

@ -7,3 +7,4 @@ xml:
message.xml
book.xml
line.xml
reconciliation.xml

View file

@ -9,11 +9,7 @@ full copyright notices and license terms. -->
<field name="quantity" symbol="quantity_uom"/>
<label name="asset_rate"/>
<field name="asset_rate" symbol="cashbook"/>
<label name="quantity_credit" />
<field name="quantity_credit" symbol="quantity_uom"/>
<label name="quantity_debit"/>
<field name="quantity_debit" symbol="quantity_uom"/>
<newline/>
<field name="quantity_digits"/>
<newline/>

16
view/recon_form.xml Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<data>
<xpath expr="/form/field[@name='end_amount']" position="after">
<newline/>
<label name="start_quantity" />
<field name="start_quantity" symbol="quantity_uom"/>
<label name="end_quantity"/>
<field name="end_quantity" symbol="quantity_uom"/>
</xpath>
</data>