Go to matching
This commit is contained in:
@@ -1328,8 +1328,13 @@ class LotQt(
|
|||||||
qt_p = Decimal(str(lp.lot_matched_qt or 0))
|
qt_p = Decimal(str(lp.lot_matched_qt or 0))
|
||||||
if qt_p <= 0:
|
if qt_p <= 0:
|
||||||
continue
|
continue
|
||||||
lqt = LotQt(lp.lot_r_id)
|
lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None
|
||||||
available = cls._matching_tolerated_available(lqt, 'purchase')
|
if lqt:
|
||||||
|
available = cls._matching_tolerated_available(lqt, 'purchase')
|
||||||
|
else:
|
||||||
|
lot = Lot(lp.lot_id)
|
||||||
|
available = Decimal(str(
|
||||||
|
lot.get_current_quantity_converted() or 0))
|
||||||
if qt_p > available:
|
if qt_p > available:
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"Quantity to match exceeds the available purchase "
|
"Quantity to match exceeds the available purchase "
|
||||||
@@ -1348,8 +1353,8 @@ class LotQt(
|
|||||||
qt_p = lp.lot_matched_qt
|
qt_p = lp.lot_matched_qt
|
||||||
if qt_p == 0:
|
if qt_p == 0:
|
||||||
continue
|
continue
|
||||||
lqt = LotQt(lp.lot_r_id)
|
lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None
|
||||||
if not lqt.lot_p or lqt.lot_s:
|
if lqt and (not lqt.lot_p or lqt.lot_s):
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"Please select only unmatched purchase quantities.")
|
"Please select only unmatched purchase quantities.")
|
||||||
for ls in lot_s:
|
for ls in lot_s:
|
||||||
@@ -1368,28 +1373,29 @@ class LotQt(
|
|||||||
raise UserError(
|
raise UserError(
|
||||||
"Please select only unmatched sale quantities.")
|
"Please select only unmatched sale quantities.")
|
||||||
if l.lot_type == 'physic':
|
if l.lot_type == 'physic':
|
||||||
Sale = Pool().get('sale.sale')
|
ll = lqt2.lot_s
|
||||||
s = Sale(ls.lot_sale)
|
#Decrease forecasted virtual parts matched
|
||||||
ll = s.lines[0].lots[0]
|
vlot_p = l.getVlot_p()
|
||||||
#Decrease forecasted virtual parts matched
|
vlot_s = ll.getVlot_s()
|
||||||
vlot_p = l.getVlot_p()
|
logger.info("SALEVIRTUALLOT:%s",vlot_p)
|
||||||
vlot_s = ll.getVlot_s()
|
logger.info("SALEVIRTUALLOT2:%s",vlot_s)
|
||||||
logger.info("SALEVIRTUALLOT:%s",vlot_p)
|
|
||||||
logger.info("SALEVIRTUALLOT2:%s",vlot_s)
|
|
||||||
#l.updateVirtualPart(-qt,l.lot_shipment_origin,l.getVlot_s())
|
#l.updateVirtualPart(-qt,l.lot_shipment_origin,l.getVlot_s())
|
||||||
#Increase forecasted virtual part non matched
|
#Increase forecasted virtual part non matched
|
||||||
#if not l.updateVirtualPart(qt,l.lot_shipment_origin,None):
|
#if not l.updateVirtualPart(qt,l.lot_shipment_origin,None):
|
||||||
# l.createVirtualPart(qt,l.lot_shipment_origin,None)
|
# l.createVirtualPart(qt,l.lot_shipment_origin,None)
|
||||||
l.updateVirtualPart(-qt,None,ll,'only sale')
|
l.updateVirtualPart(-qt,None,ll,'only sale')
|
||||||
lqt.lot_s = ll
|
if lqt:
|
||||||
lqt.lot_av = 'reserved'
|
lqt.lot_s = ll
|
||||||
LotQt.save([lqt])
|
lqt.lot_av = 'reserved'
|
||||||
l.sale_line = s.lines[0]
|
LotQt.save([lqt])
|
||||||
Lot.save([l])
|
else:
|
||||||
#ll.lot_gross_quantity -= qt
|
l.createVirtualPart(qt, None, ll)
|
||||||
ll.lot_quantity -= qt
|
l.sale_line = ll.sale_line
|
||||||
ll.lot_qt -= float(qt)
|
Lot.save([l])
|
||||||
Lot.save([ll])
|
#ll.lot_gross_quantity -= qt
|
||||||
|
ll.lot_quantity -= qt
|
||||||
|
ll.lot_qt -= float(qt)
|
||||||
|
Lot.save([ll])
|
||||||
else:
|
else:
|
||||||
#Increase forecasted virtual part matched
|
#Increase forecasted virtual part matched
|
||||||
if not l.updateVirtualPart(qt,lqt.lot_shipment_origin,lqt2.lot_s):
|
if not l.updateVirtualPart(qt,lqt.lot_shipment_origin,lqt2.lot_s):
|
||||||
@@ -2938,13 +2944,45 @@ class LotMatching(Wizard):
|
|||||||
values.update(LotQt._matching_line_tolerance_info(trade_line))
|
values.update(LotQt._matching_line_tolerance_info(trade_line))
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _physical_purchase_matching_value(cls, lot):
|
||||||
|
line = getattr(lot, 'line', None)
|
||||||
|
purchase = getattr(line, 'purchase', None)
|
||||||
|
quantity = Decimal(str(lot.get_current_quantity_converted() or 0))
|
||||||
|
values = {
|
||||||
|
'lot_id': cls._record_id(lot),
|
||||||
|
'lot_r_id': None,
|
||||||
|
'lot_purchase': cls._record_id(purchase),
|
||||||
|
'lot_sale': None,
|
||||||
|
'lot_shipment_in': cls._record_id(
|
||||||
|
getattr(lot, 'lot_shipment_in', None)),
|
||||||
|
'lot_shipment_internal': cls._record_id(
|
||||||
|
getattr(lot, 'lot_shipment_internal', None)),
|
||||||
|
'lot_shipment_out': cls._record_id(
|
||||||
|
getattr(lot, 'lot_shipment_out', None)),
|
||||||
|
'lot_type': getattr(lot, 'lot_type', None),
|
||||||
|
'lot_product': cls._record_id(getattr(lot, 'lot_product', None)),
|
||||||
|
'lot_quantity': quantity,
|
||||||
|
'lot_matched_qt': 0,
|
||||||
|
'lot_already_matched_qt': 0,
|
||||||
|
'lot_cp': cls._record_id(getattr(purchase, 'party', None)),
|
||||||
|
}
|
||||||
|
values.update(LotQt._matching_line_tolerance_info(line))
|
||||||
|
return values
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _selected_matching_defaults(cls, active_ids):
|
def _selected_matching_defaults(cls, active_ids):
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
|
Lot = Pool().get('lot.lot')
|
||||||
lot_p = []
|
lot_p = []
|
||||||
lot_s = []
|
lot_s = []
|
||||||
for active_id in active_ids or []:
|
for active_id in active_ids or []:
|
||||||
if active_id <= 10000000:
|
if active_id <= 10000000:
|
||||||
|
lot = Lot(active_id)
|
||||||
|
if (getattr(lot, 'lot_type', None) == 'physic'
|
||||||
|
and getattr(lot, 'line', None)
|
||||||
|
and not getattr(lot, 'sale_line', None)):
|
||||||
|
lot_p.append(cls._physical_purchase_matching_value(lot))
|
||||||
continue
|
continue
|
||||||
lqt = LotQt(active_id - 10000000)
|
lqt = LotQt(active_id - 10000000)
|
||||||
if lqt.lot_p and lqt.lot_s:
|
if lqt.lot_p and lqt.lot_s:
|
||||||
@@ -2961,12 +2999,12 @@ class LotMatching(Wizard):
|
|||||||
def transition_matching(self):
|
def transition_matching(self):
|
||||||
Warning = Pool().get('res.user.warning')
|
Warning = Pool().get('res.user.warning')
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
|
Lot = Pool().get('lot.lot')
|
||||||
if self.match.tot_p != self.match.tot_s:
|
if self.match.tot_p != self.match.tot_s:
|
||||||
warning_name = Warning.format("Quantity's issue", [])
|
warning_name = Warning.format("Quantity's issue", [])
|
||||||
if Warning.check(warning_name):
|
if Warning.check(warning_name):
|
||||||
raise QtWarning(warning_name,
|
raise QtWarning(warning_name,
|
||||||
"Quantities not compatibles")
|
"Quantities not compatibles")
|
||||||
Lot = Pool().get('lot.lot')
|
|
||||||
with Lot.skip_quantity_consistency():
|
with Lot.skip_quantity_consistency():
|
||||||
LotQt.match_lots(self.match.lot_p,self.match.lot_s)
|
LotQt.match_lots(self.match.lot_p,self.match.lot_s)
|
||||||
affected_lines = []
|
affected_lines = []
|
||||||
@@ -3011,10 +3049,18 @@ class LotGoMatching(Wizard):
|
|||||||
context = Transaction().context
|
context = Transaction().context
|
||||||
values = LotMatching._selected_matching_defaults(
|
values = LotMatching._selected_matching_defaults(
|
||||||
context.get('active_ids') or [])
|
context.get('active_ids') or [])
|
||||||
|
if not values.get('lot_p') or not values.get('lot_s'):
|
||||||
|
raise UserError(
|
||||||
|
"Please select at least one purchase line and one sale line "
|
||||||
|
"before using Go to matching.")
|
||||||
values['qt_type'] = 'all'
|
values['qt_type'] = 'all'
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def transition_matching(self):
|
def transition_matching(self):
|
||||||
|
if not self.match.lot_p or not self.match.lot_s:
|
||||||
|
raise UserError(
|
||||||
|
"Please select at least one purchase line and one sale line "
|
||||||
|
"before using Go to matching.")
|
||||||
return LotMatching.transition_matching(self)
|
return LotMatching.transition_matching(self)
|
||||||
|
|
||||||
def end(self):
|
def end(self):
|
||||||
|
|||||||
@@ -3417,6 +3417,77 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(result['lot_s'][0]['lot_qt_max'], Decimal('80.00000'))
|
self.assertEqual(result['lot_s'][0]['lot_qt_max'], Decimal('80.00000'))
|
||||||
self.assertEqual(result['lot_s'][0]['lot_cp'], 40)
|
self.assertEqual(result['lot_s'][0]['lot_cp'], 40)
|
||||||
|
|
||||||
|
def test_go_matching_defaults_selected_physical_purchase_lot(self):
|
||||||
|
'go to matching preloads selected unmatched physical purchase lots'
|
||||||
|
purchase = Mock(id=10, party=Mock(id=20))
|
||||||
|
sale = Mock(id=30, party=Mock(id=40))
|
||||||
|
product = Mock(id=50)
|
||||||
|
physical_lot = Mock(
|
||||||
|
id=101,
|
||||||
|
line=Mock(
|
||||||
|
purchase=purchase, inherit_tol=False,
|
||||||
|
quantity_theorical=Decimal('100'),
|
||||||
|
targeted_qt=Decimal('96'), tol_min=0, tol_max=0),
|
||||||
|
sale_line=None,
|
||||||
|
lot_type='physic',
|
||||||
|
lot_product=product,
|
||||||
|
lot_shipment_in=None,
|
||||||
|
lot_shipment_internal=None,
|
||||||
|
lot_shipment_out=None)
|
||||||
|
physical_lot.get_current_quantity_converted.return_value = (
|
||||||
|
Decimal('70'))
|
||||||
|
sale_lot = Mock(
|
||||||
|
id=102, line=None,
|
||||||
|
sale_line=Mock(
|
||||||
|
sale=sale, inherit_tol=False,
|
||||||
|
quantity_theorical=Decimal('80'),
|
||||||
|
targeted_qt=Decimal('84'), tol_min=0, tol_max=0),
|
||||||
|
lot_type='virtual', lot_product=product)
|
||||||
|
sale_lqt = Mock(
|
||||||
|
id=2, lot_p=None, lot_s=sale_lot,
|
||||||
|
lot_quantity=Decimal('80'),
|
||||||
|
lot_shipment_in=None, lot_shipment_internal=None,
|
||||||
|
lot_shipment_out=None)
|
||||||
|
|
||||||
|
class LotQtMock:
|
||||||
|
def __call__(self, _id):
|
||||||
|
return sale_lqt
|
||||||
|
|
||||||
|
class LotMock:
|
||||||
|
def __call__(self, _id):
|
||||||
|
return physical_lot
|
||||||
|
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = (
|
||||||
|
lambda name: LotQtMock() if name == 'lot.qt' else LotMock())
|
||||||
|
transaction = Mock()
|
||||||
|
transaction.context = {
|
||||||
|
'active_ids': [101, 10000002],
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(lot_module, 'Pool', return_value=pool):
|
||||||
|
with patch.object(
|
||||||
|
lot_module, 'Transaction', return_value=transaction):
|
||||||
|
result = lot_module.LotGoMatching().default_match([])
|
||||||
|
|
||||||
|
self.assertEqual(len(result['lot_p']), 1)
|
||||||
|
self.assertEqual(len(result['lot_s']), 1)
|
||||||
|
self.assertEqual(result['lot_p'][0]['lot_id'], 101)
|
||||||
|
self.assertIsNone(result['lot_p'][0]['lot_r_id'])
|
||||||
|
self.assertEqual(result['lot_p'][0]['lot_type'], 'physic')
|
||||||
|
self.assertEqual(result['lot_p'][0]['lot_quantity'], Decimal('70'))
|
||||||
|
self.assertEqual(result['lot_p'][0]['lot_targeted_qt'], Decimal('96'))
|
||||||
|
self.assertEqual(result['lot_p'][0]['lot_cp'], 20)
|
||||||
|
self.assertEqual(result['lot_s'][0]['lot_r_id'], 2)
|
||||||
|
|
||||||
|
def test_go_matching_requires_purchase_and_sale_selection(self):
|
||||||
|
'go to matching requires one purchase and one sale row'
|
||||||
|
wizard = lot_module.LotGoMatching()
|
||||||
|
wizard.match = Mock(lot_p=[], lot_s=[Mock()])
|
||||||
|
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
wizard.transition_matching()
|
||||||
|
|
||||||
def test_purchase_tolerance_used_is_weighted_line_average(self):
|
def test_purchase_tolerance_used_is_weighted_line_average(self):
|
||||||
'purchase tolerance gauge averages line gauges by theoretical quantity'
|
'purchase tolerance gauge averages line gauges by theoretical quantity'
|
||||||
unit = Mock()
|
unit = Mock()
|
||||||
|
|||||||
178
notes/accounting/excel_api/GLAccountsExcelApi.bas
Normal file
178
notes/accounting/excel_api/GLAccountsExcelApi.bas
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
Attribute VB_Name = "GLAccountsExcelApi"
|
||||||
|
Option Explicit
|
||||||
|
|
||||||
|
' Finaccount-like Excel API POC for GL ACCOUNTS FUNCTIONS.
|
||||||
|
'
|
||||||
|
' Setup:
|
||||||
|
' 1. Install PostgreSQL ODBC driver.
|
||||||
|
' 2. Apply notes/accounting/excel_api/gl_accounts_sql_poc.sql.
|
||||||
|
' 3. In Excel: Developer > Visual Basic > File > Import File...
|
||||||
|
' 4. Import this .bas module.
|
||||||
|
' 5. Edit GL_API_CONNECTION_STRING below.
|
||||||
|
'
|
||||||
|
' Example:
|
||||||
|
' =GetAccountAmount("COMPANY01","400000","USD",DATE(2026,5,31))
|
||||||
|
|
||||||
|
Private Const GL_API_CONNECTION_STRING As String = _
|
||||||
|
"Driver={PostgreSQL Unicode(x64)};" & _
|
||||||
|
"Server=localhost;" & _
|
||||||
|
"Port=5432;" & _
|
||||||
|
"Database=tradon;" & _
|
||||||
|
"Uid=tradon_readonly;" & _
|
||||||
|
"Pwd=change_me;"
|
||||||
|
|
||||||
|
Private gConnection As Object
|
||||||
|
Private gCache As Object
|
||||||
|
|
||||||
|
Private Function ApiCache() As Object
|
||||||
|
If gCache Is Nothing Then
|
||||||
|
Set gCache = CreateObject("Scripting.Dictionary")
|
||||||
|
End If
|
||||||
|
Set ApiCache = gCache
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Private Function DbConnection() As Object
|
||||||
|
If gConnection Is Nothing Then
|
||||||
|
Set gConnection = CreateObject("ADODB.Connection")
|
||||||
|
gConnection.ConnectionString = GL_API_CONNECTION_STRING
|
||||||
|
gConnection.Open
|
||||||
|
ElseIf gConnection.State = 0 Then
|
||||||
|
gConnection.Open
|
||||||
|
End If
|
||||||
|
Set DbConnection = gConnection
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Private Sub ResetDbConnection()
|
||||||
|
On Error Resume Next
|
||||||
|
If Not gConnection Is Nothing Then
|
||||||
|
If gConnection.State <> 0 Then gConnection.Close
|
||||||
|
End If
|
||||||
|
Set gConnection = Nothing
|
||||||
|
On Error GoTo 0
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Public Sub ClearGLApiCache()
|
||||||
|
If Not gCache Is Nothing Then gCache.RemoveAll
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Public Sub CloseGLApiConnection()
|
||||||
|
If Not gConnection Is Nothing Then
|
||||||
|
If gConnection.State <> 0 Then gConnection.Close
|
||||||
|
End If
|
||||||
|
Set gConnection = Nothing
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Private Function SqlDate(ByVal valueDate As Variant) As String
|
||||||
|
SqlDate = Format$(CDate(valueDate), "yyyy-mm-dd")
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Private Function SqlText(ByVal value As Variant) As String
|
||||||
|
SqlText = Replace(CStr(value), "'", "''")
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Private Function QueryScalar( _
|
||||||
|
ByVal functionName As String, _
|
||||||
|
ByVal companyKey As Variant, _
|
||||||
|
ByVal accountCode As Variant, _
|
||||||
|
ByVal currencyIso As Variant, _
|
||||||
|
ByVal valueDate As Variant) As Variant
|
||||||
|
|
||||||
|
On Error GoTo ErrorHandler
|
||||||
|
|
||||||
|
Dim key As String
|
||||||
|
key = functionName & "|" & CStr(companyKey) & "|" & CStr(accountCode) _
|
||||||
|
& "|" & CStr(currencyIso) & "|" & SqlDate(valueDate)
|
||||||
|
|
||||||
|
If ApiCache.Exists(key) Then
|
||||||
|
QueryScalar = ApiCache(key)
|
||||||
|
Exit Function
|
||||||
|
End If
|
||||||
|
|
||||||
|
Dim sql As String
|
||||||
|
sql = "select excel_api." & functionName & "('" _
|
||||||
|
& SqlText(companyKey) & "','" _
|
||||||
|
& SqlText(accountCode) & "','" _
|
||||||
|
& SqlText(currencyIso) & "','" _
|
||||||
|
& SqlDate(valueDate) & "'::date)"
|
||||||
|
|
||||||
|
Dim rs As Object
|
||||||
|
Dim attempt As Integer
|
||||||
|
RetryQuery:
|
||||||
|
Set rs = CreateObject("ADODB.Recordset")
|
||||||
|
rs.Open sql, DbConnection, 0, 1
|
||||||
|
|
||||||
|
If rs.EOF Or IsNull(rs.Fields(0).Value) Then
|
||||||
|
QueryScalar = Empty
|
||||||
|
Else
|
||||||
|
QueryScalar = rs.Fields(0).Value
|
||||||
|
End If
|
||||||
|
rs.Close
|
||||||
|
|
||||||
|
ApiCache.Add key, QueryScalar
|
||||||
|
Exit Function
|
||||||
|
|
||||||
|
ErrorHandler:
|
||||||
|
If attempt = 0 Then
|
||||||
|
attempt = 1
|
||||||
|
If Not rs Is Nothing Then
|
||||||
|
If rs.State <> 0 Then rs.Close
|
||||||
|
End If
|
||||||
|
ResetDbConnection
|
||||||
|
Resume RetryQuery
|
||||||
|
End If
|
||||||
|
QueryScalar = "#ERR " & Err.Number & ": " & Err.Description
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Public Function GetAccountBaseAmount( _
|
||||||
|
ByVal companyKey As Variant, _
|
||||||
|
ByVal accountCode As Variant, _
|
||||||
|
ByVal currencyIso As Variant, _
|
||||||
|
ByVal valueDate As Variant) As Variant
|
||||||
|
GetAccountBaseAmount = QueryScalar("get_account_base_amount", _
|
||||||
|
companyKey, accountCode, currencyIso, valueDate)
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Public Function GetAccountRealBaseAmount( _
|
||||||
|
ByVal companyKey As Variant, _
|
||||||
|
ByVal accountCode As Variant, _
|
||||||
|
ByVal currencyIso As Variant, _
|
||||||
|
ByVal valueDate As Variant) As Variant
|
||||||
|
GetAccountRealBaseAmount = QueryScalar("get_account_real_base_amount", _
|
||||||
|
companyKey, accountCode, currencyIso, valueDate)
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Public Function GetAccountAmount( _
|
||||||
|
ByVal companyKey As Variant, _
|
||||||
|
ByVal accountCode As Variant, _
|
||||||
|
ByVal currencyIso As Variant, _
|
||||||
|
ByVal valueDate As Variant) As Variant
|
||||||
|
GetAccountAmount = QueryScalar("get_account_amount", _
|
||||||
|
companyKey, accountCode, currencyIso, valueDate)
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Public Function GetAccountAbbr( _
|
||||||
|
ByVal companyKey As Variant, _
|
||||||
|
ByVal accountCode As Variant, _
|
||||||
|
ByVal currencyIso As Variant, _
|
||||||
|
ByVal valueDate As Variant) As Variant
|
||||||
|
GetAccountAbbr = QueryScalar("get_account_abbr", _
|
||||||
|
companyKey, accountCode, currencyIso, valueDate)
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Public Function GetAccountName( _
|
||||||
|
ByVal companyKey As Variant, _
|
||||||
|
ByVal accountCode As Variant, _
|
||||||
|
ByVal currencyIso As Variant, _
|
||||||
|
ByVal valueDate As Variant) As Variant
|
||||||
|
GetAccountName = QueryScalar("get_account_name", _
|
||||||
|
companyKey, accountCode, currencyIso, valueDate)
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Public Function GetAccountContact( _
|
||||||
|
ByVal companyKey As Variant, _
|
||||||
|
ByVal accountCode As Variant, _
|
||||||
|
ByVal currencyIso As Variant, _
|
||||||
|
ByVal valueDate As Variant) As Variant
|
||||||
|
GetAccountContact = QueryScalar("get_account_contact", _
|
||||||
|
companyKey, accountCode, currencyIso, valueDate)
|
||||||
|
End Function
|
||||||
BIN
notes/accounting/excel_api/GL_Accounts_POC.xlsx
Normal file
BIN
notes/accounting/excel_api/GL_Accounts_POC.xlsx
Normal file
Binary file not shown.
BIN
notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsm
Normal file
BIN
notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsm
Normal file
Binary file not shown.
BIN
notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsx
Normal file
BIN
notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsx
Normal file
Binary file not shown.
111
notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md
Normal file
111
notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Excel API POC - GL Accounts
|
||||||
|
|
||||||
|
This POC covers only the PDF section **GL ACCOUNTS FUNCTIONS**.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `gl_accounts_sql_poc.sql`: PostgreSQL schema/view/functions.
|
||||||
|
- `GLAccountsExcelApi.bas`: VBA module exposing Excel formulas.
|
||||||
|
- `GL_Accounts_POC.xlsx`: sample workbook with formulas wired to the VBA UDFs.
|
||||||
|
|
||||||
|
## SQL Objects
|
||||||
|
|
||||||
|
The SQL creates:
|
||||||
|
|
||||||
|
- `excel_api.gl_account_lines`
|
||||||
|
- `excel_api.currency_rate_at(currency_id, value_date)`
|
||||||
|
- `excel_api.get_account_base_amount(company, account, currency, date)`
|
||||||
|
- `excel_api.get_account_real_base_amount(company, account, currency, date)`
|
||||||
|
- `excel_api.get_account_amount(company, account, currency, date)`
|
||||||
|
- `excel_api.get_account_abbr(company, account, currency, date)`
|
||||||
|
- `excel_api.get_account_name(company, account, currency, date)`
|
||||||
|
- `excel_api.get_account_contact(company, account, currency, date)`
|
||||||
|
|
||||||
|
## Excel Formulas
|
||||||
|
|
||||||
|
After importing the VBA module:
|
||||||
|
|
||||||
|
```excel
|
||||||
|
=GetAccountBaseAmount("COMPANY01","400000","USD",DATE(2026,5,31))
|
||||||
|
=GetAccountRealBaseAmount("COMPANY01","400000","USD",DATE(2026,5,31))
|
||||||
|
=GetAccountAmount("COMPANY01","400000","USD",DATE(2026,5,31))
|
||||||
|
=GetAccountAbbr("COMPANY01","400000","USD",DATE(2026,5,31))
|
||||||
|
=GetAccountName("COMPANY01","400000","USD",DATE(2026,5,31))
|
||||||
|
=GetAccountContact("COMPANY01","400000","USD",DATE(2026,5,31))
|
||||||
|
```
|
||||||
|
|
||||||
|
The sample workbook already contains these formulas in the `GL Accounts POC`
|
||||||
|
sheet. Save it as `.xlsm` after importing the VBA module.
|
||||||
|
|
||||||
|
## Current POC Assumptions
|
||||||
|
|
||||||
|
- The `company` parameter accepts either `party_party.code` of the company
|
||||||
|
party or the company party name.
|
||||||
|
- Only posted moves are included.
|
||||||
|
- The currency parameter filters transaction currency. It accepts the internal
|
||||||
|
currency id, `currency_currency.code`, `name`, `symbol`, or `numeric_code`.
|
||||||
|
- `GetAccountAbbr` returns `account_account.code` because no separate GL
|
||||||
|
abbreviation field exists in the current schema.
|
||||||
|
- `GetAccountContact` returns a comma-separated list of parties found on
|
||||||
|
posted GL lines for the account/currency/date.
|
||||||
|
- `GetAccountBaseAmount` revalues the transaction amount at `value_date` using
|
||||||
|
`currency_currency_rate`.
|
||||||
|
|
||||||
|
## Session Notes - 2026-05-26
|
||||||
|
|
||||||
|
Validated manually with Excel Desktop in safe mode after Excel initially hung
|
||||||
|
while opening from the web flow.
|
||||||
|
|
||||||
|
What worked:
|
||||||
|
|
||||||
|
- Workbook saved as macro-enabled file:
|
||||||
|
`GL_Accounts_POC_with_formulas.xlsm`.
|
||||||
|
- VBA module imported into a standard module after removing the raw
|
||||||
|
`Attribute VB_Name = ...` line when pasted manually.
|
||||||
|
- `#NOM?` was resolved once macros/VBA compilation were correct.
|
||||||
|
- Temporary VBA error display (`#ERR <number>: <message>`) helped expose ODBC
|
||||||
|
failures instead of generic `#VALEUR!`.
|
||||||
|
- `ICT TRADING` can be entered as the company value after SQL support for
|
||||||
|
matching by company party name.
|
||||||
|
- Currency matching now supports values such as `1`, `EUR`, and `USD` by
|
||||||
|
checking currency id/code/name/symbol/numeric code.
|
||||||
|
- Verified live examples:
|
||||||
|
- `ICT TRADING`, account `20011`, currency `1`/`USD`, date `2026-05-13`
|
||||||
|
returns Debtors values and contact list.
|
||||||
|
- `ICT TRADING`, account `30011`, currency `EUR`, date `2026-05-13`
|
||||||
|
returns Creditors values and contact `1741`.
|
||||||
|
|
||||||
|
Important findings:
|
||||||
|
|
||||||
|
- In the tested database, `currency_currency.code` is not the ISO label:
|
||||||
|
`id=1` has `code=1`, `name=USD`, `symbol=USD`; `id=2` has `code=2`,
|
||||||
|
`name=EUR`, `symbol=EUR`.
|
||||||
|
- The currency parameter currently means transaction-currency filter, not
|
||||||
|
output currency conversion. `EUR` and `USD` can legitimately return different
|
||||||
|
subsets for the same account.
|
||||||
|
- The VBA cache can make testing confusing; run `ClearGLApiCache` and
|
||||||
|
`CloseGLApiConnection` before comparing changed parameters.
|
||||||
|
- Excel/ODBC may return `SQLExecDirectW unable due to the connection lost`;
|
||||||
|
the VBA now resets the ADODB connection and retries once.
|
||||||
|
- The `@` shown before UDF names in modern Excel formulas is normal implicit
|
||||||
|
intersection syntax and was not the issue.
|
||||||
|
|
||||||
|
Next session:
|
||||||
|
|
||||||
|
- Decide whether the currency parameter should remain a transaction-currency
|
||||||
|
filter or become a requested output currency with conversion.
|
||||||
|
- Add an explicit workbook refresh button or visible refresh instructions for
|
||||||
|
`ClearGLApiCache` / `CloseGLApiConnection`.
|
||||||
|
- Consider moving ODBC connection settings to a hidden/settings worksheet
|
||||||
|
instead of hardcoding `GL_API_CONNECTION_STRING`.
|
||||||
|
- Review whether contact output should be party codes, names, or both.
|
||||||
|
|
||||||
|
## Recommended Production Shape
|
||||||
|
|
||||||
|
For production, prefer a readonly DB user and an ODBC DSN. The VBA module is a
|
||||||
|
minimal proof of concept; a production workbook should add:
|
||||||
|
|
||||||
|
- a settings sheet for connection parameters;
|
||||||
|
- refresh/cache controls;
|
||||||
|
- error messages visible to users;
|
||||||
|
- optional Power Query preload for large reports.
|
||||||
235
notes/accounting/excel_api/gl_accounts_sql_poc.sql
Normal file
235
notes/accounting/excel_api/gl_accounts_sql_poc.sql
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
-- Finaccount-like Excel API POC for GL ACCOUNTS FUNCTIONS.
|
||||||
|
--
|
||||||
|
-- Scope:
|
||||||
|
-- - GetAccountBaseAmount
|
||||||
|
-- - GetAccountRealBaseAmount
|
||||||
|
-- - GetAccountAmount
|
||||||
|
-- - GetAccountAbbr
|
||||||
|
-- - GetAccountName
|
||||||
|
-- - GetAccountContact
|
||||||
|
--
|
||||||
|
-- Design notes:
|
||||||
|
-- - PostgreSQL lowercases function names; VBA calls these SQL functions.
|
||||||
|
-- - Balances are computed as of p_value_date.
|
||||||
|
-- - This POC uses posted account moves only.
|
||||||
|
-- - p_currency_iso is treated as the transaction currency.
|
||||||
|
-- - p_company_key accepts either the company party code or company name.
|
||||||
|
-- - p_currency_iso accepts the currency code/name/symbol/numeric code/id.
|
||||||
|
-- - If account.move.line.second_currency is empty, the company currency is
|
||||||
|
-- used as transaction currency and the company amount is reused.
|
||||||
|
|
||||||
|
create schema if not exists excel_api;
|
||||||
|
|
||||||
|
create or replace view excel_api.gl_account_lines as
|
||||||
|
select
|
||||||
|
aml.id as move_line_id,
|
||||||
|
am.id as move_id,
|
||||||
|
cc.id as company_id,
|
||||||
|
cp.code as company_key,
|
||||||
|
cp.name as company_name,
|
||||||
|
aa.id as account_id,
|
||||||
|
aa.code as account_code,
|
||||||
|
aa.name as account_name,
|
||||||
|
coalesce(tx_cur.code, base_cur.code) as transaction_currency_code,
|
||||||
|
coalesce(aml.second_currency, cc.currency) as transaction_currency_id,
|
||||||
|
base_cur.code as base_currency_code,
|
||||||
|
cc.currency as base_currency_id,
|
||||||
|
am.date as posting_date,
|
||||||
|
am.state as posting_status,
|
||||||
|
aml.party as party_id,
|
||||||
|
party.code as party_code,
|
||||||
|
party.name as party_name,
|
||||||
|
coalesce(aml.debit, 0) as debit_base_currency,
|
||||||
|
coalesce(aml.credit, 0) as credit_base_currency,
|
||||||
|
coalesce(aml.debit, 0) - coalesce(aml.credit, 0)
|
||||||
|
as balance_base_currency,
|
||||||
|
case
|
||||||
|
when aml.second_currency is not null then
|
||||||
|
coalesce(aml.amount_second_currency, 0)
|
||||||
|
else
|
||||||
|
coalesce(aml.debit, 0) - coalesce(aml.credit, 0)
|
||||||
|
end as balance_transaction_currency
|
||||||
|
from account_move_line aml
|
||||||
|
join account_move am on am.id = aml.move
|
||||||
|
join account_account aa on aa.id = aml.account
|
||||||
|
join company_company cc on cc.id = am.company
|
||||||
|
left join party_party cp on cp.id = cc.party
|
||||||
|
left join currency_currency base_cur on base_cur.id = cc.currency
|
||||||
|
left join currency_currency tx_cur on tx_cur.id = aml.second_currency
|
||||||
|
left join party_party party on party.id = aml.party
|
||||||
|
where aa.type is not null
|
||||||
|
and coalesce(aa.closed, false) is false;
|
||||||
|
|
||||||
|
create or replace function excel_api.currency_rate_at(
|
||||||
|
p_currency_id integer,
|
||||||
|
p_value_date date)
|
||||||
|
returns numeric
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select cr.rate
|
||||||
|
from currency_currency_rate cr
|
||||||
|
where cr.currency = p_currency_id
|
||||||
|
and cr.date <= p_value_date
|
||||||
|
order by cr.date desc
|
||||||
|
limit 1
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function excel_api.company_matches(
|
||||||
|
p_company_key text,
|
||||||
|
p_company_input text)
|
||||||
|
returns boolean
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select upper(btrim(coalesce(p_company_key, '')))
|
||||||
|
= upper(btrim(coalesce(p_company_input, '')))
|
||||||
|
or upper(btrim(coalesce(p_company_input, ''))) = (
|
||||||
|
select upper(btrim(coalesce(pp.name, '')))
|
||||||
|
from company_company cc
|
||||||
|
left join party_party pp on pp.id = cc.party
|
||||||
|
where pp.code = p_company_key
|
||||||
|
limit 1)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function excel_api.currency_matches(
|
||||||
|
p_currency_id integer,
|
||||||
|
p_currency_input text)
|
||||||
|
returns boolean
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from currency_currency cur
|
||||||
|
where cur.id = p_currency_id
|
||||||
|
and upper(btrim(coalesce(p_currency_input, ''))) in (
|
||||||
|
upper(btrim(cur.id::text)),
|
||||||
|
upper(btrim(coalesce(cur.code, ''))),
|
||||||
|
upper(btrim(coalesce(cur.name, ''))),
|
||||||
|
upper(btrim(coalesce(cur.symbol, ''))),
|
||||||
|
upper(btrim(coalesce(cur.numeric_code, '')))))
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function excel_api.get_account_amount(
|
||||||
|
p_company_key text,
|
||||||
|
p_account_code text,
|
||||||
|
p_currency_iso text,
|
||||||
|
p_value_date date)
|
||||||
|
returns numeric
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select coalesce(sum(gl.balance_transaction_currency), 0)
|
||||||
|
from excel_api.gl_account_lines gl
|
||||||
|
where excel_api.company_matches(gl.company_key, p_company_key)
|
||||||
|
and gl.account_code = p_account_code
|
||||||
|
and excel_api.currency_matches(gl.transaction_currency_id, p_currency_iso)
|
||||||
|
and gl.posting_date <= p_value_date
|
||||||
|
and gl.posting_status = 'posted'
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function excel_api.get_account_real_base_amount(
|
||||||
|
p_company_key text,
|
||||||
|
p_account_code text,
|
||||||
|
p_currency_iso text,
|
||||||
|
p_value_date date)
|
||||||
|
returns numeric
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select coalesce(sum(gl.balance_base_currency), 0)
|
||||||
|
from excel_api.gl_account_lines gl
|
||||||
|
where excel_api.company_matches(gl.company_key, p_company_key)
|
||||||
|
and gl.account_code = p_account_code
|
||||||
|
and excel_api.currency_matches(gl.transaction_currency_id, p_currency_iso)
|
||||||
|
and gl.posting_date <= p_value_date
|
||||||
|
and gl.posting_status = 'posted'
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function excel_api.get_account_base_amount(
|
||||||
|
p_company_key text,
|
||||||
|
p_account_code text,
|
||||||
|
p_currency_iso text,
|
||||||
|
p_value_date date)
|
||||||
|
returns numeric
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select coalesce(sum(
|
||||||
|
case
|
||||||
|
when gl.transaction_currency_id = gl.base_currency_id then
|
||||||
|
gl.balance_transaction_currency
|
||||||
|
else
|
||||||
|
gl.balance_transaction_currency
|
||||||
|
* excel_api.currency_rate_at(gl.base_currency_id, p_value_date)
|
||||||
|
/ nullif(
|
||||||
|
excel_api.currency_rate_at(
|
||||||
|
gl.transaction_currency_id, p_value_date),
|
||||||
|
0)
|
||||||
|
end), 0)
|
||||||
|
from excel_api.gl_account_lines gl
|
||||||
|
where excel_api.company_matches(gl.company_key, p_company_key)
|
||||||
|
and gl.account_code = p_account_code
|
||||||
|
and excel_api.currency_matches(gl.transaction_currency_id, p_currency_iso)
|
||||||
|
and gl.posting_date <= p_value_date
|
||||||
|
and gl.posting_status = 'posted'
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function excel_api.get_account_abbr(
|
||||||
|
p_company_key text,
|
||||||
|
p_account_code text,
|
||||||
|
p_currency_iso text,
|
||||||
|
p_value_date date)
|
||||||
|
returns text
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select gl.account_code
|
||||||
|
from excel_api.gl_account_lines gl
|
||||||
|
where excel_api.company_matches(gl.company_key, p_company_key)
|
||||||
|
and gl.account_code = p_account_code
|
||||||
|
and excel_api.currency_matches(gl.transaction_currency_id, p_currency_iso)
|
||||||
|
and gl.posting_date <= p_value_date
|
||||||
|
order by gl.posting_date desc, gl.move_line_id desc
|
||||||
|
limit 1
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function excel_api.get_account_name(
|
||||||
|
p_company_key text,
|
||||||
|
p_account_code text,
|
||||||
|
p_currency_iso text,
|
||||||
|
p_value_date date)
|
||||||
|
returns text
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select gl.account_name
|
||||||
|
from excel_api.gl_account_lines gl
|
||||||
|
where excel_api.company_matches(gl.company_key, p_company_key)
|
||||||
|
and gl.account_code = p_account_code
|
||||||
|
and excel_api.currency_matches(gl.transaction_currency_id, p_currency_iso)
|
||||||
|
and gl.posting_date <= p_value_date
|
||||||
|
order by gl.posting_date desc, gl.move_line_id desc
|
||||||
|
limit 1
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function excel_api.get_account_contact(
|
||||||
|
p_company_key text,
|
||||||
|
p_account_code text,
|
||||||
|
p_currency_iso text,
|
||||||
|
p_value_date date)
|
||||||
|
returns text
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select nullif(string_agg(distinct coalesce(gl.party_code, gl.party_name),
|
||||||
|
', ' order by coalesce(gl.party_code, gl.party_name)), '')
|
||||||
|
from excel_api.gl_account_lines gl
|
||||||
|
where excel_api.company_matches(gl.company_key, p_company_key)
|
||||||
|
and gl.account_code = p_account_code
|
||||||
|
and excel_api.currency_matches(gl.transaction_currency_id, p_currency_iso)
|
||||||
|
and gl.posting_date <= p_value_date
|
||||||
|
and gl.posting_status = 'posted'
|
||||||
|
and coalesce(gl.party_code, gl.party_name) is not null
|
||||||
|
$$;
|
||||||
Reference in New Issue
Block a user