Merge pull request #31647 from frappe/version-13-hotfix

chore: weekly version-13 release
This commit is contained in:
Deepesh Garg
2022-07-20 20:49:09 +05:30
committed by GitHub
53 changed files with 1275 additions and 318 deletions

View File

@@ -19,6 +19,7 @@
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"post_change_gl_entries",
"enable_discount_accounting",
"tax_settings_section",
@@ -276,14 +277,21 @@
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"
}
},
{
"default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account"
}
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-10-11 17:42:36.427699",
"modified": "2022-07-11 13:37:50.605141",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -43,7 +43,7 @@ class GLEntry(Document):
self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center()
if not self.flags.from_repost:
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.check_mandatory()
self.validate_cost_center()
self.check_pl_account()
@@ -52,7 +52,7 @@ class GLEntry(Document):
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()

View File

@@ -25,7 +25,10 @@ from erpnext.accounts.utils import (
get_stock_and_account_balance,
)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.hr.doctype.expense_claim.expense_claim import (
get_outstanding_amount_for_claim,
update_reimbursed_amount,
)
class StockAccountInvalidTransaction(frappe.ValidationError):
@@ -935,15 +938,12 @@ class JournalEntry(AccountsController):
def validate_expense_claim(self):
for d in self.accounts:
if d.reference_type == "Expense Claim":
sanctioned_amount, reimbursed_amount = frappe.db.get_value(
"Expense Claim", d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed")
)
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
if d.debit > pending_amount:
outstanding_amt = get_outstanding_amount_for_claim(d.reference_name)
if d.debit > outstanding_amt:
frappe.throw(
_(
"Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}"
).format(d.idx, d.reference_name, pending_amount)
"Row No {0}: Amount cannot be greater than the Outstanding Amount against Expense Claim {1}. Outstanding Amount is {2}"
).format(d.idx, d.reference_name, outstanding_amt)
)
def validate_credit_debit_note(self):

View File

@@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
doc: frm.doc,
btn: $(btn_primary),
method: "make_invoices",
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
freeze: 1,
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
callback: function(r) {
if (r.message.length == 1) {
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
} else if (r.message.length < 50) {
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
}
}
});
});

View File

@@ -30,7 +30,10 @@ from erpnext.controllers.accounts_controller import (
get_supplier_block_status,
validate_taxes_and_charges,
)
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.hr.doctype.expense_claim.expense_claim import (
get_outstanding_amount_for_claim,
update_reimbursed_amount,
)
from erpnext.setup.utils import get_exchange_rate
@@ -1649,12 +1652,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
outstanding_amount = (
flt(ref_doc.get("total_sanctioned_amount"))
+ flt(ref_doc.get("total_taxes_and_charges"))
- flt(ref_doc.get("total_amount_reimbursed"))
- flt(ref_doc.get("total_advance_amount"))
)
outstanding_amount = get_outstanding_amount_for_claim(ref_doc)
elif reference_doctype == "Employee Advance":
outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
if party_account_currency != ref_doc.currency:

View File

@@ -10,10 +10,11 @@
"fiscal_year",
"amended_from",
"company",
"cost_center_wise_pnl",
"column_break1",
"closing_account_head",
"remarks"
"remarks",
"gle_processing_status",
"error_message"
],
"fields": [
{
@@ -86,17 +87,26 @@
"reqd": 1
},
{
"default": "0",
"fieldname": "cost_center_wise_pnl",
"fieldtype": "Check",
"label": "Book Cost Center Wise Profit/Loss"
"depends_on": "eval:doc.docstatus!=0",
"fieldname": "gle_processing_status",
"fieldtype": "Select",
"label": "GL Entry Processing Status",
"options": "In Progress\nCompleted\nFailed",
"read_only": 1
},
{
"depends_on": "eval:doc.gle_processing_status=='Failed'",
"fieldname": "error_message",
"fieldtype": "Text",
"label": "Error Message",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-05-20 15:27:37.210458",
"modified": "2022-07-20 14:51:04.714154",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Period Closing Voucher",

View File

@@ -8,7 +8,6 @@ from frappe.utils import flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimensions,
)
from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController
@@ -20,13 +19,28 @@ class PeriodClosingVoucher(AccountsController):
self.validate_posting_date()
def on_submit(self):
self.db_set("gle_processing_status", "In Progress")
self.make_gl_entries()
def on_cancel(self):
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
from erpnext.accounts.general_ledger import make_reverse_gl_entries
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
)
if gle_count > 5000:
frappe.enqueue(
make_reverse_gl_entries,
voucher_type="Period Closing Voucher",
voucher_no=self.name,
queue="long",
)
frappe.msgprint(
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
)
else:
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
def validate_account_head(self):
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
@@ -67,90 +81,80 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self):
gl_entries = self.get_gl_entries()
if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
if len(gl_entries) > 5000:
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True,
)
else:
process_gl_entries(gl_entries)
def get_gl_entries(self):
gl_entries = []
pl_accounts = self.get_pl_balances()
for acc in pl_accounts:
# pl account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
if flt(acc.bal_in_company_currency):
gl_entries.append(
self.get_gl_dict(
{
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) > 0
else 0,
},
item=acc,
)
)
gl_entries.append(self.get_gle_for_pl_account(acc))
if gl_entries:
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
gl_entries += gle_for_net_pl_bal
# closing liability account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_closing_account(acc))
return gl_entries
def get_pnl_gl_entry(self, pl_accounts):
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entries = []
def get_gle_for_pl_account(self, acc):
gl_entry = self.get_gl_dict(
{
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
},
item=acc,
)
self.update_default_dimensions(gl_entry, acc)
return gl_entry
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
gl_entry = self.get_gl_dict(
{
"account": self.closing_account_head,
"cost_center": cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) > 0
else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
},
item=acc,
)
def get_gle_for_closing_account(self, acc):
gl_entry = self.get_gl_dict(
{
"account": self.closing_account_head,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
},
item=acc,
)
self.update_default_dimensions(gl_entry, acc)
return gl_entry
self.update_default_dimensions(gl_entry)
gl_entries.append(gl_entry)
return gl_entries
def update_default_dimensions(self, gl_entry):
def update_default_dimensions(self, gl_entry, acc):
if not self.accounting_dimensions:
self.accounting_dimensions = get_accounting_dimensions()
_, default_dimensions = get_dimensions()
for dimension in self.accounting_dimensions:
gl_entry.update({dimension: default_dimensions.get(self.company, {}).get(dimension)})
gl_entry.update({dimension: acc.get(dimension)})
def get_pl_balances(self):
def get_pl_balances_based_on_dimensions(self, group_by_account=False):
"""Get balance for dimension-wise pl accounts"""
dimension_fields = ["t1.cost_center", "t1.finance_book"]
@@ -159,20 +163,56 @@ class PeriodClosingVoucher(AccountsController):
for dimension in self.accounting_dimensions:
dimension_fields.append("t1.{0}".format(dimension))
if group_by_account:
dimension_fields.append("t1.account")
return frappe.db.sql(
"""
select
t1.account, t2.account_currency, {dimension_fields},
t2.account_currency,
{dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2 and t2.company = %s
and t1.posting_date between %s and %s
group by t1.account, {dimension_fields}
where
t1.is_cancelled = 0
and t1.account = t2.name
and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2
and t2.company = %s
and t1.posting_date between %s and %s
group by {dimension_fields}
""".format(
dimension_fields=", ".join(dimension_fields)
),
(self.company, self.get("year_start_date"), self.posting_date),
as_dict=1,
)
def process_gl_entries(gl_entries):
from erpnext.accounts.general_ledger import make_gl_entries
try:
make_gl_entries(gl_entries, merge_entries=False)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
)
def make_reverse_gl_entries(voucher_type, voucher_no):
from erpnext.accounts.general_ledger import make_reverse_gl_entries
try:
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")

