Merge branch 'develop' into lifo_valuation
This commit is contained in:
commit
befcca4183
44
cypress/integration/test_bulk_transaction_processing.js
Normal file
44
cypress/integration/test_bulk_transaction_processing.js
Normal file
@ -0,0 +1,44 @@
|
||||
describe("Bulk Transaction Processing", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
it("Creates To Sales Order", () => {
|
||||
cy.visit("/app/sales-order");
|
||||
cy.url().should("include", "/sales-order");
|
||||
cy.window()
|
||||
.its("frappe.csrf_token")
|
||||
.then((csrf_token) => {
|
||||
return cy
|
||||
.request({
|
||||
url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Frappe-CSRF-Token": csrf_token,
|
||||
},
|
||||
timeout: 60000,
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.status).eq(200);
|
||||
});
|
||||
});
|
||||
cy.wait(5000);
|
||||
cy.get(
|
||||
".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
|
||||
).check({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".actions-btn-group > .btn-primary").click({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
|
||||
.contains("Sales Invoice")
|
||||
.click({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".modal-content > .modal-footer > .standard-actions")
|
||||
.contains("Yes")
|
||||
.click({ force: true });
|
||||
cy.contains("Creation of Sales Invoice successful");
|
||||
});
|
||||
});
|
@ -7,35 +7,30 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"column_break_11",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"invoice_and_billing_tab",
|
||||
"enable_features_section",
|
||||
"unlink_payment_on_cancellation_of_invoice",
|
||||
"automatically_fetch_payment_terms",
|
||||
"delete_linked_ledger_entries",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"column_break_13",
|
||||
"delete_linked_ledger_entries",
|
||||
"invoicing_features_section",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"automatically_fetch_payment_terms",
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"post_change_gl_entries",
|
||||
"enable_discount_accounting",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"frozen_accounts_modifier",
|
||||
"column_break_4",
|
||||
"report_setting_section",
|
||||
"use_custom_cash_flow",
|
||||
"deferred_accounting_settings_section",
|
||||
"book_deferred_entries_based_on",
|
||||
"column_break_18",
|
||||
"automatically_process_deferred_accounting_entry",
|
||||
"book_deferred_entries_via_journal_entry",
|
||||
"submit_journal_entries",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"column_break_12",
|
||||
@ -43,8 +38,25 @@
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"stale_days",
|
||||
"report_settings_sb",
|
||||
"use_custom_cash_flow"
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
"column_break_11",
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"report_settings_sb"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -70,10 +82,6 @@
|
||||
"label": "Determine Address Tax Category From",
|
||||
"options": "Billing Address\nShipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_controller",
|
||||
"fieldtype": "Link",
|
||||
@ -83,6 +91,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field",
|
||||
"fieldname": "check_supplier_invoice_uniqueness",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Supplier Invoice Number Uniqueness"
|
||||
@ -168,7 +177,7 @@
|
||||
"description": "Only select this if you have set up the Cash Flow Mapper documents",
|
||||
"fieldname": "use_custom_cash_flow",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Custom Cash Flow Format"
|
||||
"label": "Enable Custom Cash Flow Format"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@ -241,7 +250,7 @@
|
||||
{
|
||||
"fieldname": "accounts_transactions_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transactions Settings"
|
||||
"label": "Credit Limit Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
@ -272,9 +281,72 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
"fieldname": "enable_common_party_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
},
|
||||
{
|
||||
"fieldname": "enable_features_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Invoice Cancellation"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_25",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "asset_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Asset Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoicing_settings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Credit Limits"
|
||||
},
|
||||
{
|
||||
"fieldname": "assets_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Assets"
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_settings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounts Closing"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_and_billing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Invoice and Billing"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoicing_features_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Invoicing Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS"
|
||||
},
|
||||
{
|
||||
"fieldname": "report_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Report Setting"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -282,7 +354,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-11 17:42:36.427699",
|
||||
"modified": "2022-02-04 12:32:36.805652",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@ -309,5 +381,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -14,6 +14,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
frm.trigger('bank_account');
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frappe.require("bank-reconciliation-tool.bundle.js", () =>
|
||||
frm.trigger("make_reconciliation_tool")
|
||||
@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
bank_account: function (frm) {
|
||||
frappe.db.get_value(
|
||||
"Bank Account",
|
||||
frm.bank_account,
|
||||
frm.doc.bank_account,
|
||||
"account",
|
||||
(r) => {
|
||||
frappe.db.get_value(
|
||||
|
@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2018-11-22 22:45:00.370913",
|
||||
"creation": "2022-01-19 01:09:13.297137",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
@ -10,6 +10,9 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"company",
|
||||
"column_break_3",
|
||||
"disabled",
|
||||
"section_break_5",
|
||||
"taxes"
|
||||
],
|
||||
"fields": [
|
||||
@ -36,10 +39,24 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-03-08 19:50:21.416513",
|
||||
"modified": "2022-01-18 21:11:23.105589",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template",
|
||||
@ -82,6 +99,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
@ -178,8 +178,8 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(
|
||||
_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
|
||||
.format(frappe.bold("Credit To")), title=_("Invalid Account")
|
||||
_("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.")
|
||||
.format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account")
|
||||
)
|
||||
|
||||
self.party_account_currency = account.account_currency
|
||||
|
@ -56,4 +56,14 @@ frappe.listview_settings["Purchase Invoice"] = {
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(listview) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -572,7 +572,10 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
|
||||
msg = _("Please ensure {} account {} is a Receivable account.").format(
|
||||
frappe.bold("Debit To"),
|
||||
frappe.bold(self.debit_to)
|
||||
) + " "
|
||||
msg += _("Change the account type to Receivable or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
@ -1249,14 +1252,14 @@ class SalesInvoice(SellingController):
|
||||
def update_billing_status_in_dn(self, update_modified=True):
|
||||
updated_delivery_notes = []
|
||||
for d in self.get("items"):
|
||||
if d.dn_detail:
|
||||
if d.so_detail:
|
||||
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
|
||||
elif d.dn_detail:
|
||||
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
|
||||
where dn_detail=%s and docstatus=1""", d.dn_detail)
|
||||
billed_amt = billed_amt and billed_amt[0][0] or 0
|
||||
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
|
||||
updated_delivery_notes.append(d.delivery_note)
|
||||
elif d.so_detail:
|
||||
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
|
||||
|
||||
for dn in set(updated_delivery_notes):
|
||||
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)
|
||||
|
@ -21,5 +21,15 @@ frappe.listview_settings['Sales Invoice'] = {
|
||||
};
|
||||
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
|
||||
},
|
||||
right_column: "grand_total"
|
||||
right_column: "grand_total",
|
||||
|
||||
onload: function(listview) {
|
||||
listview.page.add_action_item(__("Delivery Note"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -2,12 +2,13 @@
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2018-11-22 23:38:39.668804",
|
||||
"creation": "2022-01-19 01:09:28.920486",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title"
|
||||
"title",
|
||||
"disabled"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -18,14 +19,21 @@
|
||||
"label": "Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-03 11:50:38.748872",
|
||||
"modified": "2022-01-18 21:13:41.161017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Category",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -65,5 +73,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
0
erpnext/bulk_transaction/__init__.py
Normal file
0
erpnext/bulk_transaction/__init__.py
Normal file
0
erpnext/bulk_transaction/doctype/__init__.py
Normal file
0
erpnext/bulk_transaction/doctype/__init__.py
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Bulk Transaction Log', {
|
||||
|
||||
before_load: function(frm) {
|
||||
query(frm);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
|
||||
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
|
||||
query(frm);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function query(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
|
||||
args: {
|
||||
log_date: frm.doc.log_date
|
||||
}
|
||||
}).then((r) => {
|
||||
if (r.message) {
|
||||
frm.remove_custom_button("Retry Failed Transactions");
|
||||
} else {
|
||||
frappe.show_alert(__("Retrying Failed Transactions"), 5);
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-30 13:41:16.343827",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"log_date",
|
||||
"logger_data"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "log_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Log Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "logger_data",
|
||||
"fieldtype": "Table",
|
||||
"label": "Logger Data",
|
||||
"options": "Bulk Transaction Log Detail"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-03 17:23:02.935325",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Bulk Transaction",
|
||||
"name": "Bulk Transaction Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.utilities.bulk_transaction import task, update_logger
|
||||
|
||||
|
||||
class BulkTransactionLog(Document):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def retry_failing_transaction(log_date=None):
|
||||
btp = frappe.qb.DocType("Bulk Transaction Log Detail")
|
||||
data = (
|
||||
frappe.qb.from_(btp)
|
||||
.select(btp.transaction_name, btp.from_doctype, btp.to_doctype)
|
||||
.distinct()
|
||||
.where(btp.retried != 1)
|
||||
.where(btp.transaction_status == "Failed")
|
||||
.where(btp.date == log_date)
|
||||
).run(as_dict=True)
|
||||
|
||||
if data:
|
||||
if not log_date:
|
||||
log_date = str(date.today())
|
||||
if len(data) > 10:
|
||||
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
|
||||
else:
|
||||
job(data, log_date)
|
||||
else:
|
||||
return "No Failed Records"
|
||||
|
||||
def job(data, log_date):
|
||||
for d in data:
|
||||
failed = []
|
||||
try:
|
||||
frappe.db.savepoint("before_creation_of_record")
|
||||
task(d.transaction_name, d.from_doctype, d.to_doctype)
|
||||
except Exception as e:
|
||||
frappe.db.rollback(save_point="before_creation_of_record")
|
||||
failed.append(e)
|
||||
update_logger(
|
||||
d.transaction_name,
|
||||
e,
|
||||
d.from_doctype,
|
||||
d.to_doctype,
|
||||
status="Failed",
|
||||
log_date=log_date,
|
||||
restarted=1
|
||||
)
|
||||
|
||||
if not failed:
|
||||
update_logger(
|
||||
d.transaction_name,
|
||||
None,
|
||||
d.from_doctype,
|
||||
d.to_doctype,
|
||||
status="Success",
|
||||
log_date=log_date,
|
||||
restarted=1,
|
||||
)
|
@ -0,0 +1,81 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.utilities.bulk_transaction import transaction_processing
|
||||
|
||||
|
||||
class TestBulkTransactionLog(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
create_company()
|
||||
create_customer()
|
||||
create_item()
|
||||
|
||||
def test_for_single_record(self):
|
||||
so_name = create_so()
|
||||
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
|
||||
data = frappe.db.get_list("Sales Invoice", filters = {"posting_date": date.today(), "customer": "Bulk Customer"}, fields=["*"])
|
||||
if not data:
|
||||
self.fail("No Sales Invoice Created !")
|
||||
|
||||
def test_entry_in_log(self):
|
||||
so_name = create_so()
|
||||
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
|
||||
doc = frappe.get_doc("Bulk Transaction Log", str(date.today()))
|
||||
for d in doc.get("logger_data"):
|
||||
if d.transaction_name == so_name:
|
||||
self.assertEqual(d.transaction_name, so_name)
|
||||
self.assertEqual(d.transaction_status, "Success")
|
||||
self.assertEqual(d.from_doctype, "Sales Order")
|
||||
self.assertEqual(d.to_doctype, "Sales Invoice")
|
||||
self.assertEqual(d.retried, 0)
|
||||
|
||||
|
||||
|
||||
def create_company():
|
||||
if not frappe.db.exists('Company', '_Test Company'):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Company',
|
||||
'company_name': '_Test Company',
|
||||
'country': 'India',
|
||||
'default_currency': 'INR'
|
||||
}).insert()
|
||||
|
||||
def create_customer():
|
||||
if not frappe.db.exists('Customer', 'Bulk Customer'):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Customer',
|
||||
'customer_name': 'Bulk Customer'
|
||||
}).insert()
|
||||
|
||||
def create_item():
|
||||
if not frappe.db.exists("Item", "MK"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Item",
|
||||
"item_code": "MK",
|
||||
"item_name": "Milk",
|
||||
"description": "Milk",
|
||||
"item_group": "Products"
|
||||
}).insert()
|
||||
|
||||
def create_so(intent=None):
|
||||
so = frappe.new_doc("Sales Order")
|
||||
so.customer = "Bulk Customer"
|
||||
so.company = "_Test Company"
|
||||
so.transaction_date = date.today()
|
||||
|
||||
so.set_warehouse = "Finished Goods - _TC"
|
||||
so.append("items", {
|
||||
"item_code": "MK",
|
||||
"delivery_date": date.today(),
|
||||
"qty": 10,
|
||||
"rate": 80,
|
||||
})
|
||||
so.insert()
|
||||
so.submit()
|
||||
return so.name
|
@ -0,0 +1,86 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-30 13:38:30.926047",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"transaction_name",
|
||||
"date",
|
||||
"time",
|
||||
"transaction_status",
|
||||
"error_description",
|
||||
"from_doctype",
|
||||
"to_doctype",
|
||||
"retried"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "transaction_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"options": "from_doctype"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_status",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "error_description",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Error Description",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "From Doctype",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "To Doctype",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date ",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "retried",
|
||||
"fieldtype": "Int",
|
||||
"label": "Retried",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-03 19:57:31.650359",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Bulk Transaction",
|
||||
"name": "Bulk Transaction Log Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BulkTransactionLogDetail(Document):
|
||||
pass
|
@ -6,14 +6,17 @@
|
||||
"document_type": "Other",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier_and_price_defaults_section",
|
||||
"supp_master_name",
|
||||
"supplier_group",
|
||||
"column_break_4",
|
||||
"buying_price_list",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"column_break_3",
|
||||
"transaction_settings_section",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
@ -42,10 +45,6 @@
|
||||
"label": "Default Buying Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "po_required",
|
||||
"fieldtype": "Select",
|
||||
@ -73,7 +72,7 @@
|
||||
{
|
||||
"fieldname": "subcontract",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subcontract"
|
||||
"label": "Subcontracting Settings"
|
||||
},
|
||||
{
|
||||
"default": "Material Transferred for Subcontract",
|
||||
@ -116,6 +115,24 @@
|
||||
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bill for Rejected Quantity in Purchase Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_and_price_defaults_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Supplier and Price Defaults"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transaction Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@ -123,7 +140,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-08 19:26:23.548837",
|
||||
"modified": "2022-01-27 17:57:58.367048",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
@ -141,5 +158,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -29,8 +29,22 @@ frappe.listview_settings['Purchase Order'] = {
|
||||
listview.call_for_selected_items(method, { "status": "Closed" });
|
||||
});
|
||||
|
||||
listview.page.add_menu_item(__("Re-open"), function () {
|
||||
listview.page.add_menu_item(__("Reopen"), function () {
|
||||
listview.call_for_selected_items(method, { "status": "Submitted" });
|
||||
});
|
||||
|
||||
|
||||
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Advance Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
@ -142,6 +142,26 @@ def make_purchase_order(source_name, target_doc=None):
|
||||
|
||||
return doclist
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(source_name, target_doc=None):
|
||||
doc = get_mapped_doc("Supplier Quotation", source_name, {
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Purchase Invoice",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
}
|
||||
},
|
||||
"Supplier Quotation Item": {
|
||||
"doctype": "Purchase Invoice Item"
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges"
|
||||
}
|
||||
}, target_doc)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quotation(source_name, target_doc=None):
|
||||
doclist = get_mapped_doc("Supplier Quotation", source_name, {
|
||||
|
@ -8,5 +8,15 @@ frappe.listview_settings['Supplier Quotation'] = {
|
||||
} else if(doc.status==="Expired") {
|
||||
return [__("Expired"), "gray", "status,=,Expired"];
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(listview) {
|
||||
listview.page.add_action_item(__("Purchase Order"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1318,6 +1318,9 @@ class AccountsController(TransactionBase):
|
||||
payment_schedule['discount_type'] = schedule.discount_type
|
||||
payment_schedule['discount'] = schedule.discount
|
||||
|
||||
if not schedule.invoice_portion:
|
||||
payment_schedule['payment_amount'] = schedule.payment_amount
|
||||
|
||||
self.append("payment_schedule", payment_schedule)
|
||||
|
||||
def set_due_date(self):
|
||||
|
@ -710,6 +710,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
|
||||
item_group = filters.get('item_group')
|
||||
company = filters.get('company')
|
||||
taxes = item_doc.taxes or []
|
||||
|
||||
while item_group:
|
||||
@ -718,7 +719,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
item_group = item_group_doc.parent_item_group
|
||||
|
||||
if not taxes:
|
||||
return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """)
|
||||
return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True)
|
||||
else:
|
||||
valid_from = filters.get('valid_from')
|
||||
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
|
||||
@ -727,7 +728,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
'item_code': filters.get('item_code'),
|
||||
'posting_date': valid_from,
|
||||
'tax_category': filters.get('tax_category'),
|
||||
'company': filters.get('company')
|
||||
'company': company
|
||||
}
|
||||
|
||||
taxes = _get_item_tax_template(args, taxes, for_validate=True)
|
||||
|
@ -74,7 +74,8 @@ class SellingController(StockController):
|
||||
doctype=self.doctype, company=self.company,
|
||||
posting_date=self.get('posting_date'),
|
||||
fetch_payment_terms_template=fetch_payment_terms_template,
|
||||
party_address=self.customer_address, shipping_address=self.shipping_address_name)
|
||||
party_address=self.customer_address, shipping_address=self.shipping_address_name,
|
||||
company_address=self.get('company_address'))
|
||||
if not self.meta.get_field("sales_team"):
|
||||
party_details.pop("sales_team")
|
||||
self.update_if_missing(party_details)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from typing import List, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@ -181,33 +182,28 @@ class StockController(AccountsController):
|
||||
|
||||
return details
|
||||
|
||||
def get_items_and_warehouses(self):
|
||||
items, warehouses = [], []
|
||||
def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]:
|
||||
"""Get list of items and warehouses affected by a transaction"""
|
||||
|
||||
if hasattr(self, "items"):
|
||||
item_doclist = self.get("items")
|
||||
elif self.doctype == "Stock Reconciliation":
|
||||
item_doclist = []
|
||||
data = json.loads(self.reconciliation_json)
|
||||
for row in data[data.index(self.head_row)+1:]:
|
||||
d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row))
|
||||
item_doclist.append(d)
|
||||
if not (hasattr(self, "items") or hasattr(self, "packed_items")):
|
||||
return [], []
|
||||
|
||||
if item_doclist:
|
||||
for d in item_doclist:
|
||||
if d.item_code and d.item_code not in items:
|
||||
items.append(d.item_code)
|
||||
item_rows = (self.get("items") or []) + (self.get("packed_items") or [])
|
||||
|
||||
if d.get("warehouse") and d.warehouse not in warehouses:
|
||||
warehouses.append(d.warehouse)
|
||||
items = {d.item_code for d in item_rows if d.item_code}
|
||||
|
||||
if self.doctype == "Stock Entry":
|
||||
if d.get("s_warehouse") and d.s_warehouse not in warehouses:
|
||||
warehouses.append(d.s_warehouse)
|
||||
if d.get("t_warehouse") and d.t_warehouse not in warehouses:
|
||||
warehouses.append(d.t_warehouse)
|
||||
warehouses = set()
|
||||
for d in item_rows:
|
||||
if d.get("warehouse"):
|
||||
warehouses.add(d.warehouse)
|
||||
|
||||
return items, warehouses
|
||||
if self.doctype == "Stock Entry":
|
||||
if d.get("s_warehouse"):
|
||||
warehouses.add(d.s_warehouse)
|
||||
if d.get("t_warehouse"):
|
||||
warehouses.add(d.t_warehouse)
|
||||
|
||||
return list(items), list(warehouses)
|
||||
|
||||
def get_stock_ledger_details(self):
|
||||
stock_ledger = {}
|
||||
@ -219,7 +215,7 @@ class StockController(AccountsController):
|
||||
from
|
||||
`tabStock Ledger Entry`
|
||||
where
|
||||
voucher_type=%s and voucher_no=%s
|
||||
voucher_type=%s and voucher_no=%s and is_cancelled = 0
|
||||
""", (self.doctype, self.name), as_dict=True)
|
||||
|
||||
for sle in stock_ledger_entries:
|
||||
|
@ -341,7 +341,8 @@ scheduler_events = {
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
|
||||
],
|
||||
"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.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction"
|
||||
],
|
||||
"daily": [
|
||||
"erpnext.stock.reorder_item.reorder_item",
|
||||
|
@ -546,7 +546,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
i = 0
|
||||
while(i<14):
|
||||
allocate_earned_leaves()
|
||||
allocate_earned_leaves(ignore_duplicates=True)
|
||||
i += 1
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||
|
||||
@ -554,7 +554,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0)
|
||||
i = 0
|
||||
while(i<6):
|
||||
allocate_earned_leaves()
|
||||
allocate_earned_leaves(ignore_duplicates=True)
|
||||
i += 1
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||
|
||||
|
@ -8,7 +8,7 @@ from math import ceil
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate
|
||||
from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate
|
||||
|
||||
|
||||
class LeavePolicyAssignment(Document):
|
||||
@ -108,8 +108,8 @@ class LeavePolicyAssignment(Document):
|
||||
def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
|
||||
from erpnext.hr.utils import get_monthly_earned_leave
|
||||
|
||||
current_month = get_datetime().month
|
||||
current_year = get_datetime().year
|
||||
current_month = get_datetime(frappe.flags.current_date).month or get_datetime().month
|
||||
current_year = get_datetime(frappe.flags.current_date).year or get_datetime().year
|
||||
|
||||
from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date")
|
||||
if getdate(date_of_joining) > getdate(from_date):
|
||||
@ -119,10 +119,14 @@ class LeavePolicyAssignment(Document):
|
||||
from_date_year = get_datetime(from_date).year
|
||||
|
||||
months_passed = 0
|
||||
|
||||
if current_year == from_date_year and current_month > from_date_month:
|
||||
months_passed = current_month - from_date_month
|
||||
months_passed = add_current_month_if_applicable(months_passed)
|
||||
|
||||
elif current_year > from_date_year:
|
||||
months_passed = (12 - from_date_month) + current_month
|
||||
months_passed = add_current_month_if_applicable(months_passed)
|
||||
|
||||
if months_passed > 0:
|
||||
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
|
||||
@ -134,6 +138,17 @@ class LeavePolicyAssignment(Document):
|
||||
return new_leaves_allocated
|
||||
|
||||
|
||||
def add_current_month_if_applicable(months_passed):
|
||||
date = getdate(frappe.flags.current_date) or getdate()
|
||||
last_day_of_month = get_last_day(date)
|
||||
|
||||
# if its the last day of the month, then that month should also be considered
|
||||
if last_day_of_month == date:
|
||||
months_passed += 1
|
||||
|
||||
return months_passed
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_assignment_for_multiple_employees(employees, data):
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, get_first_day, getdate
|
||||
from frappe.utils import add_months, get_first_day, get_last_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_employee,
|
||||
@ -125,6 +125,121 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 0)
|
||||
|
||||
def test_earned_leave_allocation_for_passed_months(self):
|
||||
employee = get_employee()
|
||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||
leave_period = create_leave_period("Test Earned Leave Period",
|
||||
start_date=get_first_day(add_months(getdate(), -1)))
|
||||
leave_policy = frappe.get_doc({
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
|
||||
}).insert()
|
||||
|
||||
# Case 1: assignment created one month after the leave period, should allocate 1 leave
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 1)
|
||||
|
||||
def test_earned_leave_allocation_for_passed_months_on_month_end(self):
|
||||
employee = get_employee()
|
||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||
leave_period = create_leave_period("Test Earned Leave Period",
|
||||
start_date=get_first_day(add_months(getdate(), -2)))
|
||||
leave_policy = frappe.get_doc({
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
|
||||
}).insert()
|
||||
|
||||
# Case 2: assignment created on the last day of the leave period's latter month
|
||||
# should allocate 1 leave for current month even though the month has not ended
|
||||
# since the daily job might have already executed
|
||||
frappe.flags.current_date = get_last_day(getdate())
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# if the daily job is not completed yet, there is another check present
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_earned_leave_allocation_for_passed_months_with_carry_forwarded_leaves(self):
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
|
||||
employee = get_employee()
|
||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||
leave_period = create_leave_period("Test Earned Leave Period",
|
||||
start_date=get_first_day(add_months(getdate(), -2)))
|
||||
leave_policy = frappe.get_doc({
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
|
||||
}).insert()
|
||||
|
||||
# initial leave allocation = 5
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=employee.name,
|
||||
employee_name=employee.employee_name,
|
||||
leave_type=leave_type.name,
|
||||
from_date=add_months(getdate(), -12),
|
||||
to_date=add_months(getdate(), -3),
|
||||
new_leaves_allocated=5,
|
||||
carry_forward=0)
|
||||
leave_allocation.submit()
|
||||
|
||||
# Case 3: assignment created on the last day of the leave period's latter month with carry forwarding
|
||||
frappe.flags.current_date = get_last_day(add_months(getdate(), -1))
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name,
|
||||
"carry_forward": 1
|
||||
}
|
||||
# carry forwarded leaves = 5, 3 leaves allocated for passed months
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
details = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True)
|
||||
self.assertEqual(details.new_leaves_allocated, 2)
|
||||
self.assertEqual(details.unused_leaves, 5)
|
||||
self.assertEqual(details.total_leaves_allocated, 7)
|
||||
|
||||
# if the daily job is not completed yet, there is another check present
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import is_earned_leave_already_allocated
|
||||
frappe.flags.current_date = get_last_day(getdate())
|
||||
|
||||
allocation = frappe.get_doc('Leave Allocation', details.name)
|
||||
# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
|
||||
self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
@ -138,13 +253,14 @@ def create_earned_leave_type(leave_type):
|
||||
is_earned_leave=1,
|
||||
earned_leave_frequency="Monthly",
|
||||
rounding=0.5,
|
||||
max_leaves_allowed=6
|
||||
is_carry_forward=1
|
||||
)).insert()
|
||||
|
||||
|
||||
def create_leave_period(name):
|
||||
def create_leave_period(name, start_date=None):
|
||||
frappe.delete_doc_if_exists("Leave Period", name, force=1)
|
||||
start_date = get_first_day(getdate())
|
||||
if not start_date:
|
||||
start_date = get_first_day(getdate())
|
||||
|
||||
return frappe.get_doc(dict(
|
||||
name=name,
|
||||
|
@ -237,7 +237,7 @@ def generate_leave_encashment():
|
||||
|
||||
create_leave_encashment(leave_allocation=leave_allocation)
|
||||
|
||||
def allocate_earned_leaves():
|
||||
def allocate_earned_leaves(ignore_duplicates=False):
|
||||
'''Allocate earned leaves to Employees'''
|
||||
e_leave_types = get_earned_leaves()
|
||||
today = getdate()
|
||||
@ -265,9 +265,9 @@ def allocate_earned_leaves():
|
||||
from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
|
||||
|
||||
if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date):
|
||||
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
|
||||
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates)
|
||||
|
||||
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
|
||||
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False):
|
||||
earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
|
||||
|
||||
allocation = frappe.get_doc('Leave Allocation', allocation.name)
|
||||
@ -277,9 +277,12 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type
|
||||
new_allocation = e_leave_type.max_leaves_allowed
|
||||
|
||||
if new_allocation != allocation.total_leaves_allocated:
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
today_date = today()
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
|
||||
if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
|
||||
|
||||
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
|
||||
earned_leaves = 0.0
|
||||
@ -297,6 +300,28 @@ def get_monthly_earned_leave(annual_leaves, frequency, rounding):
|
||||
return earned_leaves
|
||||
|
||||
|
||||
def is_earned_leave_already_allocated(allocation, annual_allocation):
|
||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
get_leave_type_details,
|
||||
)
|
||||
|
||||
leave_type_details = get_leave_type_details()
|
||||
date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
|
||||
|
||||
assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment)
|
||||
leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type,
|
||||
annual_allocation, leave_type_details, date_of_joining)
|
||||
|
||||
# exclude carry-forwarded leaves while checking for leave allocation for passed months
|
||||
num_allocations = allocation.total_leaves_allocated
|
||||
if allocation.unused_leaves:
|
||||
num_allocations -= allocation.unused_leaves
|
||||
|
||||
if num_allocations >= leaves_for_passed_months:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_leave_allocations(date, leave_type):
|
||||
return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
|
||||
from `tabLeave Allocation`
|
||||
|
@ -46,7 +46,7 @@ frappe.ui.form.on('Loan', {
|
||||
});
|
||||
});
|
||||
|
||||
$.each(["payment_account", "loan_account"], function (i, field) {
|
||||
$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
|
||||
frm.set_query(field, function () {
|
||||
return {
|
||||
"filters": {
|
||||
@ -88,6 +88,10 @@ frappe.ui.form.on('Loan', {
|
||||
frm.add_custom_button(__('Loan Write Off'), function() {
|
||||
frm.trigger("make_loan_write_off_entry");
|
||||
},__('Create'));
|
||||
|
||||
frm.add_custom_button(__('Loan Refund'), function() {
|
||||
frm.trigger("make_loan_refund");
|
||||
},__('Create'));
|
||||
}
|
||||
}
|
||||
frm.trigger("toggle_fields");
|
||||
@ -155,6 +159,21 @@ frappe.ui.form.on('Loan', {
|
||||
})
|
||||
},
|
||||
|
||||
make_loan_refund: function(frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
"loan": frm.doc.name
|
||||
},
|
||||
method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv",
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
let doc = frappe.model.sync(r.message)[0];
|
||||
frappe.set_route("Form", doc.doctype, doc.name);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
request_loan_closure: function(frm) {
|
||||
frappe.confirm(__("Do you really want to close this loan"),
|
||||
function() {
|
||||
|
@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "ACC-LOAN-.YYYY.-.#####",
|
||||
"creation": "2019-08-29 17:29:18.176786",
|
||||
"creation": "2022-01-25 10:30:02.294967",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
@ -34,6 +34,7 @@
|
||||
"is_term_loan",
|
||||
"account_info",
|
||||
"mode_of_payment",
|
||||
"disbursement_account",
|
||||
"payment_account",
|
||||
"column_break_9",
|
||||
"loan_account",
|
||||
@ -356,12 +357,21 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Closure Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_type.disbursement_account",
|
||||
"fieldname": "disbursement_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Disbursement Account",
|
||||
"options": "Account",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-12 18:10:32.360818",
|
||||
"modified": "2022-01-25 16:29:16.325501",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
@ -391,5 +401,6 @@
|
||||
"search_fields": "posting_date",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -10,6 +10,7 @@ from frappe import _
|
||||
from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
|
||||
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import (
|
||||
@ -233,17 +234,15 @@ def request_loan_closure(loan, posting_date=None):
|
||||
loan_type = frappe.get_value('Loan', loan, 'loan_type')
|
||||
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
|
||||
|
||||
# checking greater than 0 as there may be some minor precision error
|
||||
if not pending_amount:
|
||||
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
|
||||
elif pending_amount < write_off_limit:
|
||||
if pending_amount and abs(pending_amount) < write_off_limit:
|
||||
# Auto create loan write off and update status as loan closure requested
|
||||
write_off = make_loan_write_off(loan)
|
||||
write_off.submit()
|
||||
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
|
||||
else:
|
||||
elif pending_amount > 0:
|
||||
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
|
||||
|
||||
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_loan_application(loan_application):
|
||||
loan = frappe.get_doc("Loan Application", loan_application)
|
||||
@ -400,4 +399,39 @@ def add_single_month(date):
|
||||
if getdate(date) == get_last_day(date):
|
||||
return get_last_day(add_months(date, 1))
|
||||
else:
|
||||
return add_months(date, 1)
|
||||
return add_months(date, 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0):
|
||||
loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant',
|
||||
'loan_account', 'payment_account', 'posting_date', 'company', 'name',
|
||||
'total_payment', 'total_principal_paid'], as_dict=1)
|
||||
|
||||
loan_details.doctype = 'Loan'
|
||||
loan_details[loan_details.applicant_type.lower()] = loan_details.applicant
|
||||
|
||||
if not amount:
|
||||
amount = flt(loan_details.total_principal_paid - loan_details.total_payment)
|
||||
|
||||
if amount < 0:
|
||||
frappe.throw(_('No excess amount pending for refund'))
|
||||
|
||||
refund_jv = get_payment_entry(loan_details, {
|
||||
"party_type": loan_details.applicant_type,
|
||||
"party_account": loan_details.loan_account,
|
||||
"amount_field_party": 'debit_in_account_currency',
|
||||
"amount_field_bank": 'credit_in_account_currency',
|
||||
"amount": amount,
|
||||
"bank_account": loan_details.payment_account
|
||||
})
|
||||
|
||||
if reference_number:
|
||||
refund_jv.cheque_no = reference_number
|
||||
|
||||
if reference_date:
|
||||
refund_jv.cheque_date = reference_date
|
||||
|
||||
if submit:
|
||||
refund_jv.submit()
|
||||
|
||||
return refund_jv
|
@ -42,16 +42,17 @@ class TestLoan(unittest.TestCase):
|
||||
create_loan_type("Personal Loan", 500000, 8.4,
|
||||
is_term_loan=1,
|
||||
mode_of_payment='Cash',
|
||||
disbursement_account='Disbursement Account - _TC',
|
||||
payment_account='Payment Account - _TC',
|
||||
loan_account='Loan Account - _TC',
|
||||
interest_income_account='Interest Income Account - _TC',
|
||||
penalty_income_account='Penalty Income Account - _TC')
|
||||
|
||||
create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC',
|
||||
'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
|
||||
'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
|
||||
create_loan_security_type()
|
||||
create_loan_security()
|
||||
@ -679,6 +680,29 @@ class TestLoan(unittest.TestCase):
|
||||
loan.load_from_db()
|
||||
self.assertEqual(loan.status, "Loan Closure Requested")
|
||||
|
||||
def test_loan_repayment_against_partially_disbursed_loan(self):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
"qty": 4000.00
|
||||
}]
|
||||
|
||||
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
|
||||
create_pledge(loan_application)
|
||||
|
||||
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
|
||||
loan.submit()
|
||||
|
||||
first_date = '2019-10-01'
|
||||
last_date = '2019-10-30'
|
||||
|
||||
make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date)
|
||||
|
||||
loan.load_from_db()
|
||||
|
||||
self.assertEqual(loan.status, "Partially Disbursed")
|
||||
create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
|
||||
flt(loan.loan_amount/3))
|
||||
|
||||
def test_loan_amount_write_off(self):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
@ -790,6 +814,18 @@ def create_loan_accounts():
|
||||
"account_type": "Bank",
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists("Account", "Disbursement Account - _TC"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
"company": "_Test Company",
|
||||
"account_name": "Disbursement Account",
|
||||
"root_type": "Asset",
|
||||
"report_type": "Balance Sheet",
|
||||
"currency": "INR",
|
||||
"parent_account": "Bank Accounts - _TC",
|
||||
"account_type": "Bank",
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists("Account", "Interest Income Account - _TC"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
@ -815,7 +851,7 @@ def create_loan_accounts():
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None,
|
||||
mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
|
||||
mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
|
||||
repayment_method=None, repayment_periods=None):
|
||||
|
||||
if not frappe.db.exists("Loan Type", loan_name):
|
||||
@ -829,6 +865,7 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i
|
||||
"penalty_interest_rate": penalty_interest_rate,
|
||||
"grace_period_in_days": grace_period_in_days,
|
||||
"mode_of_payment": mode_of_payment,
|
||||
"disbursement_account": disbursement_account,
|
||||
"payment_account": payment_account,
|
||||
"loan_account": loan_account,
|
||||
"interest_income_account": interest_income_account,
|
||||
|
@ -15,7 +15,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
class TestLoanApplication(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_loan_accounts()
|
||||
create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18)
|
||||
self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
|
||||
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR')
|
||||
|
@ -122,7 +122,7 @@ class LoanDisbursement(AccountsController):
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.loan_account,
|
||||
"against": loan_details.payment_account,
|
||||
"against": loan_details.disbursement_account,
|
||||
"debit": self.disbursed_amount,
|
||||
"debit_in_account_currency": self.disbursed_amount,
|
||||
"against_voucher_type": "Loan",
|
||||
@ -137,7 +137,7 @@ class LoanDisbursement(AccountsController):
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.payment_account,
|
||||
"account": loan_details.disbursement_account,
|
||||
"against": loan_details.loan_account,
|
||||
"credit": self.disbursed_amount,
|
||||
"credit_in_account_currency": self.disbursed_amount,
|
||||
|
@ -44,8 +44,8 @@ class TestLoanDisbursement(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_loan_accounts()
|
||||
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
|
||||
'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
|
||||
create_loan_security_type()
|
||||
create_loan_security()
|
||||
|
@ -30,8 +30,8 @@ class TestLoanInterestAccrual(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_loan_accounts()
|
||||
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
|
||||
'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
|
||||
create_loan_security_type()
|
||||
create_loan_security()
|
||||
|
@ -125,7 +125,7 @@ class LoanRepayment(AccountsController):
|
||||
|
||||
def update_paid_amount(self):
|
||||
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
|
||||
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
|
||||
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
|
||||
'written_off_amount'], as_dict=1)
|
||||
|
||||
loan.update({
|
||||
@ -153,7 +153,7 @@ class LoanRepayment(AccountsController):
|
||||
|
||||
def mark_as_unpaid(self):
|
||||
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
|
||||
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
|
||||
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
|
||||
'written_off_amount'], as_dict=1)
|
||||
|
||||
no_of_repayments = len(self.repayment_details)
|
||||
|
@ -15,7 +15,7 @@ frappe.ui.form.on('Loan Type', {
|
||||
});
|
||||
});
|
||||
|
||||
$.each(["payment_account", "loan_account"], function (i, field) {
|
||||
$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
|
||||
frm.set_query(field, function () {
|
||||
return {
|
||||
"filters": {
|
||||
|
@ -19,9 +19,10 @@
|
||||
"description",
|
||||
"account_details_section",
|
||||
"mode_of_payment",
|
||||
"disbursement_account",
|
||||
"payment_account",
|
||||
"loan_account",
|
||||
"column_break_12",
|
||||
"loan_account",
|
||||
"interest_income_account",
|
||||
"penalty_income_account",
|
||||
"amended_from"
|
||||
@ -79,7 +80,7 @@
|
||||
{
|
||||
"fieldname": "payment_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Account",
|
||||
"label": "Repayment Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
@ -149,15 +150,23 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Auto Write Off Amount ",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "disbursement_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Disbursement Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 18:10:57.368490",
|
||||
"modified": "2022-01-25 16:23:57.009349",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Type",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -181,5 +190,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -703,7 +703,8 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse,
|
||||
company=company)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture')
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture'))
|
||||
self.assertRaises(frappe.ValidationError, stock_entry.save)
|
||||
|
||||
def test_wo_completion_with_pl_bom(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import (
|
||||
|
@ -89,10 +89,10 @@ def get_bom_stock(filters):
|
||||
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
|
||||
|
||||
def get_manufacturer_records():
|
||||
details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
|
||||
details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"])
|
||||
manufacture_details = frappe._dict()
|
||||
for detail in details:
|
||||
dic = manufacture_details.setdefault(detail.get('parent'), {})
|
||||
dic = manufacture_details.setdefault(detail.get('item_code'), {})
|
||||
dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
|
||||
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
|
||||
|
||||
|
@ -21,4 +21,5 @@ Communication
|
||||
Loan Management
|
||||
Payroll
|
||||
Telephony
|
||||
Bulk Transaction
|
||||
E-commerce
|
||||
|
@ -349,3 +349,4 @@ erpnext.patches.v12_0.add_company_link_to_einvoice_settings
|
||||
erpnext.patches.v14_0.migrate_cost_center_allocations
|
||||
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
|
||||
erpnext.patches.v13_0.shopping_cart_to_ecommerce
|
||||
erpnext.patches.v13_0.update_disbursement_account
|
||||
|
22
erpnext/patches/v13_0/update_disbursement_account.py
Normal file
22
erpnext/patches/v13_0/update_disbursement_account.py
Normal file
@ -0,0 +1,22 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
frappe.reload_doc("loan_management", "doctype", "loan_type")
|
||||
frappe.reload_doc("loan_management", "doctype", "loan")
|
||||
|
||||
loan_type = frappe.qb.DocType("Loan Type")
|
||||
loan = frappe.qb.DocType("Loan")
|
||||
|
||||
frappe.qb.update(
|
||||
loan_type
|
||||
).set(
|
||||
loan_type.disbursement_account, loan_type.payment_account
|
||||
).run()
|
||||
|
||||
frappe.qb.update(
|
||||
loan
|
||||
).set(
|
||||
loan.disbursement_account, loan.payment_account
|
||||
).run()
|
@ -214,6 +214,7 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
create_loan_type("Car Loan", 500000, 8.4,
|
||||
is_term_loan=1,
|
||||
mode_of_payment='Cash',
|
||||
disbursement_account='Disbursement Account - _TC',
|
||||
payment_account='Payment Account - _TC',
|
||||
loan_account='Loan Account - _TC',
|
||||
interest_income_account='Interest Income Account - _TC',
|
||||
|
@ -370,6 +370,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
create_loan_type("Car Loan", 500000, 8.4,
|
||||
is_term_loan=1,
|
||||
mode_of_payment='Cash',
|
||||
disbursement_account='Disbursement Account - _TC',
|
||||
payment_account='Payment Account - _TC',
|
||||
loan_account='Loan Account - _TC',
|
||||
interest_income_account='Interest Income Account - _TC',
|
||||
|
@ -39,7 +39,8 @@
|
||||
"public/js/utils/dimension_tree_filter.js",
|
||||
"public/js/telephony.js",
|
||||
"public/js/templates/call_link.html",
|
||||
"public/js/templates/node_card.html"
|
||||
"public/js/templates/node_card.html",
|
||||
"public/js/bulk_transaction_processing.js"
|
||||
],
|
||||
"js/item-dashboard.min.js": [
|
||||
"stock/dashboard/item_dashboard.html",
|
||||
|
30
erpnext/public/js/bulk_transaction_processing.js
Normal file
30
erpnext/public/js/bulk_transaction_processing.js
Normal file
@ -0,0 +1,30 @@
|
||||
frappe.provide("erpnext.bulk_transaction_processing");
|
||||
|
||||
$.extend(erpnext.bulk_transaction_processing, {
|
||||
create: function(listview, from_doctype, to_doctype) {
|
||||
let checked_items = listview.get_checked_items();
|
||||
const doc_name = [];
|
||||
checked_items.forEach((Item)=> {
|
||||
if (Item.docstatus == 0) {
|
||||
doc_name.push(Item.name);
|
||||
}
|
||||
});
|
||||
|
||||
let count_of_rows = checked_items.length;
|
||||
frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), ()=>{
|
||||
if (doc_name.length == 0) {
|
||||
frappe.call({
|
||||
method: "erpnext.utilities.bulk_transaction.transaction_processing",
|
||||
args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype}
|
||||
}).then(()=> {
|
||||
|
||||
});
|
||||
if (count_of_rows > 10) {
|
||||
frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, to_doctype]);
|
||||
}
|
||||
} else {
|
||||
frappe.msgprint(__("Selected document must be in submitted state"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -2288,7 +2288,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
() => this.frm.doc.ignore_pricing_rule=1,
|
||||
() => me.ignore_pricing_rule(),
|
||||
() => this.frm.doc.ignore_pricing_rule=0,
|
||||
() => me.apply_pricing_rule()
|
||||
() => me.apply_pricing_rule(),
|
||||
() => this.frm.save()
|
||||
]);
|
||||
} else {
|
||||
frappe.run_serially([
|
||||
|
@ -22,5 +22,6 @@ import "./call_popup/call_popup";
|
||||
import "./utils/dimension_tree_filter";
|
||||
import "./telephony";
|
||||
import "./templates/call_link.html";
|
||||
import "./bulk_transaction_processing";
|
||||
|
||||
// import { sum } from 'frappe/public/utils/util.js'
|
||||
|
@ -12,6 +12,14 @@ frappe.listview_settings['Quotation'] = {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Sales Order"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Sales Invoice"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice");
|
||||
});
|
||||
},
|
||||
|
||||
get_indicator: function(doc) {
|
||||
|
@ -16,7 +16,7 @@ frappe.listview_settings['Sales Order'] = {
|
||||
return [__("Overdue"), "red",
|
||||
"per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
|
||||
} else if (flt(doc.grand_total) === 0) {
|
||||
// not delivered (zero-amount order)
|
||||
// not delivered (zeroount order)
|
||||
return [__("To Deliver"), "orange",
|
||||
"per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
|
||||
} else if (flt(doc.per_billed, 6) < 100) {
|
||||
@ -48,5 +48,17 @@ frappe.listview_settings['Sales Order'] = {
|
||||
listview.call_for_selected_items(method, {"status": "Submitted"});
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Sales Invoice"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Delivery Note"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Advance Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Advance Payment");
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
@ -80,7 +80,7 @@
|
||||
"description": "How often should Project and Company be updated based on Sales Transactions?",
|
||||
"fieldname": "sales_update_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Sales Update Frequency",
|
||||
"label": "Sales Update Frequency in Company and Project",
|
||||
"options": "Each Transaction\nDaily\nMonthly",
|
||||
"reqd": 1
|
||||
},
|
||||
@ -171,7 +171,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-13 12:32:17.004404",
|
||||
"modified": "2022-02-04 15:41:59.939261",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
@ -189,5 +189,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -339,17 +339,35 @@ class DeliveryNote(SellingController):
|
||||
frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again"))
|
||||
|
||||
def update_billed_amount_based_on_so(so_detail, update_modified=True):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
# Billed against Sales Order directly
|
||||
billed_against_so = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
|
||||
where so_detail=%s and (dn_detail is null or dn_detail = '') and docstatus=1""", so_detail)
|
||||
si = frappe.qb.DocType("Sales Invoice").as_("si")
|
||||
si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
|
||||
sum_amount = Sum(si_item.amount).as_("amount")
|
||||
|
||||
billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where(
|
||||
(si_item.parent == si.name) &
|
||||
(si_item.so_detail == so_detail) &
|
||||
((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) &
|
||||
(si_item.docstatus == 1) &
|
||||
(si.update_stock == 0)
|
||||
).run()
|
||||
billed_against_so = billed_against_so and billed_against_so[0][0] or 0
|
||||
|
||||
# Get all Delivery Note Item rows against the Sales Order Item row
|
||||
dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent
|
||||
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
|
||||
where dn.name=dn_item.parent and dn_item.so_detail=%s
|
||||
and dn.docstatus=1 and dn.is_return = 0
|
||||
order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1)
|
||||
|
||||
dn = frappe.qb.DocType("Delivery Note").as_("dn")
|
||||
dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item")
|
||||
|
||||
dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where(
|
||||
(dn.name == dn_item.parent) &
|
||||
(dn_item.so_detail == so_detail) &
|
||||
(dn.docstatus == 1) &
|
||||
(dn.is_return == 0)
|
||||
).orderby(
|
||||
dn.posting_date, dn.posting_time, dn.name
|
||||
).run(as_dict=True)
|
||||
|
||||
updated_dn = []
|
||||
for dnd in dn_details:
|
||||
@ -367,7 +385,11 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True):
|
||||
|
||||
# Distribute billed amount directly against SO between DNs based on FIFO
|
||||
if billed_against_so and billed_amt_agianst_dn < dnd.amount:
|
||||
pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn
|
||||
if dnd.returned_qty:
|
||||
pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty
|
||||
else:
|
||||
pending_to_bill = flt(dnd.amount)
|
||||
pending_to_bill -= billed_amt_agianst_dn
|
||||
if pending_to_bill <= billed_against_so:
|
||||
billed_amt_agianst_dn += pending_to_bill
|
||||
billed_against_so -= pending_to_bill
|
||||
@ -586,7 +608,18 @@ def make_packing_slip(source_name, target_doc=None):
|
||||
"validation": {
|
||||
"docstatus": ["=", 0]
|
||||
}
|
||||
},
|
||||
|
||||
"Delivery Note Item": {
|
||||
"doctype": "Packing Slip Item",
|
||||
"field_map": {
|
||||
"item_code": "item_code",
|
||||
"item_name": "item_name",
|
||||
"description": "description",
|
||||
"qty": "qty",
|
||||
}
|
||||
}
|
||||
|
||||
}, target_doc)
|
||||
|
||||
return doclist
|
||||
|
@ -14,7 +14,7 @@ frappe.listview_settings['Delivery Note'] = {
|
||||
return [__("Completed"), "green", "per_billed,=,100"];
|
||||
}
|
||||
},
|
||||
onload: function (doclist) {
|
||||
onload: function (listview) {
|
||||
const action = () => {
|
||||
const selected_docs = doclist.get_checked_items();
|
||||
const docnames = doclist.get_checked_items(true);
|
||||
@ -54,6 +54,16 @@ frappe.listview_settings['Delivery Note'] = {
|
||||
};
|
||||
};
|
||||
|
||||
doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
|
||||
// doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
|
||||
|
||||
listview.page.add_action_item(__('Create Delivery Trip'), action);
|
||||
|
||||
listview.page.add_action_item(__("Sales Invoice"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,14 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from frappe.utils import add_to_date, nowdate
|
||||
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestCase, change_settings
|
||||
|
||||
|
||||
@ -12,31 +16,30 @@ class TestPackedItem(ERPNextTestCase):
|
||||
"Test impact on Packed Items table in various scenarios."
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
make_item("_Test Product Bundle X", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
super().setUpClass()
|
||||
cls.bundle = "_Test Product Bundle X"
|
||||
cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
|
||||
make_item(cls.bundle, {"is_stock_item": 0})
|
||||
for item in cls.bundle_items:
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
|
||||
make_item("_Test Normal Stock Item", {"is_stock_item": 1})
|
||||
|
||||
make_product_bundle(
|
||||
"_Test Product Bundle X",
|
||||
["_Test Bundle Item 1", "_Test Bundle Item 2"],
|
||||
qty=2
|
||||
)
|
||||
make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
|
||||
|
||||
def test_adding_bundle_item(self):
|
||||
"Test impact on packed items if bundle item row is added."
|
||||
so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
|
||||
so = make_sales_order(item_code = self.bundle, qty=1,
|
||||
do_not_submit=True)
|
||||
|
||||
self.assertEqual(so.items[0].qty, 1)
|
||||
self.assertEqual(len(so.packed_items), 2)
|
||||
self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1")
|
||||
self.assertEqual(so.packed_items[0].item_code, self.bundle_items[0])
|
||||
self.assertEqual(so.packed_items[0].qty, 2)
|
||||
|
||||
def test_updating_bundle_item(self):
|
||||
"Test impact on packed items if bundle item row is updated."
|
||||
so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
|
||||
do_not_submit=True)
|
||||
so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True)
|
||||
|
||||
so.items[0].qty = 2 # change qty
|
||||
so.save()
|
||||
@ -55,7 +58,7 @@ class TestPackedItem(ERPNextTestCase):
|
||||
so_items = []
|
||||
for qty in [2, 4, 6, 8]:
|
||||
so_items.append({
|
||||
"item_code": "_Test Product Bundle X",
|
||||
"item_code": self.bundle,
|
||||
"qty": qty,
|
||||
"rate": 400,
|
||||
"warehouse": "_Test Warehouse - _TC"
|
||||
@ -66,7 +69,7 @@ class TestPackedItem(ERPNextTestCase):
|
||||
|
||||
# check alternate rows for qty
|
||||
self.assertEqual(len(so.packed_items), 8)
|
||||
self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2")
|
||||
self.assertEqual(so.packed_items[1].item_code, self.bundle_items[1])
|
||||
self.assertEqual(so.packed_items[1].qty, 4)
|
||||
self.assertEqual(so.packed_items[3].qty, 8)
|
||||
self.assertEqual(so.packed_items[5].qty, 12)
|
||||
@ -94,8 +97,7 @@ class TestPackedItem(ERPNextTestCase):
|
||||
@change_settings("Selling Settings", {"editable_bundle_item_rates": 1})
|
||||
def test_bundle_item_cumulative_price(self):
|
||||
"Test if Bundle Item rate is cumulative from packed items."
|
||||
so = make_sales_order(item_code = "_Test Product Bundle X", qty=2,
|
||||
do_not_submit=True)
|
||||
so = make_sales_order(item_code=self.bundle, qty=2, do_not_submit=True)
|
||||
|
||||
so.packed_items[0].rate = 150
|
||||
so.packed_items[1].rate = 200
|
||||
@ -109,7 +111,7 @@ class TestPackedItem(ERPNextTestCase):
|
||||
so_items = []
|
||||
for qty in [2, 4]:
|
||||
so_items.append({
|
||||
"item_code": "_Test Product Bundle X",
|
||||
"item_code": self.bundle,
|
||||
"qty": qty,
|
||||
"rate": 400,
|
||||
"warehouse": "_Test Warehouse - _TC"
|
||||
@ -124,4 +126,33 @@ class TestPackedItem(ERPNextTestCase):
|
||||
|
||||
self.assertEqual(len(dn.packed_items), 4)
|
||||
self.assertEqual(dn.packed_items[2].qty, 6)
|
||||
self.assertEqual(dn.packed_items[3].qty, 6)
|
||||
self.assertEqual(dn.packed_items[3].qty, 6)
|
||||
|
||||
def test_reposting_packed_items(self):
|
||||
warehouse = "Stores - TCP1"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
today = nowdate()
|
||||
yesterday = add_to_date(today, days=-1, as_string=True)
|
||||
|
||||
for item in self.bundle_items:
|
||||
make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today)
|
||||
|
||||
so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse)
|
||||
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.save()
|
||||
dn.submit()
|
||||
|
||||
gles = get_gl_entries(dn.doctype, dn.name)
|
||||
credit_before_repost = sum(gle.credit for gle in gles)
|
||||
|
||||
# backdated stock entry
|
||||
for item in self.bundle_items:
|
||||
make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday)
|
||||
|
||||
# assert correct reposting
|
||||
gles = get_gl_entries(dn.doctype, dn.name)
|
||||
credit_after_reposting = sum(gle.credit for gle in gles)
|
||||
self.assertNotEqual(credit_before_repost, credit_after_reposting)
|
||||
self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost)
|
||||
|
@ -288,9 +288,6 @@ class PurchaseReceipt(BuyingController):
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": self.name,
|
||||
"voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference")
|
||||
|
||||
if not stock_value_diff:
|
||||
continue
|
||||
|
||||
warehouse_account_name = warehouse_account[d.warehouse]["account"]
|
||||
warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"]
|
||||
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
|
||||
|
@ -13,5 +13,13 @@ frappe.listview_settings['Purchase Receipt'] = {
|
||||
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
|
||||
return [__("Completed"), "green", "per_billed,=,100"];
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(listview) {
|
||||
|
||||
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Receipt", "Purchase Invoice");
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, cint, cstr, flt, today
|
||||
@ -16,7 +17,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas
|
||||
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
from erpnext.tests.utils import ERPNextTestCase, change_settings
|
||||
|
||||
|
||||
class TestPurchaseReceipt(ERPNextTestCase):
|
||||
@ -1387,6 +1388,36 @@ class TestPurchaseReceipt(ERPNextTestCase):
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
@change_settings("Stock Settings", {"allow_negative_stock": 1})
|
||||
def test_neg_to_positive(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
item_code = "_TestNegToPosItem"
|
||||
warehouse = "Stores - TCP1"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
account = "Stock Received But Not Billed - TCP1"
|
||||
|
||||
make_item(item_code)
|
||||
se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0)
|
||||
se.items[0].allow_zero_valuation_rate = 1
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
qty=50,
|
||||
rate=1,
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
get_taxes_and_charges=True,
|
||||
company=company,
|
||||
)
|
||||
gles = get_gl_entries(pr.doctype, pr.name)
|
||||
|
||||
for gle in gles:
|
||||
if gle.account == account:
|
||||
self.assertEqual(gle.credit, 50)
|
||||
|
||||
|
||||
def get_sl_entries(voucher_type, voucher_no):
|
||||
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
|
||||
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
|
||||
|
@ -13,7 +13,7 @@ from erpnext.accounts.utils import (
|
||||
check_if_stock_and_account_balance_synced,
|
||||
update_gl_entries_after,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import repost_future_sle
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle
|
||||
|
||||
|
||||
class RepostItemValuation(Document):
|
||||
@ -138,13 +138,20 @@ def repost_gl_entries(doc):
|
||||
|
||||
if doc.based_on == 'Transaction':
|
||||
ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
|
||||
items, warehouses = ref_doc.get_items_and_warehouses()
|
||||
doc_items, doc_warehouses = ref_doc.get_items_and_warehouses()
|
||||
|
||||
sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no)
|
||||
sle_items = [sle.item_code for sle in sles]
|
||||
sle_warehouse = [sle.warehouse for sle in sles]
|
||||
|
||||
items = list(set(doc_items).union(set(sle_items)))
|
||||
warehouses = list(set(doc_warehouses).union(set(sle_warehouse)))
|
||||
else:
|
||||
items = [doc.item_code]
|
||||
warehouses = [doc.warehouse]
|
||||
|
||||
update_gl_entries_after(doc.posting_date, doc.posting_time,
|
||||
warehouses, items, company=doc.company)
|
||||
for_warehouses=warehouses, for_items=items, company=doc.company)
|
||||
|
||||
def notify_error_to_stock_managers(doc, traceback):
|
||||
recipients = get_users_with_role("Stock Manager")
|
||||
|
@ -8,7 +8,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"items_section",
|
||||
"title",
|
||||
"naming_series",
|
||||
"stock_entry_type",
|
||||
"outgoing_stock_entry",
|
||||
@ -83,14 +82,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
@ -353,9 +344,9 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "scan_barcode",
|
||||
"options": "Barcode",
|
||||
"fieldtype": "Data",
|
||||
"label": "Scan Barcode"
|
||||
"label": "Scan Barcode",
|
||||
"options": "Barcode"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
@ -628,10 +619,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-20 19:19:31.514846",
|
||||
"modified": "2022-02-07 12:55:14.614077",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -698,6 +690,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "title",
|
||||
"states": [],
|
||||
"title_field": "stock_entry_type",
|
||||
"track_changes": 1
|
||||
}
|
@ -76,7 +76,6 @@ class StockEntry(StockController):
|
||||
|
||||
self.validate_posting_time()
|
||||
self.validate_purpose()
|
||||
self.set_title()
|
||||
self.validate_item()
|
||||
self.validate_customer_provided_item()
|
||||
self.validate_qty()
|
||||
@ -1116,7 +1115,7 @@ class StockEntry(StockController):
|
||||
self.set_actual_qty()
|
||||
self.update_items_for_process_loss()
|
||||
self.validate_customer_provided_item()
|
||||
self.calculate_rate_and_amount()
|
||||
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
|
||||
|
||||
def set_scrap_items(self):
|
||||
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
|
||||
@ -1835,14 +1834,6 @@ class StockEntry(StockController):
|
||||
|
||||
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
|
||||
|
||||
def set_title(self):
|
||||
if frappe.flags.in_import and self.title:
|
||||
# Allow updating title during data import/update
|
||||
return
|
||||
|
||||
self.title = self.purpose
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def move_sample_to_retention_warehouse(company, items):
|
||||
if isinstance(items, str):
|
||||
|
@ -18,7 +18,6 @@
|
||||
"items",
|
||||
"section_break_9",
|
||||
"expense_account",
|
||||
"reconciliation_json",
|
||||
"column_break_13",
|
||||
"difference_amount",
|
||||
"amended_from",
|
||||
@ -111,15 +110,6 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "reconciliation_json",
|
||||
"fieldtype": "Long Text",
|
||||
"hidden": 1,
|
||||
"label": "Reconciliation JSON",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
@ -155,7 +145,7 @@
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-30 01:33:51.437194",
|
||||
"modified": "2022-02-06 14:28:19.043905",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reconciliation",
|
||||
@ -178,5 +168,6 @@
|
||||
"search_fields": "posting_date",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -25,8 +25,8 @@ from erpnext.tests.utils import ERPNextTestCase, change_settings
|
||||
class TestStockReconciliation(ERPNextTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
create_batch_or_serial_no_items()
|
||||
super().setUpClass()
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -5,35 +5,41 @@
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"defaults_tab",
|
||||
"item_defaults_section",
|
||||
"item_naming_by",
|
||||
"item_group",
|
||||
"stock_uom",
|
||||
"default_warehouse",
|
||||
"column_break_4",
|
||||
"valuation_method",
|
||||
"default_warehouse",
|
||||
"sample_retention_warehouse",
|
||||
"use_naming_series",
|
||||
"naming_series_prefix",
|
||||
"valuation_method",
|
||||
"price_list_defaults_section",
|
||||
"auto_insert_price_list_rate_if_missing",
|
||||
"column_break_12",
|
||||
"update_existing_price_list_rate",
|
||||
"stock_validations_tab",
|
||||
"section_break_9",
|
||||
"over_delivery_receipt_allowance",
|
||||
"role_allowed_to_over_deliver_receive",
|
||||
"mr_qty_allowance",
|
||||
"column_break_12",
|
||||
"auto_insert_price_list_rate_if_missing",
|
||||
"update_existing_price_list_rate",
|
||||
"column_break_121",
|
||||
"role_allowed_to_over_deliver_receive",
|
||||
"allow_negative_stock",
|
||||
"show_barcode_field",
|
||||
"clean_description_html",
|
||||
"quality_inspection_settings_section",
|
||||
"action_if_quality_inspection_is_not_submitted",
|
||||
"column_break_21",
|
||||
"column_break_23",
|
||||
"action_if_quality_inspection_is_rejected",
|
||||
"serial_and_batch_item_settings_tab",
|
||||
"section_break_7",
|
||||
"automatically_set_serial_nos_based_on_fifo",
|
||||
"set_qty_in_transactions_based_on_serial_no_input",
|
||||
"column_break_10",
|
||||
"disable_serial_no_and_batch_selector",
|
||||
"use_naming_series",
|
||||
"naming_series_prefix",
|
||||
"stock_planning_tab",
|
||||
"auto_material_request",
|
||||
"auto_indent",
|
||||
"column_break_27",
|
||||
@ -42,6 +48,7 @@
|
||||
"allow_from_dn",
|
||||
"column_break_31",
|
||||
"allow_from_pr",
|
||||
"stock_closing_tab",
|
||||
"control_historical_stock_transactions_section",
|
||||
"stock_frozen_upto",
|
||||
"stock_frozen_upto_days",
|
||||
@ -122,7 +129,7 @@
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Serialised and Batch Setting"
|
||||
"label": "Serial & Batch Item Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@ -275,10 +282,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Quality Inspection Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Stop",
|
||||
"fieldname": "action_if_quality_inspection_is_rejected",
|
||||
@ -298,6 +301,44 @@
|
||||
"fieldname": "update_existing_price_list_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Existing Price List Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "defaults_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Defaults"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_validations_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Stock Validations"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_planning_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Stock Planning"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_closing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Stock Closing"
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_and_batch_item_settings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Serial & Batch Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list_defaults_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Price List Defaults"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_121",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -305,7 +346,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-15 17:42:53.174865",
|
||||
"modified": "2022-02-05 15:33:43.692736",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
21
erpnext/tests/ui_test_bulk_transaction_processing.py
Normal file
21
erpnext/tests/ui_test_bulk_transaction_processing.py
Normal file
@ -0,0 +1,21 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.bulk_transaction.doctype.bulk_transaction_logger.test_bulk_transaction_logger import (
|
||||
create_company,
|
||||
create_customer,
|
||||
create_item,
|
||||
create_so,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_records():
|
||||
create_company()
|
||||
create_customer()
|
||||
create_item()
|
||||
|
||||
gd = frappe.get_doc("Global Defaults")
|
||||
gd.set("default_company", "Test Bulk")
|
||||
gd.save()
|
||||
frappe.clear_cache()
|
||||
create_so()
|
201
erpnext/utilities/bulk_transaction.py
Normal file
201
erpnext/utilities/bulk_transaction.py
Normal file
@ -0,0 +1,201 @@
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def transaction_processing(data, from_doctype, to_doctype):
|
||||
if isinstance(data, str):
|
||||
deserialized_data = json.loads(data)
|
||||
|
||||
else:
|
||||
deserialized_data = data
|
||||
|
||||
length_of_data = len(deserialized_data)
|
||||
|
||||
if length_of_data > 10:
|
||||
frappe.msgprint(
|
||||
_("Started a background job to create {1} {0}").format(to_doctype, length_of_data)
|
||||
)
|
||||
frappe.enqueue(
|
||||
job,
|
||||
deserialized_data=deserialized_data,
|
||||
from_doctype=from_doctype,
|
||||
to_doctype=to_doctype,
|
||||
)
|
||||
else:
|
||||
job(deserialized_data, from_doctype, to_doctype)
|
||||
|
||||
|
||||
def job(deserialized_data, from_doctype, to_doctype):
|
||||
failed_history = []
|
||||
i = 0
|
||||
for d in deserialized_data:
|
||||
failed = []
|
||||
|
||||
try:
|
||||
i += 1
|
||||
doc_name = d.get("name")
|
||||
frappe.db.savepoint("before_creation_state")
|
||||
task(doc_name, from_doctype, to_doctype)
|
||||
|
||||
except Exception as e:
|
||||
frappe.db.rollback(save_point="before_creation_state")
|
||||
failed_history.append(e)
|
||||
failed.append(e)
|
||||
update_logger(doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today()))
|
||||
if not failed:
|
||||
update_logger(doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today()))
|
||||
|
||||
show_job_status(failed_history, deserialized_data, to_doctype)
|
||||
|
||||
|
||||
def task(doc_name, from_doctype, to_doctype):
|
||||
from erpnext.accounts.doctype.payment_entry import payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice import purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice import sales_invoice
|
||||
from erpnext.buying.doctype.purchase_order import purchase_order
|
||||
from erpnext.buying.doctype.supplier_quotation import supplier_quotation
|
||||
from erpnext.selling.doctype.quotation import quotation
|
||||
from erpnext.selling.doctype.sales_order import sales_order
|
||||
from erpnext.stock.doctype.delivery_note import delivery_note
|
||||
from erpnext.stock.doctype.purchase_receipt import purchase_receipt
|
||||
|
||||
mapper = {
|
||||
"Sales Order": {
|
||||
"Sales Invoice": sales_order.make_sales_invoice,
|
||||
"Delivery Note": sales_order.make_delivery_note,
|
||||
"Advance Payment": payment_entry.get_payment_entry,
|
||||
},
|
||||
"Sales Invoice": {
|
||||
"Delivery Note": sales_invoice.make_delivery_note,
|
||||
"Payment": payment_entry.get_payment_entry,
|
||||
},
|
||||
"Delivery Note": {
|
||||
"Sales Invoice": delivery_note.make_sales_invoice,
|
||||
"Packing Slip": delivery_note.make_packing_slip,
|
||||
},
|
||||
"Quotation": {
|
||||
"Sales Order": quotation.make_sales_order,
|
||||
"Sales Invoice": quotation.make_sales_invoice,
|
||||
},
|
||||
"Supplier Quotation": {
|
||||
"Purchase Order": supplier_quotation.make_purchase_order,
|
||||
"Purchase Invoice": supplier_quotation.make_purchase_invoice,
|
||||
"Advance Payment": payment_entry.get_payment_entry,
|
||||
},
|
||||
"Purchase Order": {
|
||||
"Purchase Invoice": purchase_order.make_purchase_invoice,
|
||||
"Purchase Receipt": purchase_order.make_purchase_receipt,
|
||||
},
|
||||
"Purhcase Invoice": {
|
||||
"Purchase Receipt": purchase_invoice.make_purchase_receipt,
|
||||
"Payment": payment_entry.get_payment_entry,
|
||||
},
|
||||
"Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice},
|
||||
}
|
||||
if to_doctype in ['Advance Payment', 'Payment']:
|
||||
obj = mapper[from_doctype][to_doctype](from_doctype, doc_name)
|
||||
else:
|
||||
obj = mapper[from_doctype][to_doctype](doc_name)
|
||||
|
||||
obj.flags.ignore_validate = True
|
||||
obj.insert(ignore_mandatory=True)
|
||||
|
||||
|
||||
def check_logger_doc_exists(log_date):
|
||||
return frappe.db.exists("Bulk Transaction Log", log_date)
|
||||
|
||||
|
||||
def get_logger_doc(log_date):
|
||||
return frappe.get_doc("Bulk Transaction Log", log_date)
|
||||
|
||||
|
||||
def create_logger_doc():
|
||||
log_doc = frappe.new_doc("Bulk Transaction Log")
|
||||
log_doc.set_new_name(set_name=str(date.today()))
|
||||
log_doc.log_date = date.today()
|
||||
|
||||
return log_doc
|
||||
|
||||
|
||||
def append_data_to_logger(log_doc, doc_name, error, from_doctype, to_doctype, status, restarted):
|
||||
row = log_doc.append("logger_data", {})
|
||||
row.transaction_name = doc_name
|
||||
row.date = date.today()
|
||||
now = datetime.now()
|
||||
row.time = now.strftime("%H:%M:%S")
|
||||
row.transaction_status = status
|
||||
row.error_description = str(error)
|
||||
row.from_doctype = from_doctype
|
||||
row.to_doctype = to_doctype
|
||||
row.retried = restarted
|
||||
|
||||
|
||||
def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0):
|
||||
if not check_logger_doc_exists(log_date):
|
||||
log_doc = create_logger_doc()
|
||||
append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted)
|
||||
log_doc.insert()
|
||||
else:
|
||||
log_doc = get_logger_doc(log_date)
|
||||
if record_exists(log_doc, doc_name, status):
|
||||
append_data_to_logger(
|
||||
log_doc, doc_name, e, from_doctype, to_doctype, status, restarted
|
||||
)
|
||||
log_doc.save()
|
||||
|
||||
|
||||
def show_job_status(failed_history, deserialized_data, to_doctype):
|
||||
if not failed_history:
|
||||
frappe.msgprint(
|
||||
_("Creation of {0} successful").format(to_doctype),
|
||||
title="Successful",
|
||||
indicator="green",
|
||||
)
|
||||
|
||||
if len(failed_history) != 0 and len(failed_history) < len(deserialized_data):
|
||||
frappe.msgprint(
|
||||
_("""Creation of {0} partially successful.
|
||||
Check <b><a href="/app/bulk-transaction-log">Bulk Transaction Log</a></b>""").format(
|
||||
to_doctype
|
||||
),
|
||||
title="Partially successful",
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
if len(failed_history) == len(deserialized_data):
|
||||
frappe.msgprint(
|
||||
_("""Creation of {0} failed.
|
||||
Check <b><a href="/app/bulk-transaction-log">Bulk Transaction Log</a></b>""").format(
|
||||
to_doctype
|
||||
),
|
||||
title="Failed",
|
||||
indicator="red",
|
||||
)
|
||||
|
||||
|
||||
def record_exists(log_doc, doc_name, status):
|
||||
|
||||
record = mark_retrired_transaction(log_doc, doc_name)
|
||||
|
||||
if record and status == "Failed":
|
||||
return False
|
||||
elif record and status == "Success":
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def mark_retrired_transaction(log_doc, doc_name):
|
||||
record = 0
|
||||
for d in log_doc.get("logger_data"):
|
||||
if d.transaction_name == doc_name and d.transaction_status == "Failed":
|
||||
d.retried = 1
|
||||
record = record + 1
|
||||
|
||||
log_doc.save()
|
||||
|
||||
return record
|
Loading…
Reference in New Issue
Block a user