feat: Generate entries for invoice lines w/ disposition "Journal"
With this change, although still no Payment Entry or Collection records can be generated, the app actually becomes potentially useful, although it still requires significant further development, including at least: * Generate remaining types of entries * Update Data status of an invoice line when reference_doc or account changes * Update Disposition of an invoice when data_status changes Given the major milestone, bumping version number to 0.1.0
This commit is contained in:
parent
51ed8a85a8
commit
7958f6baf6
@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '0.0.1'
|
||||
__version__ = '0.1.0'
|
||||
|
||||
|
@ -10,13 +10,19 @@ from frappe.utils.csvutils import read_csv_content
|
||||
from frappe.utils.data import flt
|
||||
from frappe.utils.dateutils import parse_date
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||
|
||||
from collections import defaultdict
|
||||
from fnmatch import fnmatch as wildcard_match
|
||||
import json
|
||||
from operator import itemgetter
|
||||
from os.path import commonprefix
|
||||
import re
|
||||
|
||||
class JWImport(Document):
|
||||
account_mapping_fields = 'charge_type charge_subtype map_to_account'.split()
|
||||
jea_remark_fields = 'voucher_id voucher_type charge_subtype department reference_work_id notes'.split()
|
||||
je_remark_fields = 'voucher_pay_date period_start_date period_end_date'.split()
|
||||
collection_ref = 'Collection Summary'
|
||||
|
||||
def process_invoice_file(self):
|
||||
@ -31,11 +37,19 @@ class JWImport(Document):
|
||||
id_idx = header.index('invoice_id')
|
||||
cur_id = ''
|
||||
rows.append(['THIS_IS_END_SENTINEL'] * (id_idx+1))
|
||||
si_date = ''
|
||||
if self.start_invoice_date:
|
||||
si_date = get_datetime(self.start_invoice_date).date()
|
||||
ei_date = ''
|
||||
if self.end_invoice_date:
|
||||
ei_date = get_datetime(self.end_invoice_date).date()
|
||||
# Load the base data from the file
|
||||
for row in rows:
|
||||
# Emit a collection record if invoice is complete
|
||||
if ccr_flag and cur_id and cur_id != row[id_idx]:
|
||||
lrec = self.lines[-1]
|
||||
factor = 10.0**lrec.precision('amount')
|
||||
inv_total = int(inv_total*factor + 0.5)/factor
|
||||
ctmp = dict(invoice_id=cur_id,
|
||||
invoice_date=lrec.invoice_date,
|
||||
debit_date=lrec.debit_date,
|
||||
@ -44,6 +58,7 @@ class JWImport(Document):
|
||||
crec = self.append('lines', {})
|
||||
for k in ctmp: crec.set(k, ctmp[k])
|
||||
inv_total = 0.0
|
||||
cur_id = ''
|
||||
if row[id_idx] == 'THIS_IS_END_SENTINEL': break
|
||||
|
||||
temp = {}
|
||||
@ -54,9 +69,9 @@ class JWImport(Document):
|
||||
elif fieldname == 'amount':
|
||||
val = re.sub('^[^.,\d]*', '', val)
|
||||
temp[fieldname] = val
|
||||
if (self.start_invoice_date and temp['invoice_date'] < self.start_invoice_date):
|
||||
if (si_date and temp['invoice_date'] < si_date):
|
||||
continue
|
||||
if (self.end_invoice_date and temp['invoice_date'] > self.end_invoice_date):
|
||||
if (ei_date and temp['invoice_date'] > ei_date):
|
||||
continue
|
||||
cur_id = temp['invoice_id']
|
||||
rec = self.append('lines', {})
|
||||
@ -140,13 +155,15 @@ class JWImport(Document):
|
||||
"""Look for a purchase invoice with the same supplier and
|
||||
amount as rec and fill in the reference_doc if found
|
||||
"""
|
||||
fullref = rec.reference
|
||||
(lastref, comma, restref) = fullref.partition(',')
|
||||
# Reference should be a supplier
|
||||
suppliers = (frappe.get_all('Supplier',
|
||||
{'supplier_name': rec.reference}) or
|
||||
frappe.get_all('Supplier', {
|
||||
'supplier_name':
|
||||
['like', f"%{rec.reference}%"]})
|
||||
)
|
||||
{'supplier_name': fullref})
|
||||
or frappe.get_all('Supplier', {
|
||||
'supplier_name': ['like', f"%{fullref}%"]})
|
||||
or (comma and frappe.get_all('Supplier', {
|
||||
'supplier_name': ['like', f"%{lastref}"]})))
|
||||
if not suppliers or len(suppliers) != 1: return
|
||||
suplr = suppliers[0].name
|
||||
invoices = frappe.get_all('Purchase Invoice',
|
||||
@ -158,18 +175,19 @@ class JWImport(Document):
|
||||
if len(invoices) == 1:
|
||||
rec.set('reference_doc', invoices[0].name)
|
||||
return
|
||||
rec_amt= flt(rec.amount)
|
||||
match_inv = ([inv for inv in invoices
|
||||
if inv.outstanding_amount == rec.amount]
|
||||
if inv.outstanding_amount == rec_amt]
|
||||
or [inv for inv in invoices
|
||||
if inv.outstanding_amount > rec.amount])
|
||||
if inv.outstanding_amount > rec_amt])
|
||||
if len(match_inv) > 0:
|
||||
# Best match is earliest unpaid invoice
|
||||
earliest = min(match_inv, key=itemgetter('date'))
|
||||
rec.set('reference_doc', earliest.name)
|
||||
return
|
||||
match_inv = [inv for inv in invoices
|
||||
if inv.grand_total == rec.amount or
|
||||
inv.rounded_total == rec.amount]
|
||||
if inv.grand_total == rec_amt or
|
||||
inv.rounded_total == rec_amt]
|
||||
if len(match_inv) > 0:
|
||||
latest = max(match_inv, key=itemgetter('date'))
|
||||
rec.set('reference_doc', latest.name)
|
||||
@ -191,9 +209,10 @@ class JWImport(Document):
|
||||
if len(claims) == 1:
|
||||
rec.set('reference_doc', claims[0].name)
|
||||
return
|
||||
match_claims = {c for c in claims if c.total == rec.amount}
|
||||
rec_amt = flt(rec.amount)
|
||||
match_claims = {c for c in claims if c.total == rec_amt}
|
||||
unpaid_claims = {c for c in claims
|
||||
if c.total >= rec.amount and not c.is_paid}
|
||||
if c.total >= rec_amt and not c.is_paid}
|
||||
mu_claims = match_claims & unpaid_claims
|
||||
if len(mu_claims) > 0:
|
||||
earliest = min(mu_claims, key=itemgetter('date'))
|
||||
@ -238,7 +257,10 @@ class JWImport(Document):
|
||||
def determine_disposition(self, recs):
|
||||
for rec in recs:
|
||||
if rec.data_status != 'OK':
|
||||
rec.dispostion = 'Data Missing'
|
||||
rec.disposition = 'Data Missing'
|
||||
continue
|
||||
if flt(rec.amount) < 0.01:
|
||||
rec.disposition = 'Ignore'
|
||||
continue
|
||||
|
||||
if rec.reference == self.collection_ref:
|
||||
@ -263,6 +285,7 @@ class JWImport(Document):
|
||||
if rec.disposition == 'Collection':
|
||||
jes = frappe.get_all('Journal Entry', filters={
|
||||
'company': self.justworks_company,
|
||||
'docstatus': 1,
|
||||
'posting_date': rec.debit_date})
|
||||
if not jes: return False
|
||||
je_names = [je.name for je in jes]
|
||||
@ -270,10 +293,11 @@ class JWImport(Document):
|
||||
'doctype': 'Journal Entry Account',
|
||||
'parent': ('in', je_names),
|
||||
'account': self.jw_collection_debits,
|
||||
'debit_in_account_currency': rec.amount})
|
||||
'debit_in_account_currency': flt(rec.amount)})
|
||||
if rec.disposition[:3] == 'Pay':
|
||||
pes = frappe.get_all('Payment Entry', filters={
|
||||
'company': self.justworks_company,
|
||||
'docstatus': 1,
|
||||
'paid_from': self.jw_payment_account,
|
||||
'paid_amount': rec.amount})
|
||||
if not pes: return False
|
||||
@ -285,7 +309,8 @@ class JWImport(Document):
|
||||
if rec.disposition == 'Journal':
|
||||
jes = frappe.get_all('Journal Entry', filters={
|
||||
'company': self.justworks_company,
|
||||
'posting_date': rec.debit_date})
|
||||
'docstatus': 1,
|
||||
'posting_date': rec.invoice_date})
|
||||
if not jes: return False
|
||||
je_names = [je.name for je in jes]
|
||||
jeas = frappe.get_all('Journal Entry Account',
|
||||
@ -294,6 +319,7 @@ class JWImport(Document):
|
||||
'account': rec.account
|
||||
},
|
||||
fields=['name','parent'])
|
||||
if not jeas: return False
|
||||
jea_parents= [jea.parent for jea in jeas]
|
||||
return frappe.db.exists({
|
||||
'doctype': 'Journal Entry Account',
|
||||
@ -307,7 +333,130 @@ def get_employee_allocation(emp, date, cache):
|
||||
alloc = frappe.get_all('JW Employee Allocation',
|
||||
filters={'employee': emp, 'docstatus': 1},
|
||||
fields=['name','percentage','start_date','end_date'])
|
||||
alloc = [r for r in alloc if (not r.start_date or date >= r.start_date)
|
||||
alloc = [frappe.get_doc('JW Employee Allocation',r.name) for r
|
||||
in alloc if (not r.start_date or date >= r.start_date)
|
||||
and (not r.end_date or date <= r.end_date)]
|
||||
cache[emp] = alloc
|
||||
return alloc
|
||||
|
||||
@frappe.whitelist()
|
||||
def record_entries(doc):
|
||||
importer = frappe.get_doc(json.loads(doc))
|
||||
n_detail = len(importer.lines)
|
||||
n_entries = 0
|
||||
last_l = n_detail - 1
|
||||
current_jeas = []
|
||||
current_total = 0.0
|
||||
acc_dims = get_accounting_dimensions()
|
||||
acc_dims.append('cost_center')
|
||||
rebalance_dims = [dim for dim in acc_dims if importer.get(dim,'')]
|
||||
current_dim_imbalance = defaultdict(float)
|
||||
standard_dims = {dim:importer.get(dim,'') for dim in acc_dims}
|
||||
standard_alloc = [dict(standard_dims, percentage=100)]
|
||||
standard_remark = 'Transfer to ' + ",".join(f"{d} {v}"
|
||||
for (d,v) in standard_dims.items())
|
||||
# The following makes a tuple from a comprehension:
|
||||
standard_key = *((dim,standard_dims[dim]) for dim in rebalance_dims),
|
||||
alloc_cache = {}
|
||||
je_remarks = {field:'' for field in importer.je_remark_fields}
|
||||
for l in range(n_detail):
|
||||
rec = importer.lines[l]
|
||||
if rec.disposition == 'Journal':
|
||||
rec_amt =flt(rec.amount)
|
||||
current_total += rec_amt
|
||||
for field in importer.je_remark_fields:
|
||||
if rec.get(field,''): je_remarks[field] = rec.get(field)
|
||||
jea_remarks = "\n".join(f"{f}: {rec.get(f)}"
|
||||
for f in importer.jea_remark_fields
|
||||
if rec.get(f,''))
|
||||
alloc = standard_alloc
|
||||
if rec.reference_doctype == 'Employee':
|
||||
alloc = get_employee_allocation(
|
||||
rec.reference_doc,
|
||||
get_datetime(rec.invoice_date).date(),
|
||||
alloc_cache)
|
||||
so_far = 0.0
|
||||
for s in range(len(alloc)-1, -1, -1):
|
||||
share = alloc[s]
|
||||
amnt = int(rec_amt*share.percentage)/100.0
|
||||
if s == 0: amnt = rec_amt - so_far
|
||||
so_far += amnt
|
||||
if importer.dim_balance_account:
|
||||
imb_key = *((dim,share.get(dim))
|
||||
for dim in rebalance_dims),
|
||||
if imb_key != standard_key:
|
||||
current_dim_imbalance[imb_key] += amnt
|
||||
current_jeas.append(dict(
|
||||
{d:share.get(d) for d in acc_dims},
|
||||
account=rec.account,
|
||||
party_type=rec.reference_doctype,
|
||||
party=rec.reference_doc,
|
||||
account_currency=importer.justworks_currency,
|
||||
debit_in_account_currency=amnt,
|
||||
credit_in_account_currency=0.0,
|
||||
user_remark=jea_remarks))
|
||||
rec.set('disposition', 'Duplicate')
|
||||
# Should we emit a journal entry?
|
||||
if (current_total > 0.0 and (l == last_l
|
||||
or importer.lines[l+1].invoice_id != rec.invoice_id)):
|
||||
je_remarks = "\n".join(f"{f}: {je_remarks[f]}"
|
||||
for f in importer.je_remark_fields
|
||||
if je_remarks[f])
|
||||
je = frappe.get_doc(doctype='Journal Entry',
|
||||
title='Justworks Invoice '+rec.invoice_date,
|
||||
voucher_type='Journal Entry',
|
||||
company=importer.justworks_company,
|
||||
posting_date=rec.invoice_date,
|
||||
cheque_no=rec.invoice_id,
|
||||
cheque_date=rec.invoice_date,
|
||||
user_remark=je_remarks,
|
||||
pay_to_recd_from='Justworks')
|
||||
for jeaf in current_jeas:
|
||||
jea = je.append('accounts', {})
|
||||
for f in jeaf: jea.set(f, jeaf[f])
|
||||
# Now generate "other sides":
|
||||
if importer.dim_balance_account:
|
||||
for dimvec in current_dim_imbalance:
|
||||
if not current_dim_imbalance[dimvec] > 0.0:
|
||||
continue
|
||||
jea = je.append('accounts', {})
|
||||
jea.set('account',
|
||||
importer.dim_balance_account)
|
||||
for (dim, val) in dimvec:
|
||||
jea.set(dim, val)
|
||||
jea.set('account_currency',
|
||||
importer.justworks_currency)
|
||||
jea.set('debit_in_account_currency', 0.0)
|
||||
jea.set('credit_in_account_currency',
|
||||
current_dim_imbalance[dimvec])
|
||||
jea.set('user_remark', standard_remark)
|
||||
oja = je.append('accounts', {})
|
||||
oja.set('account',
|
||||
importer.dim_balance_account)
|
||||
for (dim,val) in standard_dims.items():
|
||||
oja.set(dim,val)
|
||||
oja.set('account_currency',
|
||||
importer.justworks_currency)
|
||||
oja.set('debit_in_account_currency',
|
||||
current_dim_imbalance[dimvec])
|
||||
oja.set('credit_in_account_currency', 0.0)
|
||||
oja.set('user_remark', standard_remark)
|
||||
bja = je.append('accounts', {})
|
||||
bja.set('account', importer.jw_obligation_account)
|
||||
for (dim,val) in standard_dims.items():
|
||||
bja.set(dim,val)
|
||||
bja.set('account_currency', importer.justworks_currency)
|
||||
bja.set('debit_in_account_currency', 0.0)
|
||||
bja.set('credit_in_account_currency', current_total)
|
||||
bja.set('user_remark', 'Total Justworks expenses')
|
||||
je.insert()
|
||||
if importer.auto_submit: je.submit()
|
||||
n_entries += 1
|
||||
current_jeas = []
|
||||
current_total = 0.0
|
||||
for k in current_dim_imbalance:
|
||||
current_dim_imbalance[k] = 0.0
|
||||
je_remarks = {field:'' for field
|
||||
in importer.je_remark_fields}
|
||||
importer.save()
|
||||
frappe.msgprint(f"Entries created: {n_entries}", "Justworks Import");
|
||||
|
@ -1 +1,2 @@
|
||||
frappe
|
||||
erpnext
|
||||
|
Loading…
Reference in New Issue
Block a user