View File

@@ -49,7 +49,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expected_gle = (
("Cost of Goods Sold - TPC", 0.0, 600.0),
(surplus_account, 600.0, 400.0),
(surplus_account, 200.0, 0.0),
("Sales - TPC", 400.0, 0.0),
)
@@ -59,7 +59,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
""",
(pcv.name),
)
pcv.reload()
self.assertEqual(pcv.gle_processing_status, "Completed")
self.assertEqual(pcv_gle, expected_gle)
def test_cost_center_wise_posting(self):
@@ -94,7 +95,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
)
pcv = self.make_period_closing_voucher(submit=False)
pcv.cost_center_wise_pnl = 1
pcv.save()
pcv.submit()
surplus_account = pcv.closing_account_head
@@ -117,6 +117,16 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(pcv_gle, expected_gle)
pcv.reload()
pcv.cancel()
self.assertFalse(
frappe.db.get_value(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": pcv.name, "is_cancelled": 0},
)
)
def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")

View File

@@ -148,6 +148,7 @@ def update_net_values(entry):
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
for entry in gl_map:
# if there is already an entry in this account then just add it
# to that entry
@@ -229,9 +230,10 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.from_repost = from_repost
gle.flags.adv_adj = adv_adj
gle.flags.update_outstanding = update_outstanding or "Yes"
gle.flags.notify_update = False
gle.submit()
if not from_repost:
if not from_repost and gle.voucher_type != "Period Closing Voucher":
validate_expense_against_budget(args)

View File

@@ -8,11 +8,11 @@ from frappe import _
def execute(filters=None):
validate_filters(filters)
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters)
columns = get_columns(filters)
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map)
return columns, res
@@ -22,10 +22,11 @@ def validate_filters(filters):
frappe.throw(_("From Date must be before To Date"))
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map):
supplier_map = get_supplier_pan_map()
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
print(journal_entry_party_map)
out = []
for name, details in gle_map.items():
@@ -38,6 +39,11 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if voucher_type == "Journal Entry":
suppliers = journal_entry_party_map.get(name)
if suppliers:
supplier = suppliers[0]
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category)
@@ -176,6 +182,7 @@ def get_tds_docs(filters):
journal_entries = []
tax_category_map = {}
or_filters = {}
journal_entry_party_map = {}
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
tds_accounts = frappe.get_all(
@@ -218,9 +225,24 @@ def get_tds_docs(filters):
get_tax_category_map(payment_entries, "Payment Entry", tax_category_map)
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
get_tax_category_map(journal_entries, "Journal Entry", tax_category_map)
return tds_documents, tds_accounts, tax_category_map
return tds_documents, tds_accounts, tax_category_map, journal_entry_party_map
def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
["parent", "party"],
):
if d.parent not in journal_entry_party_map:
journal_entry_party_map[d.parent] = []
journal_entry_party_map[d.parent].append(d.party)
return journal_entry_party_map
def get_tax_category_map(vouchers, doctype, tax_category_map):

View File

@@ -38,7 +38,6 @@
"purchase_date",
"section_break_23",
"calculate_depreciation",
"allow_monthly_depreciation",
"column_break_33",
"opening_accumulated_depreciation",
"number_of_depreciations_booked",
@@ -454,13 +453,6 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "calculate_depreciation",
"fieldname": "allow_monthly_depreciation",
"fieldtype": "Check",
"label": "Allow Monthly Depreciation"
},
{
"collapsible": 1,
"collapsible_depends_on": "is_existing_asset",
@@ -503,7 +495,7 @@
"link_fieldname": "asset"
}
],
"modified": "2022-01-30 20:19:24.680027",
"modified": "2022-07-20 16:22:44.437579",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -545,4 +537,4 @@
"sort_order": "DESC",
"title_field": "asset_name",
"track_changes": 1
}
}

View File

@@ -353,61 +353,16 @@ class Asset(AccountsController):
skip_row = True
if depreciation_amount > 0:
# With monthly depreciation, each depreciation is divided by months remaining until next date
if self.allow_monthly_depreciation:
# month range is 1 to 12
# In pro rata case, for first and last depreciation, month range would be different
if (has_pro_rata and n == 0 and not self.number_of_depreciations_booked) or (
has_pro_rata and n == cint(number_of_pending_depreciations) - 1
):
month_range = months
else:
month_range = finance_book.frequency_of_depreciation
for r in range(month_range):
if has_pro_rata and n == 0 and not self.number_of_depreciations_booked:
# For first entry of monthly depr
if r == 0:
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1
per_day_amt = depreciation_amount / days
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
depreciation_amount -= depreciation_amount_for_current_month
date = monthly_schedule_date
amount = depreciation_amount_for_current_month
else:
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / (month_range - 1)
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(
month_range
) - 1:
# For last entry of monthly depr
date = last_schedule_date
amount = depreciation_amount / month_range
else:
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / month_range
self.append(
"schedules",
{
"schedule_date": date,
"depreciation_amount": amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx,
},
)
else:
self.append(
"schedules",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx,
},
)
self.append(
"schedules",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx,
},
)
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
@@ -853,7 +808,7 @@ class Asset(AccountsController):
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
return 100 * (1 - flt(depreciation_rate, float_precision))
return flt((100 * (1 - depreciation_rate)), float_precision)
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)

View File

@@ -145,6 +145,7 @@ class TestAsset(AssetSetup):
def test_is_fixed_asset_set(self):
asset = create_asset(is_existing_asset=1)
doc = frappe.new_doc("Purchase Invoice")
doc.company = "_Test Company"
doc.supplier = "_Test Supplier"
doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name})
@@ -702,6 +703,8 @@ class TestDepreciationMethods(AssetSetup):
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
def test_monthly_depreciation_by_wdv_method(self):
existing_precision = frappe.db.get_default("float_precision")
frappe.db.set_default("float_precision", 3)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2022-02-15",
@@ -715,12 +718,12 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2022-02-28", 645.0, 645.0],
["2022-03-31", 1206.8, 1851.8],
["2022-04-30", 1051.12, 2902.92],
["2022-05-31", 915.52, 3818.44],
["2022-06-30", 797.42, 4615.86],
["2022-07-15", 384.14, 5000.0],
["2022-02-28", 647.25, 647.25],
["2022-03-31", 1210.71, 1857.96],
["2022-04-30", 1053.99, 2911.95],
["2022-05-31", 917.55, 3829.5],
["2022-06-30", 798.77, 4628.27],
["2022-07-15", 371.73, 5000.0],
]
schedules = [
@@ -731,8 +734,8 @@ class TestDepreciationMethods(AssetSetup):
]
for d in asset.get("schedules")
]
self.assertEqual(schedules, expected_schedules)
frappe.db.set_default("float_precision", existing_precision)
class TestDepreciationBasics(AssetSetup):

View File

@@ -1470,8 +1470,15 @@ class AccountsController(TransactionBase):
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
)
party_account_currency = get_account_currency(party_account)
allow_multi_currency_invoices_against_single_party_account = frappe.db.get_singles_value(
"Accounts Settings", "allow_multi_currency_invoices_against_single_party_account"
)
if not party_gle_currency and (party_account_currency != self.currency):
if (
not party_gle_currency
and (party_account_currency != self.currency)
and not allow_multi_currency_invoices_against_single_party_account
):
frappe.throw(
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
frappe.bold(party_account), party_account_currency, self.currency

View File

@@ -355,6 +355,8 @@ class Subcontracting:
rm_obj.purchase_order = item_row.purchase_order
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
rm_obj.amount = flt(rm_obj.required_qty) * flt(rm_obj.rate)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)

View File

@@ -483,12 +483,12 @@ scheduler_events = {
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
],
"hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
],
"daily": [
"erpnext.support.doctype.issue.issue.auto_close_tickets",

View File

@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2021-06-17 11:31:37.730760",
"modified": "2022-07-18 20:03:43.188705",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@@ -349,7 +349,9 @@ def get_employee_email(employee_doc):
def get_holiday_list_for_employee(employee, raise_exception=True):
if employee:
holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"])
holiday_list, company = frappe.get_cached_value(
"Employee", employee, ["holiday_list", "company"]
)
else:
holiday_list = ""
company = frappe.db.get_value("Global Defaults", None, "default_company")

View File

@@ -26,7 +26,8 @@
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "employee.employee_name",
@@ -48,7 +49,8 @@
"fieldtype": "Link",
"label": "Shift",
"options": "Shift Type",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "column_break_4",
@@ -107,7 +109,7 @@
}
],
"links": [],
"modified": "2020-07-08 11:02:32.660986",
"modified": "2022-07-19 15:38:41.767539",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Checkin",

View File

@@ -131,7 +131,7 @@ def mark_attendance_and_link_log(
return None
elif attendance_status in ("Present", "Absent", "Half Day"):
employee_doc = frappe.get_doc("Employee", employee)
company = frappe.get_cached_value("Employee", employee, "company")
duplicate = frappe.db.exists(
"Attendance",
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
@@ -144,7 +144,7 @@ def mark_attendance_and_link_log(
"attendance_date": attendance_date,
"status": attendance_status,
"working_hours": working_hours,
"company": employee_doc.company,
"company": company,
"shift": shift,
"late_entry": late_entry,
"early_exit": early_exit,

View File

@@ -339,6 +339,30 @@ def update_reimbursed_amount(doc, amount):
frappe.db.set_value("Expense Claim", doc.name, "status", doc.status)
def get_outstanding_amount_for_claim(claim):
if isinstance(claim, str):
claim = frappe.db.get_value(
"Expense Claim",
claim,
(
"total_sanctioned_amount",
"total_taxes_and_charges",
"total_amount_reimbursed",
"total_advance_amount",
),
as_dict=True,
)
outstanding_amt = (
flt(claim.total_sanctioned_amount)
+ flt(claim.total_taxes_and_charges)
- flt(claim.total_amount_reimbursed)
- flt(claim.total_advance_amount)
)
return outstanding_amt
@frappe.whitelist()
def make_bank_entry(dt, dn):
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
@@ -348,11 +372,7 @@ def make_bank_entry(dt, dn):
if not default_bank_cash_account:
default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash")
payable_amount = (
flt(expense_claim.total_sanctioned_amount)
- flt(expense_claim.total_amount_reimbursed)
- flt(expense_claim.total_advance_amount)
)
payable_amount = get_outstanding_amount_for_claim(expense_claim)
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Bank Entry"

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate, random_string
from erpnext.accounts.doctype.account.test_account import create_account
@@ -14,9 +15,18 @@ test_dependencies = ["Employee"]
company_name = "_Test Company 3"
class TestExpenseClaim(unittest.TestCase):
def tearDown(self):
frappe.db.rollback()
class TestExpenseClaim(FrappeTestCase):
def setUp(self):
if not frappe.db.get_value("Cost Center", {"company": company_name}):
frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "_Test Cost Center 3",
"parent_cost_center": "_Test Company 3 - _TC3",
"is_group": 0,
"company": company_name,
}
).insert()
def test_total_expense_claim_for_project(self):
frappe.db.sql("""delete from `tabTask`""")
@@ -58,12 +68,7 @@ class TestExpenseClaim(unittest.TestCase):
payable_account, 300, 200, company_name, "Travel Expenses - _TC3"
)
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict)
je.posting_date = nowdate()
je.cheque_no = random_string(5)
je.cheque_date = nowdate()
je.submit()
je = make_journal_entry(expense_claim)
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
self.assertEqual(expense_claim.status, "Paid")
@@ -272,6 +277,24 @@ class TestExpenseClaim(unittest.TestCase):
self.assertEqual(outstanding_amount, 0)
self.assertEqual(total_amount_reimbursed, 5500)
def test_journal_entry_against_expense_claim(self):
payable_account = get_payable_account(company_name)
taxes = generate_taxes()
expense_claim = make_expense_claim(
payable_account,
300,
200,
company_name,
"Travel Expenses - _TC3",
do_not_submit=True,
taxes=taxes,
)
expense_claim.submit()
je = make_journal_entry(expense_claim)
self.assertEqual(je.accounts[0].debit_in_account_currency, expense_claim.grand_total)
def get_payable_account(company):
return frappe.get_cached_value("Company", company, "default_payable_account")
@@ -370,3 +393,14 @@ def make_payment_entry(expense_claim, payable_account, amt):
pe.references[0].allocated_amount = amt
pe.insert()
pe.submit()
def make_journal_entry(expense_claim):
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict)
je.posting_date = nowdate()
je.cheque_no = random_string(5)
je.cheque_date = nowdate()
je.submit()
return je

View File

@@ -115,6 +115,8 @@ def is_holiday(holiday_list, date=None):
if date is None:
date = today()
if holiday_list:
return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date)))
return bool(
frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True)
)
else:
return False

View File

@@ -25,7 +25,8 @@
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "employee.employee_name",
@@ -48,7 +49,8 @@
"in_list_view": 1,
"label": "Shift Type",
"options": "Shift Type",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fieldname": "column_break_3",
@@ -105,7 +107,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-06-15 14:27:54.310773",
"modified": "2022-07-19 15:27:54.310773",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Assignment",

View File

@@ -169,7 +169,7 @@ def get_employee_shift(
"""
if for_date is None:
for_date = nowdate()
default_shift = frappe.db.get_value("Employee", employee, "default_shift")
default_shift = frappe.get_cached_value("Employee", employee, "default_shift")
shift_type_name = None
shift_assignment_details = frappe.db.get_value(
"Shift Assignment",
@@ -187,7 +187,7 @@ def get_employee_shift(
if not shift_type_name and consider_default_shift:
shift_type_name = default_shift
if shift_type_name:
holiday_list_name = frappe.db.get_value("Shift Type", shift_type_name, "holiday_list")
holiday_list_name = frappe.get_cached_value("Shift Type", shift_type_name, "holiday_list")
if not holiday_list_name:
holiday_list_name = get_holiday_list_for_employee(employee, False)
if holiday_list_name and is_holiday(holiday_list_name, for_date):
@@ -294,7 +294,18 @@ def get_shift_details(shift_type_name, for_date=None):
return None
if not for_date:
for_date = nowdate()
shift_type = frappe.get_doc("Shift Type", shift_type_name)
shift_type = frappe.get_cached_value(
"Shift Type",
shift_type_name,
[
"name",
"start_time",
"end_time",
"begin_check_in_before_shift_start_time",
"allow_check_out_after_shift_end_time",
],
as_dict=1,
)
start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
for_date = (
for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date

View File

@@ -107,7 +107,7 @@ class ShiftType(Document):
"""Marks Absents for the given employee on working days in this shift which have no attendance marked.
The Absent is marked starting from 'process_attendance_after' or employee creation date.
"""
date_of_joining, relieving_date, employee_creation = frappe.db.get_value(
date_of_joining, relieving_date, employee_creation = frappe.get_cached_value(
"Employee", employee, ["date_of_joining", "relieving_date", "creation"]
)
if not date_of_joining:
@@ -156,21 +156,19 @@ class ShiftType(Document):
if not from_date:
del filters["start_date"]
assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True)
assigned_employees = [x[0] for x in assigned_employees]
assigned_employees = frappe.get_all("Shift Assignment", filters, pluck="employee")
if consider_default_shift:
filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True)
default_shift_employees = [x[0] for x in default_shift_employees]
default_shift_employees = frappe.get_all("Employee", filters, pluck="name")
return list(set(assigned_employees + default_shift_employees))
return assigned_employees
def process_auto_attendance_for_all_shifts():
shift_list = frappe.get_all("Shift Type", "name", {"enable_auto_attendance": "1"}, as_list=True)
shift_list = frappe.get_all("Shift Type", filters={"enable_auto_attendance": "1"}, pluck="name")
for shift in shift_list:
doc = frappe.get_doc("Shift Type", shift[0])
doc = frappe.get_cached_doc("Shift Type", shift)
doc.process_auto_attendance()

View File

@@ -48,6 +48,10 @@
"total_payment",
"total_principal_paid",
"written_off_amount",
"refund_amount",
"debit_adjustment_amount",
"credit_adjustment_amount",
"is_npa",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
@@ -379,12 +383,39 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "refund_amount",
"fieldtype": "Currency",
"label": "Refund amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "credit_adjustment_amount",
"fieldtype": "Currency",
"label": "Credit Adjustment Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "debit_adjustment_amount",
"fieldtype": "Currency",
"label": "Debit Adjustment Amount",
"options": "Company:company:default_currency"
},
{
"default": "0",
"description": "Mark Loan as a Nonperforming asset",
"fieldname": "is_npa",
"fieldtype": "Check",
"label": "Is NPA"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-03-10 11:50:31.957360",
"modified": "2022-06-30 12:04:13.728880",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Balance Adjustment', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,189 @@
{
"actions": [],
"autoname": "LM-ADJ-.#####",
"creation": "2022-06-28 14:48:47.736269",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"adjustment_account",
"column_break_11",
"adjustment_type",
"amount",
"reference_number",
"remarks",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Adjustment Details"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Balance Adjustment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Balance Adjustment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "adjustment_account",
"fieldtype": "Link",
"label": "Adjustment Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "adjustment_type",
"fieldtype": "Select",
"label": "Adjustment Type",
"options": "Credit Adjustment\nDebit Adjustment",
"reqd": 1
},
{
"fieldname": "remarks",
"fieldtype": "Data",
"label": "Remarks"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-07-08 16:48:54.480066",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Balance Adjustment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,143 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import add_days, nowdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_demand_loans,
)
class LoanBalanceAdjustment(AccountsController):
"""
Add credit/debit adjustments to loan ledger.
"""
def validate(self):
if self.amount == 0:
frappe.throw(_("Amount cannot be zero"))
if self.amount < 0:
frappe.throw(_("Amount cannot be negative"))
self.set_missing_values()
def on_submit(self):
self.set_status_and_amounts()
self.make_gl_entries()
def on_cancel(self):
self.set_status_and_amounts(cancel=1)
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def set_missing_values(self):
if not self.posting_date:
self.posting_date = nowdate()
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def set_status_and_amounts(self, cancel=0):
loan_details = frappe.db.get_value(
"Loan",
self.loan,
[
"loan_amount",
"credit_adjustment_amount",
"debit_adjustment_amount",
"total_payment",
"total_principal_paid",
"total_interest_payable",
"status",
"is_term_loan",
"is_secured_loan",
],
as_dict=1,
)
if cancel:
adjustment_amount = self.get_values_on_cancel(loan_details)
else:
adjustment_amount = self.get_values_on_submit(loan_details)
if self.adjustment_type == "Credit Adjustment":
adj_field = "credit_adjustment_amount"
elif self.adjustment_type == "Debit Adjustment":
adj_field = "debit_adjustment_amount"
frappe.db.set_value("Loan", self.loan, {adj_field: adjustment_amount})
def get_values_on_cancel(self, loan_details):
if self.adjustment_type == "Credit Adjustment":
adjustment_amount = loan_details.credit_adjustment_amount - self.amount
elif self.adjustment_type == "Debit Adjustment":
adjustment_amount = loan_details.debit_adjustment_amount - self.amount
return adjustment_amount
def get_values_on_submit(self, loan_details):
if self.adjustment_type == "Credit Adjustment":
adjustment_amount = loan_details.credit_adjustment_amount + self.amount
elif self.adjustment_type == "Debit Adjustment":
adjustment_amount = loan_details.debit_adjustment_amount + self.amount
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(
posting_date=add_days(self.posting_date, -1),
loan=self.loan,
accrual_type=self.adjustment_type,
)
return adjustment_amount
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_account = frappe.db.get_value("Loan", self.loan, "loan_account")
remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan)
if self.reference_number:
remarks += "with reference no. {}".format(self.reference_number)
loan_entry = {
"account": loan_account,
"against": self.adjustment_account,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.posting_date,
}
company_entry = {
"account": self.adjustment_account,
"against": loan_account,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": self.posting_date,
}
if self.adjustment_type == "Credit Adjustment":
loan_entry["credit"] = self.amount
loan_entry["credit_in_account_currency"] = self.amount
company_entry["debit"] = self.amount
company_entry["debit_in_account_currency"] = self.amount
elif self.adjustment_type == "Debit Adjustment":
loan_entry["debit"] = self.amount
loan_entry["debit_in_account_currency"] = self.amount
company_entry["credit"] = self.amount
company_entry["credit_in_account_currency"] = self.amount
gle_map.append(self.get_gl_dict(loan_entry))
gle_map.append(self.get_gl_dict(company_entry))
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLoanBalanceAdjustment(FrappeTestCase):
pass

View File

@@ -35,12 +35,15 @@
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan",
"options": "Loan"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date"
},
{
@@ -75,6 +78,8 @@
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type"
},
@@ -158,8 +163,11 @@
{
"fieldname": "accrual_type",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement"
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund"
},
{
"fieldname": "penalty_amount",
@@ -185,10 +193,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:26:38.871889",
"modified": "2022-06-30 11:51:31.911794",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -225,5 +234,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Refund', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,176 @@
{
"actions": [],
"autoname": "LM-RF-.#####",
"creation": "2022-06-24 15:51:03.165498",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"refund_account",
"column_break_11",
"refund_amount",
"reference_number",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Refund Details"
},
{
"fieldname": "refund_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Refund Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "refund_amount",
"fieldtype": "Currency",
"label": "Refund Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Refund",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Refund",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-06-24 16:13:48.793486",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Refund",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,97 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import getdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
class LoanRefund(AccountsController):
"""
Add refund if total repayment is more than that is owed.
"""
def validate(self):
self.set_missing_values()
self.validate_refund_amount()
def set_missing_values(self):
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def validate_refund_amount(self):
loan = frappe.get_doc("Loan", self.loan)
pending_amount = get_pending_principal_amount(loan)
if pending_amount >= 0:
frappe.throw(_("No excess amount to refund."))
else:
excess_amount = pending_amount * -1
if self.refund_amount > excess_amount:
frappe.throw(_("Refund amount cannot be greater than excess amount {0}").format(excess_amount))
def on_submit(self):
self.update_outstanding_amount()
self.make_gl_entries()
def on_cancel(self):
self.update_outstanding_amount(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1)
def update_outstanding_amount(self, cancel=0):
refund_amount = frappe.db.get_value("Loan", self.loan, "refund_amount")
if cancel:
refund_amount -= self.refund_amount
else:
refund_amount += self.refund_amount
frappe.db.set_value("Loan", self.loan, "refund_amount", refund_amount)
def make_gl_entries(self, cancel=0):
gl_entries = []
loan_details = frappe.get_doc("Loan", self.loan)
gl_entries.append(
self.get_gl_dict(
{
"account": self.refund_account,
"against": loan_details.loan_account,
"credit": self.refund_amount,
"credit_in_account_currency": self.refund_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": self.refund_account,
"debit": self.refund_amount,
"debit_in_account_currency": self.refund_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLoanRefund(FrappeTestCase):
pass

View File

@@ -387,15 +387,19 @@ class LoanRepayment(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(
remarks = "Shortfall repayment of {0}.<br>Repayment against loan {1}".format(
self.shortfall_amount, self.against_loan
)
elif self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount)
remarks = "Shortfall repayment of {0} against loan {1}".format(
self.shortfall_amount, self.against_loan
)
else:
remarks = _("Repayment against Loan: ") + self.against_loan
remarks = "Repayment against loan " + self.against_loan
if self.reference_number:
remarks += "with reference no. {}".format(self.reference_number)
if self.repay_from_salary:
payment_account = self.payroll_payable_account
@@ -446,7 +450,7 @@ class LoanRepayment(AccountsController):
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
@@ -464,7 +468,7 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
@@ -624,16 +628,22 @@ def get_pending_principal_amount(loan):
if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
pending_principal_amount = (
flt(loan.total_payment)
+ flt(loan.debit_adjustment_amount)
- flt(loan.credit_adjustment_amount)
- flt(loan.total_principal_paid)
- flt(loan.total_interest_payable)
- flt(loan.written_off_amount)
+ flt(loan.refund_amount)
)
else:
pending_principal_amount = (
flt(loan.disbursed_amount)
+ flt(loan.debit_adjustment_amount)
- flt(loan.credit_adjustment_amount)
- flt(loan.total_principal_paid)
- flt(loan.total_interest_payable)
- flt(loan.written_off_amount)
+ flt(loan.refund_amount)
)
return pending_principal_amount

View File

@@ -7,6 +7,8 @@
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"status",
"column_break_3",
"shortfall_time",
@@ -23,6 +25,8 @@
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan ",
"options": "Loan",
"read_only": 1
@@ -91,17 +95,35 @@
{
"fieldname": "shortfall_percentage",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Shortfall Percentage",
"read_only": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer"
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-01 08:13:43.263772",
"modified": "2022-06-30 11:57:09.378089",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Shortfall",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -132,5 +154,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -54,17 +54,18 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement",
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-06 13:28:51.478909",
"modified": "2022-06-29 11:19:33.203088",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Process Loan Interest Accrual",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -98,5 +99,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -444,6 +444,7 @@ class BOM(WebsiteGenerator):
and self.is_active
):
frappe.db.set(self, "is_default", 1)
frappe.db.set_value("Item", self.item, "default_bom", self.name)
else:
frappe.db.set(self, "is_default", 0)
item = frappe.get_doc("Item", self.item)

View File

@@ -519,6 +519,42 @@ class TestBOM(FrappeTestCase):
new_bom.delete()
def test_set_default_bom_for_item_having_single_bom(self):
from erpnext.stock.doctype.item.test_item import make_item
fg_item = make_item(properties={"is_stock_item": 1})
bom_item = make_item(properties={"is_stock_item": 1})
# Step 1: Create BOM
bom = frappe.new_doc("BOM")
bom.item = fg_item.item_code
bom.quantity = 1
bom.append(
"items",
{
"item_code": bom_item.item_code,
"qty": 1,
"uom": bom_item.stock_uom,
"stock_uom": bom_item.stock_uom,
"rate": 100.0,
},
)
bom.save()
bom.submit()
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
# Step 2: Uncheck is_active field
bom.is_active = 0
bom.save()
bom.reload()
self.assertIsNone(frappe.get_value("Item", fg_item.item_code, "default_bom"))
# Step 3: Check is_active field
bom.is_active = 1
bom.save()
bom.reload()
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@@ -368,4 +368,5 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
erpnext.patches.v13_0.update_employee_advance_status
erpnext.patches.v13_0.job_card_status_on_hold
erpnext.patches.v13_0.add_cost_center_in_loans
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation

View File

@@ -33,7 +33,10 @@ def execute():
"insert_after": insert_after_field,
}
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)
try:
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)
except Exception as e:
pass
count += 1

View File

@@ -0,0 +1,16 @@
import frappe
def execute():
if not frappe.db.has_column("Asset", "allow_monthly_depreciation"):
return
assets = frappe.get_all("Asset", filters={"allow_monthly_depreciation": 1})
for d in assets:
print(d.name)
asset_doc = frappe.get_doc("Asset", d.name)
for i in asset_doc.get("finance_books"):
if i.frequency_of_depreciation != 1:
i.total_number_of_depreciations *= i.frequency_of_depreciation
i.frequency_of_depreciation = 1
i.db_update()

View File

@@ -144,7 +144,7 @@ class GSTR3BReport(Document):
def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql(
"""
SELECT p.place_of_supply, p.supplier_address,
SELECT p.name, p.place_of_supply, p.supplier_address, p.gst_category,
i.base_amount, i.is_nil_exempt, i.is_non_gst
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent

View File

@@ -65,6 +65,7 @@ def validate_eligibility(doc):
"SEZ",
"Overseas",
"Deemed Export",
"UIN Holders",
]
company_transaction = doc.get("billing_address_gstin") == doc.get("company_gstin")
@@ -130,9 +131,7 @@ def read_json(name):
def get_transaction_details(invoice):
supply_type = ""
if (
invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition"
):
if invoice.gst_category in ("Registered Regular", "Registered Composition", "UIN Holders"):
supply_type = "B2B"
elif invoice.gst_category == "SEZ":
if invoice.export_type == "Without Payment of Tax":
@@ -148,15 +147,18 @@ def get_transaction_details(invoice):
supply_type = "DEXP"
if not supply_type:
rr, rc, sez, overseas, export = (
rr, rc, sez, overseas, export, uin = (
bold("Registered Regular"),
bold("Registered Composition"),
bold("SEZ"),
bold("Overseas"),
bold("Deemed Export"),
bold("UIN Holders"),
)
frappe.throw(
_("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export),
_("GST category should be one of {}, {}, {}, {}, {}, {}").format(
rr, rc, sez, overseas, export, uin
),
title=_("Invalid Supply Type"),
)
@@ -272,45 +274,43 @@ def get_item_list(invoice):
item.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty)
item_qty = item.qty
item.discount_amount = abs(item.discount_amount)
item.taxable_value = abs(item.taxable_value)
if invoice.get("is_return") or invoice.get("is_debit_note"):
item_qty = item_qty or 1
hide_discount_in_einvoice = cint(
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
)
if hide_discount_in_einvoice:
if flt(item.qty) != 0.0:
item.unit_rate = abs(item.taxable_value / item.qty)
else:
item.unit_rate = abs(item.taxable_value)
item.gross_amount = abs(item.taxable_value)
item.taxable_value = abs(item.taxable_value)
item.unit_rate = item.taxable_value / item_qty
item.gross_amount = item.taxable_value
item.discount_amount = 0
else:
if invoice.get("apply_discount_on") and (abs(invoice.get("base_discount_amount") or 0.0) > 0.0):
# TODO: need to handle case when tax included in basic rate is checked.
item.discount_amount = (item.discount_amount * item.qty) + (
item.discount_amount = (item.discount_amount * item_qty) + (
abs(item.base_amount) - abs(item.base_net_amount)
)
else:
item.discount_amount = item.discount_amount * item.qty
item.discount_amount = item.discount_amount * item_qty
if invoice.get("is_return") or invoice.get("is_debit_note"):
item.unit_rate = (abs(item.taxable_value) + item.discount_amount) / (
1 if (item.qty == 0) else item.qty
try:
item.unit_rate = (item.taxable_value + item.discount_amount) / item_qty
except ZeroDivisionError:
# This will never run but added as safety measure
frappe.throw(
title=_("Error: Qty is Zero"),
msg=_("Quantity can't be zero unless it's Credit/Debit Note."),
)
else:
try:
item.unit_rate = abs(item.taxable_value + item.discount_amount) / item.qty
except ZeroDivisionError:
# This will never run but added as safety measure
frappe.throw(
title=_("Error: Qty is Zero"),
msg=_("Quantity can't be zero unless it's Credit/Debit Note."),
)
item.gross_amount = abs(item.taxable_value) + item.discount_amount
item.taxable_value = abs(item.taxable_value)
item.gross_amount = item.taxable_value + item.discount_amount
item.taxable_value = item.taxable_value
item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N"
item.serial_no = ""

View File

@@ -714,7 +714,7 @@ def get_custom_fields():
insert_after="customer",
no_copy=1,
print_hide=1,
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0',
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export", "UIN Holders"], doc.gst_category) && doc.irn_cancelled === 0',
),
dict(
fieldname="irn_cancelled",

View File

@@ -40,13 +40,12 @@ def validate_gstin_for_india(doc, method):
gst_category = []
if hasattr(doc, "gst_category"):
if len(doc.links):
link_doctype = doc.links[0].get("link_doctype")
link_name = doc.links[0].get("link_name")
if len(doc.links):
link_doctype = doc.links[0].get("link_doctype")
link_name = doc.links[0].get("link_name")
if link_doctype in ["Customer", "Supplier"]:
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
if link_doctype in ["Customer", "Supplier"]:
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
doc.gstin = doc.gstin.upper().strip()
if not doc.gstin or doc.gstin == "NA":

View File

@@ -4,7 +4,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, flt, now
from frappe.utils import add_days, add_to_date, flt, now
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -120,6 +120,61 @@ class TestLandedCostVoucher(FrappeTestCase):
expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}"
)
def test_landed_cost_voucher_stock_impact(self):
"Test impact of LCV on future stock balances."
from erpnext.stock.doctype.item.test_item import make_item
item = make_item("LCV Stock Item", {"is_stock_item": 1})
warehouse = "Stores - _TC"
pr1 = make_purchase_receipt(
item_code=item.name,
warehouse=warehouse,
qty=500,
rate=80,
posting_date=add_days(frappe.utils.nowdate(), -2),
)
pr2 = make_purchase_receipt(
item_code=item.name,
warehouse=warehouse,
qty=100,
rate=80,
posting_date=frappe.utils.nowdate(),
)
last_sle = frappe.db.get_value( # SLE of second PR
"Stock Ledger Entry",
{
"voucher_type": pr2.doctype,
"voucher_no": pr2.name,
"item_code": item.name,
"warehouse": warehouse,
"is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"],
as_dict=1,
)
create_landed_cost_voucher("Purchase Receipt", pr1.name, pr1.company)
last_sle_after_landed_cost = frappe.db.get_value( # SLE of second PR after LCV's effect
"Stock Ledger Entry",
{
"voucher_type": pr2.doctype,
"voucher_no": pr2.name,
"item_code": item.name,
"warehouse": warehouse,
"is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"],
as_dict=1,
)
self.assertEqual(
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice(
@@ -219,11 +274,11 @@ class TestLandedCostVoucher(FrappeTestCase):
landed costs, this should be allowed for serial nos too.
Case:
- receipt a serial no @ X rate
- delivery the serial no @ X rate
- add LCV to receipt X + Y
- LCV should be successful
- delivery should reflect X+Y valuation.
- receipt a serial no @ X rate
- delivery the serial no @ X rate
- add LCV to receipt X + Y
- LCV should be successful
- delivery should reflect X+Y valuation.
"""
serial_no = "LCV_TEST_SR_NO"
item_code = "_Test Serialized Item"

View File

@@ -94,27 +94,26 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
if not args.get("posting_date"):
args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after(
{
"item_code": args.get("item_code"),
"warehouse": args.get("warehouse"),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get("name"),
"creation": args.get("creation"),
},
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
)
if not (args.get("is_cancelled") and via_landed_cost_voucher):
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after(
{
"item_code": args.get("item_code"),
"warehouse": args.get("warehouse"),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get("name"),
"creation": args.get("creation"),
},
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
)
# update qty in future sle and Validate negative qty
# For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
update_qty_in_future_sle(args, allow_negative_stock)
@@ -208,8 +207,14 @@ def repost_future_sle(
allow_negative_stock=None,
via_landed_cost_voucher=False,
):
if not args and voucher_type and voucher_no:
args = get_items_to_be_repost(voucher_type, voucher_no, doc)
if not args:
args = [] # set args to empty list if None to avoid enumerate error
items_to_be_repost = get_items_to_be_repost(
voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
)
if items_to_be_repost:
args = items_to_be_repost
distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
affected_transactions = get_affected_transactions(doc)
@@ -276,17 +281,21 @@ def update_args_in_repost_item_valuation(
)
def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
items_to_be_repost = []
if doc and doc.items_to_be_repost:
return json.loads(doc.items_to_be_repost) or []
items_to_be_repost = json.loads(doc.items_to_be_repost) or []
return frappe.db.get_all(
"Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
group_by="item_code, warehouse",
)
if not items_to_be_repost and voucher_type and voucher_no:
items_to_be_repost = frappe.db.get_all(
"Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
group_by="item_code, warehouse",
)
return items_to_be_repost
def get_distinct_item_warehouse(args=None, doc=None):
@@ -486,7 +495,8 @@ class update_entries_after(object):
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
else:
self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
self.initialize_previous_data(dependant_sle)
self.update_distinct_item_warehouses(dependant_sle)
return entries_to_fix
def update_distinct_item_warehouses(self, dependant_sle):
@@ -504,14 +514,6 @@ class update_entries_after(object):
self.distinct_item_warehouses[key] = val
self.new_items_found = True
def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
self.initialize_previous_data(dependant_sle)
self.distinct_item_warehouses[(self.item_code, dependant_sle.warehouse)] = frappe._dict(
{"sle": dependant_sle}
)
self.new_items_found = True
def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos

View File

@@ -898,6 +898,7 @@ ERPNext Settings,ERPNext-Einstellungen,
Earliest,Frühestens,
Earnest Money,Anzahlung,
Earning,Einkommen,
Earnings & Deductions,Verdienste & Abzüge,
Edit,Bearbeiten,
Edit Publishing Details,Bearbeitungsdetails bearbeiten,
"Edit in full page for more options like assets, serial nos, batches etc.","Bearbeiten Sie in Vollansicht für weitere Optionen wie Vermögenswerte, Seriennummern, Chargen usw.",
Can't render this file because it is too large.