Merge branch 'develop' into fix-repayment-schedule

This commit is contained in:
Deepesh Garg
2022-11-10 09:39:39 +05:30
committed by GitHub
41 changed files with 856 additions and 270 deletions

View File

@@ -1410,7 +1410,7 @@ class PurchaseInvoice(BuyingController):
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.update_project() self.update_project()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ( self.ignore_linked_doctypes = (

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Repost Payment Ledger', {
setup: function(frm) {
frm.set_query("voucher_type", () => {
return {
filters: {
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']]
}
};
});
frm.fields_dict['repost_vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
return {
filters: {
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']]
}
}
}
frm.fields_dict['repost_vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
if (doc.company) {
return {
filters: {
company: doc.company,
docstatus: 1
}
}
}
}
},
refresh: function(frm) {
if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.repost_status)) {
frm.set_intro(__("Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status."));
var btn_label = __("Repost in background")
frm.add_custom_button(btn_label, () => {
frappe.call({
method: 'erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.execute_repost_payment_ledger',
args: {
docname: frm.doc.name,
}
});
frappe.msgprint(__('Reposting in the background.'));
});
}
}
});

View File

@@ -0,0 +1,159 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-10-19 21:59:33.553852",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"filters_section",
"company",
"posting_date",
"column_break_4",
"voucher_type",
"add_manually",
"status_section",
"repost_status",
"repost_error_log",
"selected_vouchers_section",
"repost_vouchers",
"amended_from"
],
"fields": [
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Repost Payment Ledger",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "selected_vouchers_section",
"fieldtype": "Section Break",
"label": "Vouchers"
},
{
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "repost_vouchers",
"fieldtype": "Table",
"label": "Selected Vouchers",
"options": "Repost Payment Ledger Items"
},
{
"fieldname": "repost_status",
"fieldtype": "Select",
"label": "Repost Status",
"options": "\nQueued\nFailed\nCompleted",
"read_only": 1
},
{
"fieldname": "status_section",
"fieldtype": "Section Break",
"label": "Status"
},
{
"default": "0",
"description": "Ignore Voucher Type filter and Select Vouchers Manually",
"fieldname": "add_manually",
"fieldtype": "Check",
"label": "Add Manually"
},
{
"depends_on": "eval:doc.repost_error_log",
"fieldname": "repost_error_log",
"fieldtype": "Long Text",
"label": "Repost Error Log"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-11-08 07:38:40.079038",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,111 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils.background_jobs import is_job_queued
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
if voucher_type and voucher_no and gle_map:
_delete_pl_entries(voucher_type, voucher_no)
create_payment_ledger_entry(gle_map, cancel=0)
@frappe.whitelist()
def start_payment_ledger_repost(docname=None):
"""
Repost Payment Ledger Entries for Vouchers through Background Job
"""
if docname:
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
if repost_doc.docstatus == 1 and repost_doc.repost_status in ["Queued", "Failed"]:
try:
for entry in repost_doc.repost_vouchers:
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
else:
gle_map = doc.get_gl_entries()
repost_ple_for_voucher(entry.voucher_type, entry.voucher_no, gle_map)
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", "")
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Completed")
except Exception as e:
frappe.db.rollback()
traceback = frappe.get_traceback()
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Failed")
class RepostPaymentLedger(Document):
def __init__(self, *args, **kwargs):
super(RepostPaymentLedger, self).__init__(*args, **kwargs)
self.vouchers = []
def before_validate(self):
self.load_vouchers_based_on_filters()
self.set_status()
def load_vouchers_based_on_filters(self):
if not self.add_manually:
self.repost_vouchers.clear()
self.get_vouchers()
self.extend("repost_vouchers", copy.deepcopy(self.vouchers))
def get_vouchers(self):
self.vouchers.clear()
filter_on_voucher_types = [self.voucher_type] if self.voucher_type else VOUCHER_TYPES
for vtype in filter_on_voucher_types:
doc = qb.DocType(vtype)
doctype_name = ConstantColumn(vtype)
query = (
qb.from_(doc)
.select(doctype_name.as_("voucher_type"), doc.name.as_("voucher_no"))
.where(
(doc.docstatus == 1)
& (doc.company == self.company)
& (doc.posting_date.gte(self.posting_date))
)
)
entries = query.run(as_dict=True)
self.vouchers.extend(entries)
def set_status(self):
if self.docstatus == 0:
self.repost_status = "Queued"
def on_submit(self):
execute_repost_payment_ledger(self.name)
frappe.msgprint(_("Repost started in the background"))
@frappe.whitelist()
def execute_repost_payment_ledger(docname):
"""Repost Payment Ledger Entries by background job."""
job_name = "payment_ledger_repost_" + docname
if not is_job_queued(job_name):
frappe.enqueue(
method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
docname=docname,
is_async=True,
job_name=job_name,
)

View File

@@ -0,0 +1,12 @@
frappe.listview_settings["Repost Payment Ledger"] = {
add_fields: ["repost_status"],
get_indicator: function(doc) {
var colors = {
'Queued': 'orange',
'Completed': 'green',
'Failed': 'red',
};
let status = doc.repost_status;
return [__(status), colors[status], 'status,=,'+status];
},
};

View File

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

View File

@@ -0,0 +1,35 @@
{
"actions": [],
"creation": "2022-10-20 10:44:18.796489",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"voucher_type",
"voucher_no"
],
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": "Voucher No",
"options": "voucher_type"
}
],
"istable": 1,
"links": [],
"modified": "2022-10-28 14:47:11.838109",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger Items",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RepostPaymentLedgerItems(Document):
pass

View File

@@ -64,6 +64,25 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return); this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update."));
this.frm.add_custom_button(__('Repost Accounting Entries'),
() => {
this.frm.call({
doc: this.frm.doc,
method: 'repost_accounting_entries',
freeze: true,
freeze_message: __('Reposting...'),
callback: (r) => {
if (!r.exc) {
frappe.msgprint(__('Accounting Entries are reposted'));
me.frm.refresh();
}
}
});
}).removeClass('btn-default').addClass('btn-warning');
}
if (this.frm.doc.is_return) { if (this.frm.doc.is_return) {
this.frm.return_print_format = "Sales Invoice Return"; this.frm.return_print_format = "Sales Invoice Return";
} }

View File

@@ -207,6 +207,7 @@
"is_internal_customer", "is_internal_customer",
"is_discounted", "is_discounted",
"remarks", "remarks",
"repost_required",
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
@@ -1035,6 +1036,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "redeem_loyalty_points", "depends_on": "redeem_loyalty_points",
"fieldname": "loyalty_redemption_account", "fieldname": "loyalty_redemption_account",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1333,6 +1335,7 @@
"options": "fa fa-money" "options": "fa fa-money"
}, },
{ {
"allow_on_submit": 1,
"depends_on": "is_pos", "depends_on": "is_pos",
"fieldname": "cash_bank_account", "fieldname": "cash_bank_account",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1432,6 +1435,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "is_pos", "depends_on": "is_pos",
"fieldname": "account_for_change_amount", "fieldname": "account_for_change_amount",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1480,6 +1484,7 @@
"hide_seconds": 1 "hide_seconds": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "write_off_account", "fieldname": "write_off_account",
"fieldtype": "Link", "fieldtype": "Link",
"hide_days": 1, "hide_days": 1,
@@ -1703,6 +1708,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"default": "No", "default": "No",
"fieldname": "is_opening", "fieldname": "is_opening",
"fieldtype": "Select", "fieldtype": "Select",
@@ -1917,6 +1923,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "eval:doc.is_internal_customer", "depends_on": "eval:doc.is_internal_customer",
"description": "Unrealized Profit / Loss account for intra-company transfers", "description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account", "fieldname": "unrealized_profit_loss_account",
@@ -1959,6 +1966,7 @@
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "additional_discount_account", "fieldname": "additional_discount_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Discount Account", "label": "Discount Account",
@@ -2097,6 +2105,15 @@
"hide_seconds": 1, "hide_seconds": 1,
"label": "Write Off", "label": "Write Off",
"width": "50%" "width": "50%"
},
{
"default": "0",
"fieldname": "repost_required",
"fieldtype": "Check",
"hidden": 1,
"label": "Repost Required",
"no_copy": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@@ -2109,7 +2126,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-10-11 13:07:36.488095", "modified": "2022-11-07 16:02:07.972258",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -11,6 +11,9 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form
import erpnext import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points, get_loyalty_program_details_with_points,
validate_loyalty_points, validate_loyalty_points,
@@ -100,13 +103,11 @@ class SalesInvoice(SellingController):
self.validate_debit_to_acc() self.validate_debit_to_acc()
self.clear_unallocated_advances("Sales Invoice Advance", "advances") self.clear_unallocated_advances("Sales Invoice Advance", "advances")
self.add_remarks() self.add_remarks()
self.validate_write_off_account()
self.validate_account_for_change_amount()
self.validate_fixed_asset() self.validate_fixed_asset()
self.set_income_account_for_fixed_assets() self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers() self.validate_item_cost_centers()
self.validate_income_account()
self.check_conversion_rate() self.check_conversion_rate()
self.validate_accounts()
validate_inter_company_party( validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_invoice_reference self.doctype, self.customer, self.company, self.inter_company_invoice_reference
@@ -170,6 +171,11 @@ class SalesInvoice(SellingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_accounts(self):
self.validate_write_off_account()
self.validate_account_for_change_amount()
self.validate_income_account()
def validate_fixed_asset(self): def validate_fixed_asset(self):
for d in self.get("items"): for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
@@ -367,7 +373,8 @@ class SalesInvoice(SellingController):
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.db_set("repost_required", 0)
if ( if (
frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction"
@@ -514,6 +521,92 @@ class SalesInvoice(SellingController):
def on_update(self): def on_update(self):
self.set_paid_amount() self.set_paid_amount()
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
needs_repost = 0
# Check if any field affecting accounting entry is altered
doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if opening entry check updated
if doc_before_update.get("is_opening") != self.is_opening:
needs_repost = 1
if not needs_repost:
# Parent Level Accounts excluding party account
for field in (
"additional_discount_account",
"cash_bank_account",
"account_for_change_amount",
"write_off_account",
"loyalty_redemption_account",
"unrealized_profit_loss_account",
):
if doc_before_update.get(field) != self.get(field):
needs_repost = 1
break
# Check for parent accounting dimensions
for dimension in accounting_dimensions:
if doc_before_update.get(dimension) != self.get(dimension):
needs_repost = 1
break
# Check for child tables
if self.check_if_child_table_updated(
"items",
doc_before_update,
("income_account", "expense_account", "discount_account"),
accounting_dimensions,
):
needs_repost = 1
if self.check_if_child_table_updated(
"taxes", doc_before_update, ("account_head",), accounting_dimensions
):
needs_repost = 1
self.validate_accounts()
# validate if deferred revenue is enabled for any item
# Don't allow to update the invoice if deferred revenue is enabled
for item in self.get("items"):
if item.enable_deferred_revenue:
frappe.throw(
_(
"Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission."
).format(item.item_code)
)
self.db_set("repost_required", needs_repost)
def check_if_child_table_updated(
self, child_table, doc_before_update, fields_to_check, accounting_dimensions
):
# Check if any field affecting accounting entry is altered
for index, item in enumerate(self.get(child_table)):
for field in fields_to_check:
if doc_before_update.get(child_table)[index].get(field) != item.get(field):
return True
for dimension in accounting_dimensions:
if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension):
return True
return False
@frappe.whitelist()
def repost_accounting_entries(self):
if self.repost_required:
self.docstatus = 2
self.make_gl_entries_on_cancel()
self.docstatus = 1
self.make_gl_entries()
self.db_set("repost_required", 0)
else:
frappe.throw(_("No updates pending for reposting"))
def set_paid_amount(self): def set_paid_amount(self):
paid_amount = 0.0 paid_amount = 0.0
base_paid_amount = 0.0 base_paid_amount = 0.0
@@ -2306,7 +2399,7 @@ def get_loyalty_programs(customer):
lp_details = get_loyalty_programs(customer) lp_details = get_loyalty_programs(customer)
if len(lp_details) == 1: if len(lp_details) == 1:
frappe.db.set(customer, "loyalty_program", lp_details[0]) customer.db_set("loyalty_program", lp_details[0])
return lp_details return lp_details
else: else:
return lp_details return lp_details

View File

@@ -2729,6 +2729,31 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
# Update Invoice post submit and then check GL Entries again
si.load_from_db()
si.items[0].income_account = "Service - _TC"
si.additional_discount_account = "_Test Account Sales - _TC"
si.taxes[0].account_head = "TDS Payable - _TC"
si.save()
si.load_from_db()
self.assertTrue(si.repost_required)
si.repost_accounting_entries()
expected_gle = [
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
["Debtors - _TC", 88, 0.0, nowdate()],
["Service - _TC", 0.0, 100.0, nowdate()],
["TDS Payable - _TC", 0.0, 10.0, nowdate()],
]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
si.load_from_db()
self.assertFalse(si.repost_required)
def test_asset_depreciation_on_sale_with_pro_rata(self): def test_asset_depreciation_on_sale_with_pro_rata(self):
""" """
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
@@ -3286,6 +3311,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
"""select account, debit, credit, posting_date """select account, debit, credit, posting_date
from `tabGL Entry` from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s
and is_cancelled = 0
order by posting_date asc, account asc""", order by posting_date asc, account asc""",
(voucher_no, posting_date), (voucher_no, posting_date),
as_dict=1, as_dict=1,

View File

@@ -438,6 +438,7 @@
"label": "Accounting Details" "label": "Accounting Details"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "income_account", "fieldname": "income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Income Account", "label": "Income Account",
@@ -450,6 +451,7 @@
"width": "120px" "width": "120px"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "expense_account", "fieldname": "expense_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Expense Account", "label": "Expense Account",
@@ -469,6 +471,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"allow_on_submit": 1,
"default": ":Company", "default": ":Company",
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
@@ -800,6 +803,7 @@
"options": "Finance Book" "options": "Finance Book"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
@@ -822,7 +826,6 @@
"label": "Incoming Rate (Costing)", "label": "Incoming Rate (Costing)",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1 "print_hide": 1
}, },
{ {
@@ -835,6 +838,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "discount_account", "fieldname": "discount_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Discount Account", "label": "Discount Account",

View File

@@ -51,6 +51,7 @@
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
{ {
"allow_on_submit": 1,
"columns": 2, "columns": 2,
"fieldname": "account_head", "fieldname": "account_head",
"fieldtype": "Link", "fieldtype": "Link",
@@ -63,6 +64,7 @@
"search_index": 1 "search_index": 1
}, },
{ {
"allow_on_submit": 1,
"default": ":Company", "default": ":Company",
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
@@ -216,12 +218,13 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-05 20:04:01.726867", "modified": "2022-10-17 13:08:17.776528",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Taxes and Charges", "name": "Sales Taxes and Charges",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@@ -1146,10 +1146,10 @@ def repost_gle_for_stock_vouchers(
if not existing_gle or not compare_existing_and_expected_gle( if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision existing_gle, expected_gle, precision
): ):
_delete_gl_entries(voucher_type, voucher_no) _delete_accounting_ledger_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else: else:
_delete_gl_entries(voucher_type, voucher_no) _delete_accounting_ledger_entries(voucher_type, voucher_no)
if not frappe.flags.in_test: if not frappe.flags.in_test:
frappe.db.commit() frappe.db.commit()
@@ -1161,18 +1161,28 @@ def repost_gle_for_stock_vouchers(
) )
def _delete_gl_entries(voucher_type, voucher_no): def _delete_pl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
qb.from_(ple).delete().where( qb.from_(ple).delete().where(
(ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no) (ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)
).run() ).run()
def _delete_gl_entries(voucher_type, voucher_no):
gle = qb.DocType("GL Entry")
qb.from_(gle).delete().where(
(gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)
).run()
def _delete_accounting_ledger_entries(voucher_type, voucher_no):
"""
Remove entries from both General and Payment Ledger for specified Voucher
"""
_delete_gl_entries(voucher_type, voucher_no)
_delete_pl_entries(voucher_type, voucher_no)
def sort_stock_vouchers_by_posting_date( def sort_stock_vouchers_by_posting_date(
stock_vouchers: List[Tuple[str, str]] stock_vouchers: List[Tuple[str, str]]
) -> List[Tuple[str, str]]: ) -> List[Tuple[str, str]]:

View File

@@ -361,7 +361,7 @@ class PurchaseOrder(BuyingController):
self.update_reserved_qty_for_subcontract() self.update_reserved_qty_for_subcontract()
self.check_on_hold_or_closed_status() self.check_on_hold_or_closed_status()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_prevdoc_status() self.update_prevdoc_status()

View File

@@ -31,7 +31,7 @@ class RequestforQuotation(BuyingController):
if self.docstatus < 1: if self.docstatus < 1:
# after amend and save, status still shows as cancelled, until submit # after amend and save, status still shows as cancelled, until submit
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def validate_duplicate_supplier(self): def validate_duplicate_supplier(self):
supplier_list = [d.supplier for d in self.suppliers] supplier_list = [d.supplier for d in self.suppliers]
@@ -73,14 +73,14 @@ class RequestforQuotation(BuyingController):
) )
def on_submit(self): def on_submit(self):
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
for supplier in self.suppliers: for supplier in self.suppliers:
supplier.email_sent = 0 supplier.email_sent = 0
supplier.quote_status = "Pending" supplier.quote_status = "Pending"
self.send_to_supplier() self.send_to_supplier()
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
@frappe.whitelist() @frappe.whitelist()
def get_supplier_email_preview(self, supplier): def get_supplier_email_preview(self, supplier):

View File

@@ -145,7 +145,7 @@ class Supplier(TransactionBase):
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name": if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name":
frappe.db.set(self, "supplier_name", newdn) self.db_set("supplier_name", newdn)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -30,11 +30,11 @@ class SupplierQuotation(BuyingController):
self.validate_valid_till() self.validate_valid_till()
def on_submit(self): def on_submit(self):
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
self.update_rfq_supplier_status(1) self.update_rfq_supplier_status(1)
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_rfq_supplier_status(0) self.update_rfq_supplier_status(0)
def on_trash(self): def on_trash(self):

View File

@@ -60,7 +60,7 @@ class Opportunity(TransactionBase, CRMNote):
if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field): if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
try: try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field) value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
frappe.db.set(self, field, value) self.db_set(field, value)
except Exception: except Exception:
continue continue

View File

@@ -119,7 +119,7 @@ class MaintenanceSchedule(TransactionBase):
event.add_participant(self.doctype, self.name) event.add_participant(self.doctype, self.name)
event.insert(ignore_permissions=1) event.insert(ignore_permissions=1)
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person): def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person):
schedule_list = [] schedule_list = []
@@ -245,7 +245,7 @@ class MaintenanceSchedule(TransactionBase):
self.generate_schedule() self.generate_schedule()
def on_update(self): def on_update(self):
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def update_amc_date(self, serial_nos, amc_expiry_date=None): def update_amc_date(self, serial_nos, amc_expiry_date=None):
for serial_no in serial_nos: for serial_no in serial_nos:
@@ -344,7 +344,7 @@ class MaintenanceSchedule(TransactionBase):
if d.serial_no: if d.serial_no:
serial_nos = get_valid_serial_nos(d.serial_no) serial_nos = get_valid_serial_nos(d.serial_no)
self.update_amc_date(serial_nos) self.update_amc_date(serial_nos)
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
delete_events(self.doctype, self.name) delete_events(self.doctype, self.name)
def on_trash(self): def on_trash(self):

View File

@@ -125,12 +125,12 @@ class MaintenanceVisit(TransactionBase):
def on_submit(self): def on_submit(self):
self.update_customer_issue(1) self.update_customer_issue(1)
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
self.update_status_and_actual_date() self.update_status_and_actual_date()
def on_cancel(self): def on_cancel(self):
self.check_if_last_visit() self.check_if_last_visit()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_status_and_actual_date(cancel=True) self.update_status_and_actual_date(cancel=True)
def on_update(self): def on_update(self):

View File

@@ -206,8 +206,8 @@ class BOM(WebsiteGenerator):
self.manage_default_bom() self.manage_default_bom()
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "is_active", 0) self.db_set("is_active", 0)
frappe.db.set(self, "is_default", 0) self.db_set("is_default", 0)
# check if used in any other bom # check if used in any other bom
self.validate_bom_links() self.validate_bom_links()
@@ -449,10 +449,10 @@ class BOM(WebsiteGenerator):
not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1)) not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1))
and self.is_active and self.is_active
): ):
frappe.db.set(self, "is_default", 1) self.db_set("is_default", 1)
frappe.db.set_value("Item", self.item, "default_bom", self.name) frappe.db.set_value("Item", self.item, "default_bom", self.name)
else: else:
frappe.db.set(self, "is_default", 0) self.db_set("is_default", 0)
item = frappe.get_doc("Item", self.item) item = frappe.get_doc("Item", self.item)
if item.default_bom == self.name: if item.default_bom == self.name:
frappe.db.set_value("Item", self.item, "default_bom", None) frappe.db.set_value("Item", self.item, "default_bom", None)

View File

@@ -373,7 +373,7 @@ class WorkOrder(Document):
def on_cancel(self): def on_cancel(self):
self.validate_cancel() self.validate_cancel()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
if self.production_plan and frappe.db.exists( if self.production_plan and frappe.db.exists(
"Production Plan Item Reference", {"parent": self.production_plan} "Production Plan Item Reference", {"parent": self.production_plan}

View File

@@ -100,9 +100,7 @@ def get_default_holiday_list():
def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime): def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime):
if from_datetime and to_datetime: if from_datetime and to_datetime:
if not cint( if not frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"):
frappe.db.get_value("Manufacturing Settings", None, "allow_production_on_holidays")
):
check_workstation_for_holiday(workstation, from_datetime, to_datetime) check_workstation_for_holiday(workstation, from_datetime, to_datetime)
if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")): if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")):

View File

@@ -92,18 +92,26 @@ frappe.ui.form.on("Timesheet", {
frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false); frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false);
frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false); frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false);
} }
let filters = {
"status": "Open"
};
if (frm.doc.customer) {
filters["customer"] = frm.doc.customer;
}
frm.set_query('parent_project', function(doc) {
return {
filters: filters
};
});
frm.trigger('setup_filters'); frm.trigger('setup_filters');
frm.trigger('set_dynamic_field_label'); frm.trigger('set_dynamic_field_label');
}, },
customer: function(frm) { customer: function(frm) {
frm.set_query('parent_project', function(doc) {
return {
filters: {
"customer": doc.customer
}
};
});
frm.set_query('project', 'time_logs', function(doc) { frm.set_query('project', 'time_logs', function(doc) {
return { return {
filters: { filters: {

View File

@@ -143,6 +143,12 @@ var get_payment_mode_account = function(frm, mode_of_payment, callback) {
cur_frm.cscript.account_head = function(doc, cdt, cdn) { cur_frm.cscript.account_head = function(doc, cdt, cdn) {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
if (doc.docstatus == 1) {
// Should not trigger any changes on change post submit
return;
}
if(!d.charge_type && d.account_head){ if(!d.charge_type && d.account_head){
frappe.msgprint(__("Please select Charge Type first")); frappe.msgprint(__("Please select Charge Type first"));
frappe.model.set_value(cdt, cdn, "account_head", ""); frappe.model.set_value(cdt, cdn, "account_head", "");

View File

@@ -294,7 +294,7 @@ class Customer(TransactionBase):
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": if frappe.defaults.get_global_default("cust_master_name") == "Customer Name":
frappe.db.set(self, "customer_name", newdn) self.db_set("customer_name", newdn)
def set_loyalty_program(self): def set_loyalty_program(self):
if self.loyalty_program: if self.loyalty_program:

View File

@@ -87,13 +87,13 @@ class InstallationNote(TransactionBase):
frappe.throw(_("Please pull items from Delivery Note")) frappe.throw(_("Please pull items from Delivery Note"))
def on_update(self): def on_update(self):
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def on_submit(self): def on_submit(self):
self.validate_serial_no() self.validate_serial_no()
self.update_prevdoc_status() self.update_prevdoc_status()
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
def on_cancel(self): def on_cancel(self):
self.update_prevdoc_status() self.update_prevdoc_status()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")

View File

@@ -119,10 +119,10 @@ class Quotation(SellingController):
if not (self.is_fully_ordered() or self.is_partially_ordered()): if not (self.is_fully_ordered() or self.is_partially_ordered()):
get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"])
lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons]
frappe.db.set(self, "status", "Lost") self.db_set("status", "Lost")
if detailed_reason: if detailed_reason:
frappe.db.set(self, "order_lost_reason", detailed_reason) self.db_set("order_lost_reason", detailed_reason)
for reason in lost_reasons_list: for reason in lost_reasons_list:
if reason.get("lost_reason") in lost_reasons_lst: if reason.get("lost_reason") in lost_reasons_lst:

View File

@@ -246,7 +246,7 @@ class SalesOrder(SellingController):
self.update_project() self.update_project()
self.update_prevdoc_status("cancel") self.update_prevdoc_status("cancel")
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_blanket_order() self.update_blanket_order()

View File

@@ -207,15 +207,14 @@ class Company(NestedSet):
frappe.local.flags.ignore_root_company_validation = True frappe.local.flags.ignore_root_company_validation = True
create_charts(self.name, self.chart_of_accounts, self.existing_company) create_charts(self.name, self.chart_of_accounts, self.existing_company)
frappe.db.set( self.db_set(
self,
"default_receivable_account", "default_receivable_account",
frappe.db.get_value( frappe.db.get_value(
"Account", {"company": self.name, "account_type": "Receivable", "is_group": 0} "Account", {"company": self.name, "account_type": "Receivable", "is_group": 0}
), ),
) )
frappe.db.set(
self, self.db_set(
"default_payable_account", "default_payable_account",
frappe.db.get_value( frappe.db.get_value(
"Account", {"company": self.name, "account_type": "Payable", "is_group": 0} "Account", {"company": self.name, "account_type": "Payable", "is_group": 0}
@@ -491,12 +490,12 @@ class Company(NestedSet):
cc_doc.flags.ignore_mandatory = True cc_doc.flags.ignore_mandatory = True
cc_doc.insert() cc_doc.insert()
frappe.db.set(self, "cost_center", _("Main") + " - " + self.abbr) self.db_set("cost_center", _("Main") + " - " + self.abbr)
frappe.db.set(self, "round_off_cost_center", _("Main") + " - " + self.abbr) self.db_set("round_off_cost_center", _("Main") + " - " + self.abbr)
frappe.db.set(self, "depreciation_cost_center", _("Main") + " - " + self.abbr) self.db_set("depreciation_cost_center", _("Main") + " - " + self.abbr)
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
frappe.db.set(self, "company_name", newdn) self.db_set("company_name", newdn)
frappe.db.sql( frappe.db.sql(
"""update `tabDefaultValue` set defvalue=%s """update `tabDefaultValue` set defvalue=%s

View File

@@ -120,7 +120,6 @@ class MaterialRequest(BuyingController):
self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100] self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100]
def on_submit(self): def on_submit(self):
# frappe.db.set(self, 'status', 'Submitted')
self.update_requested_qty() self.update_requested_qty()
self.update_requested_qty_in_production_plan() self.update_requested_qty_in_production_plan()
if self.material_request_type == "Purchase": if self.material_request_type == "Purchase":

View File

@@ -216,7 +216,7 @@ class TestMaterialRequest(FrappeTestCase):
po.load_from_db() po.load_from_db()
mr.update_status("Stopped") mr.update_status("Stopped")
self.assertRaises(frappe.InvalidStatusError, po.submit) self.assertRaises(frappe.InvalidStatusError, po.submit)
frappe.db.set(po, "docstatus", 1) po.db_set("docstatus", 1)
self.assertRaises(frappe.InvalidStatusError, po.cancel) self.assertRaises(frappe.InvalidStatusError, po.cancel)
# resubmit and check for per complete # resubmit and check for per complete

View File

@@ -3,7 +3,6 @@ frappe.listview_settings['Stock Entry'] = {
"`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`", "`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`",
"`tabStock Entry`.`is_return`"], "`tabStock Entry`.`is_return`"],
get_indicator: function (doc) { get_indicator: function (doc) {
debugger
if(doc.is_return===1 && doc.purpose === "Material Transfer for Manufacture") { if(doc.is_return===1 && doc.purpose === "Material Transfer for Manufacture") {
return [__("Material Returned from WIP"), "orange", return [__("Material Returned from WIP"), "orange",
"is_return,=,1|purpose,=,Material Transfer for Manufacture|docstatus,<,2"]; "is_return,=,1|purpose,=,Material Transfer for Manufacture|docstatus,<,2"];

View File

@@ -57,6 +57,18 @@ frappe.ui.form.on('Subcontracting Receipt', {
filters: { 'company': frm.doc.company } filters: { 'company': frm.doc.company }
}; };
}); });
frappe.db.get_single_value('Buying Settings', 'backflush_raw_materials_of_subcontract_based_on').then(val => {
if (val == 'Material Transferred for Subcontract') {
frm.fields_dict['supplied_items'].grid.grid_rows.forEach((grid_row) => {
grid_row.docfields.forEach((df) => {
if (df.fieldname == 'consumed_qty') {
df.read_only = 0;
}
});
});
}
});
}, },
refresh: (frm) => { refresh: (frm) => {

View File

@@ -4,6 +4,9 @@ from frappe import _
def get_data(): def get_data():
return { return {
"fieldname": "subcontracting_receipt_no", "fieldname": "subcontracting_receipt_no",
"non_standard_fieldnames": {
"Subcontracting Receipt": "return_against",
},
"internal_links": { "internal_links": {
"Subcontracting Order": ["items", "subcontracting_order"], "Subcontracting Order": ["items", "subcontracting_order"],
"Project": ["items", "project"], "Project": ["items", "project"],
@@ -11,5 +14,6 @@ def get_data():
}, },
"transactions": [ "transactions": [
{"label": _("Reference"), "items": ["Subcontracting Order", "Quality Inspection", "Project"]}, {"label": _("Reference"), "items": ["Subcontracting Order", "Quality Inspection", "Project"]},
{"label": _("Returns"), "items": ["Subcontracting Receipt"]},
], ],
} }

View File

@@ -86,6 +86,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Consumed Qty", "label": "Consumed Qty",
"read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@@ -193,7 +194,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-09-02 22:28:53.392381", "modified": "2022-11-07 17:17:21.670761",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt Supplied Item", "name": "Subcontracting Receipt Supplied Item",

View File

@@ -35,7 +35,7 @@ class WarrantyClaim(TransactionBase):
lst1 = ",".join(x[0] for x in lst) lst1 = ",".join(x[0] for x in lst)
frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1)) frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1))
else: else:
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
def on_update(self): def on_update(self):
pass pass