mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-22 14:39:19 +00:00
Merge pull request #40443 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -236,14 +236,16 @@ def get_accounting_dimensions(as_list=True, filters=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_checks_for_pl_and_bs_accounts():
|
def get_checks_for_pl_and_bs_accounts():
|
||||||
dimensions = frappe.db.sql(
|
if frappe.flags.accounting_dimensions_details is None:
|
||||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
# nosemgrep
|
||||||
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
frappe.flags.accounting_dimensions_details = frappe.db.sql(
|
||||||
WHERE p.name = c.parent""",
|
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||||
as_dict=1,
|
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
||||||
)
|
WHERE p.name = c.parent""",
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
return dimensions
|
return frappe.flags.accounting_dimensions_details
|
||||||
|
|
||||||
|
|
||||||
def get_dimension_with_children(doctype, dimensions):
|
def get_dimension_with_children(doctype, dimensions):
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ class TestAccountingDimension(unittest.TestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
disable_dimension()
|
disable_dimension()
|
||||||
|
frappe.flags.accounting_dimensions_details = None
|
||||||
|
frappe.flags.dimension_filter_map = None
|
||||||
|
|
||||||
|
|
||||||
def create_dimension():
|
def create_dimension():
|
||||||
|
|||||||
@@ -38,37 +38,41 @@ class AccountingDimensionFilter(Document):
|
|||||||
|
|
||||||
|
|
||||||
def get_dimension_filter_map():
|
def get_dimension_filter_map():
|
||||||
filters = frappe.db.sql(
|
if not frappe.flags.get("dimension_filter_map"):
|
||||||
"""
|
# nosemgrep
|
||||||
SELECT
|
filters = frappe.db.sql(
|
||||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
"""
|
||||||
p.allow_or_restrict, a.is_mandatory
|
SELECT
|
||||||
FROM
|
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||||
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
p.allow_or_restrict, a.is_mandatory
|
||||||
`tabAccounting Dimension Filter` p
|
FROM
|
||||||
WHERE
|
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
||||||
p.name = a.parent
|
`tabAccounting Dimension Filter` p
|
||||||
AND p.disabled = 0
|
WHERE
|
||||||
AND p.name = d.parent
|
p.name = a.parent
|
||||||
""",
|
AND p.disabled = 0
|
||||||
as_dict=1,
|
AND p.name = d.parent
|
||||||
)
|
""",
|
||||||
|
as_dict=1,
|
||||||
dimension_filter_map = {}
|
|
||||||
|
|
||||||
for f in filters:
|
|
||||||
f.fieldname = scrub(f.accounting_dimension)
|
|
||||||
|
|
||||||
build_map(
|
|
||||||
dimension_filter_map,
|
|
||||||
f.fieldname,
|
|
||||||
f.applicable_on_account,
|
|
||||||
f.dimension_value,
|
|
||||||
f.allow_or_restrict,
|
|
||||||
f.is_mandatory,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return dimension_filter_map
|
dimension_filter_map = {}
|
||||||
|
|
||||||
|
for f in filters:
|
||||||
|
f.fieldname = scrub(f.accounting_dimension)
|
||||||
|
|
||||||
|
build_map(
|
||||||
|
dimension_filter_map,
|
||||||
|
f.fieldname,
|
||||||
|
f.applicable_on_account,
|
||||||
|
f.dimension_value,
|
||||||
|
f.allow_or_restrict,
|
||||||
|
f.is_mandatory,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.flags.dimension_filter_map = dimension_filter_map
|
||||||
|
|
||||||
|
return frappe.flags.dimension_filter_map
|
||||||
|
|
||||||
|
|
||||||
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
|
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class TestAccountingDimensionFilter(unittest.TestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
disable_dimension_filter()
|
disable_dimension_filter()
|
||||||
disable_dimension()
|
disable_dimension()
|
||||||
|
frappe.flags.accounting_dimensions_details = None
|
||||||
|
frappe.flags.dimension_filter_map = None
|
||||||
|
|
||||||
for si in self.invoice_list:
|
for si in self.invoice_list:
|
||||||
si.load_from_db()
|
si.load_from_db()
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ class Budget(Document):
|
|||||||
|
|
||||||
def validate_expense_against_budget(args, expense_amount=0):
|
def validate_expense_against_budget(args, expense_amount=0):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
if not frappe.get_all("Budget", limit=1):
|
||||||
|
return
|
||||||
|
|
||||||
if args.get("company") and not args.fiscal_year:
|
if args.get("company") and not args.fiscal_year:
|
||||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||||
@@ -116,6 +118,11 @@ def validate_expense_against_budget(args, expense_amount=0):
|
|||||||
"Company", args.get("company"), "exception_budget_approver_role"
|
"Company", args.get("company"), "exception_budget_approver_role"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not frappe.get_cached_value(
|
||||||
|
"Budget", {"fiscal_year": args.fiscal_year, "company": args.company}
|
||||||
|
): # nosec
|
||||||
|
return
|
||||||
|
|
||||||
if not args.account:
|
if not args.account:
|
||||||
args.account = args.get("expense_account")
|
args.account = args.get("expense_account")
|
||||||
|
|
||||||
@@ -142,13 +149,13 @@ def validate_expense_against_budget(args, expense_amount=0):
|
|||||||
if (
|
if (
|
||||||
args.get(budget_against)
|
args.get(budget_against)
|
||||||
and args.account
|
and args.account
|
||||||
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
|
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
|
||||||
):
|
):
|
||||||
|
|
||||||
doctype = dimension.get("document_type")
|
doctype = dimension.get("document_type")
|
||||||
|
|
||||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||||
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||||
condition = """and exists(select name from `tab%s`
|
condition = """and exists(select name from `tab%s`
|
||||||
where lft<=%s and rgt>=%s and name=b.%s)""" % (
|
where lft<=%s and rgt>=%s and name=b.%s)""" % (
|
||||||
doctype,
|
doctype,
|
||||||
|
|||||||
@@ -1,94 +1,42 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 0,
|
"creation": "2016-05-16 11:54:09.286135",
|
||||||
"allow_rename": 0,
|
"doctype": "DocType",
|
||||||
"beta": 0,
|
"editable_grid": 1,
|
||||||
"creation": "2016-05-16 11:54:09.286135",
|
"engine": "InnoDB",
|
||||||
"custom": 0,
|
"field_order": [
|
||||||
"docstatus": 0,
|
"account",
|
||||||
"doctype": "DocType",
|
"budget_amount"
|
||||||
"document_type": "",
|
],
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "account",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"in_list_view": 1,
|
||||||
"columns": 0,
|
"label": "Account",
|
||||||
"fieldname": "account",
|
"options": "Account",
|
||||||
"fieldtype": "Link",
|
"reqd": 1,
|
||||||
"hidden": 0,
|
"search_index": 1
|
||||||
"ignore_user_permissions": 0,
|
},
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Account",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Account",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "budget_amount",
|
||||||
"bold": 0,
|
"fieldtype": "Currency",
|
||||||
"collapsible": 0,
|
"in_list_view": 1,
|
||||||
"columns": 0,
|
"label": "Budget Amount",
|
||||||
"fieldname": "budget_amount",
|
"options": "Company:company:default_currency",
|
||||||
"fieldtype": "Currency",
|
"reqd": 1
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Budget Amount",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company:company:default_currency",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
"istable": 1,
|
||||||
"hide_toolbar": 0,
|
"links": [],
|
||||||
"idx": 0,
|
"modified": "2024-03-04 15:43:27.016947",
|
||||||
"image_view": 0,
|
"modified_by": "Administrator",
|
||||||
"in_create": 0,
|
"module": "Accounts",
|
||||||
|
"name": "Budget Account",
|
||||||
"is_submittable": 0,
|
"owner": "Administrator",
|
||||||
"issingle": 0,
|
"permissions": [],
|
||||||
"istable": 1,
|
"quick_entry": 1,
|
||||||
"max_attachments": 0,
|
"sort_field": "modified",
|
||||||
"modified": "2017-01-02 17:02:53.339420",
|
"sort_order": "DESC",
|
||||||
"modified_by": "Administrator",
|
"states": []
|
||||||
"module": "Accounts",
|
|
||||||
"name": "Budget Account",
|
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"quick_entry": 1,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ class GLEntry(Document):
|
|||||||
frappe.throw(msg, title=_("Missing Cost Center"))
|
frappe.throw(msg, title=_("Missing Cost Center"))
|
||||||
|
|
||||||
def validate_dimensions_for_pl_and_bs(self):
|
def validate_dimensions_for_pl_and_bs(self):
|
||||||
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
account_type = frappe.get_cached_value("Account", self.account, "report_type")
|
||||||
|
|
||||||
for dimension in get_checks_for_pl_and_bs_accounts():
|
for dimension in get_checks_for_pl_and_bs_accounts():
|
||||||
if (
|
if (
|
||||||
@@ -159,7 +159,7 @@ class GLEntry(Document):
|
|||||||
def check_pl_account(self):
|
def check_pl_account(self):
|
||||||
if (
|
if (
|
||||||
self.is_opening == "Yes"
|
self.is_opening == "Yes"
|
||||||
and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss"
|
and frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss"
|
||||||
and not self.is_cancelled
|
and not self.is_cancelled
|
||||||
):
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
@@ -252,7 +252,7 @@ class GLEntry(Document):
|
|||||||
|
|
||||||
def validate_balance_type(account, adv_adj=False):
|
def validate_balance_type(account, adv_adj=False):
|
||||||
if not adv_adj and account:
|
if not adv_adj and account:
|
||||||
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
|
balance_must_be = frappe.get_cached_value("Account", account, "balance_must_be")
|
||||||
if balance_must_be:
|
if balance_must_be:
|
||||||
balance = frappe.db.sql(
|
balance = frappe.db.sql(
|
||||||
"""select sum(debit) - sum(credit)
|
"""select sum(debit) - sum(credit)
|
||||||
@@ -279,7 +279,7 @@ def update_outstanding_amt(
|
|||||||
party_condition = ""
|
party_condition = ""
|
||||||
|
|
||||||
if against_voucher_type == "Sales Invoice":
|
if against_voucher_type == "Sales Invoice":
|
||||||
party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to")
|
party_account = frappe.get_cached_value(against_voucher_type, against_voucher, "debit_to")
|
||||||
account_condition = "and account in ({0}, {1})".format(
|
account_condition = "and account in ({0}, {1})".format(
|
||||||
frappe.db.escape(account), frappe.db.escape(party_account)
|
frappe.db.escape(account), frappe.db.escape(party_account)
|
||||||
)
|
)
|
||||||
@@ -347,7 +347,7 @@ def update_outstanding_amt(
|
|||||||
def validate_frozen_account(account, adv_adj=None):
|
def validate_frozen_account(account, adv_adj=None):
|
||||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||||
if frozen_account == "Yes" and not adv_adj:
|
if frozen_account == "Yes" and not adv_adj:
|
||||||
frozen_accounts_modifier = frappe.db.get_value(
|
frozen_accounts_modifier = frappe.get_cached_value(
|
||||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -132,11 +132,12 @@ class PaymentLedgerEntry(Document):
|
|||||||
def on_update(self):
|
def on_update(self):
|
||||||
adv_adj = self.flags.adv_adj
|
adv_adj = self.flags.adv_adj
|
||||||
if not self.flags.from_repost:
|
if not self.flags.from_repost:
|
||||||
self.validate_account_details()
|
|
||||||
self.validate_dimensions_for_pl_and_bs()
|
|
||||||
self.validate_allowed_dimensions()
|
|
||||||
validate_balance_type(self.account, adv_adj)
|
|
||||||
validate_frozen_account(self.account, adv_adj)
|
validate_frozen_account(self.account, adv_adj)
|
||||||
|
if not self.delinked:
|
||||||
|
self.validate_account_details()
|
||||||
|
self.validate_dimensions_for_pl_and_bs()
|
||||||
|
self.validate_allowed_dimensions()
|
||||||
|
validate_balance_type(self.account, adv_adj)
|
||||||
|
|
||||||
# update outstanding amount
|
# update outstanding amount
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"is_paid",
|
"is_paid",
|
||||||
"is_return",
|
"is_return",
|
||||||
"return_against",
|
"return_against",
|
||||||
|
"update_outstanding_for_self",
|
||||||
"update_billed_amount_in_purchase_order",
|
"update_billed_amount_in_purchase_order",
|
||||||
"update_billed_amount_in_purchase_receipt",
|
"update_billed_amount_in_purchase_receipt",
|
||||||
"apply_tds",
|
"apply_tds",
|
||||||
@@ -1604,13 +1605,21 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Transaction Date Exchange Rate",
|
"label": "Use Transaction Date Exchange Rate",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||||
|
"description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
|
||||||
|
"fieldname": "update_outstanding_for_self",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Update Outstanding for Self"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-25 11:20:28.366808",
|
"modified": "2024-03-11 14:46:30.298184",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, throw
|
from frappe import _, qb, throw
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||||
@@ -575,13 +575,12 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.db_set("repost_required", self.needs_repost)
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
if not gl_entries:
|
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||||
gl_entries = self.get_gl_entries()
|
if self.docstatus == 1:
|
||||||
|
if not gl_entries:
|
||||||
|
gl_entries = self.get_gl_entries()
|
||||||
|
|
||||||
if gl_entries:
|
if gl_entries:
|
||||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
|
||||||
|
|
||||||
if self.docstatus == 1:
|
|
||||||
make_gl_entries(
|
make_gl_entries(
|
||||||
gl_entries,
|
gl_entries,
|
||||||
update_outstanding=update_outstanding,
|
update_outstanding=update_outstanding,
|
||||||
@@ -589,29 +588,43 @@ class PurchaseInvoice(BuyingController):
|
|||||||
from_repost=from_repost,
|
from_repost=from_repost,
|
||||||
)
|
)
|
||||||
self.make_exchange_gain_loss_journal()
|
self.make_exchange_gain_loss_journal()
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
|
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
|
||||||
if provisional_entries:
|
|
||||||
for entry in provisional_entries:
|
|
||||||
frappe.db.set_value(
|
|
||||||
"GL Entry",
|
|
||||||
{"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
|
|
||||||
"is_cancelled",
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
|
|
||||||
if update_outstanding == "No":
|
|
||||||
update_outstanding_amt(
|
|
||||||
self.credit_to,
|
|
||||||
"Supplier",
|
|
||||||
self.supplier,
|
|
||||||
self.doctype,
|
|
||||||
self.return_against if cint(self.is_return) and self.return_against else self.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock:
|
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
self.cancel_provisional_entries()
|
||||||
|
|
||||||
|
self.update_supplier_outstanding(update_outstanding)
|
||||||
|
|
||||||
|
def cancel_provisional_entries(self):
|
||||||
|
rows = set()
|
||||||
|
purchase_receipts = set()
|
||||||
|
for d in self.items:
|
||||||
|
if d.purchase_receipt:
|
||||||
|
purchase_receipts.add(d.purchase_receipt)
|
||||||
|
rows.add(d.name)
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
# cancel gl entries
|
||||||
|
gle = qb.DocType("GL Entry")
|
||||||
|
gle_update_query = (
|
||||||
|
qb.update(gle)
|
||||||
|
.set(gle.is_cancelled, 1)
|
||||||
|
.where(
|
||||||
|
(gle.voucher_type == "Purchase Receipt")
|
||||||
|
& (gle.voucher_no.isin(purchase_receipts))
|
||||||
|
& (gle.voucher_detail_no.isin(rows))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
gle_update_query.run()
|
||||||
|
|
||||||
|
def update_supplier_outstanding(self, update_outstanding):
|
||||||
|
if update_outstanding == "No":
|
||||||
|
update_outstanding_amt(
|
||||||
|
self.credit_to,
|
||||||
|
"Supplier",
|
||||||
|
self.supplier,
|
||||||
|
self.doctype,
|
||||||
|
self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||||
|
)
|
||||||
|
|
||||||
def get_gl_entries(self, warehouse_account=None):
|
def get_gl_entries(self, warehouse_account=None):
|
||||||
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
||||||
@@ -663,6 +676,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if grand_total and not self.is_internal_transfer():
|
if grand_total and not self.is_internal_transfer():
|
||||||
|
against_voucher = self.name
|
||||||
|
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||||
|
against_voucher = self.return_against
|
||||||
|
|
||||||
# Did not use base_grand_total to book rounding loss gle
|
# Did not use base_grand_total to book rounding loss gle
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
@@ -676,7 +693,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"credit_in_account_currency": base_grand_total
|
"credit_in_account_currency": base_grand_total
|
||||||
if self.party_account_currency == self.company_currency
|
if self.party_account_currency == self.company_currency
|
||||||
else grand_total,
|
else grand_total,
|
||||||
"against_voucher": self.name,
|
"against_voucher": against_voucher,
|
||||||
"against_voucher_type": self.doctype,
|
"against_voucher_type": self.doctype,
|
||||||
"project": self.project,
|
"project": self.project,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
@@ -720,8 +737,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.provisional_enpenses_booked_in_pr = False
|
||||||
purchase_receipt_doc_map = {}
|
if provisional_accounting_for_non_stock_items:
|
||||||
|
self.get_provisional_accounts()
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if flt(item.base_net_amount):
|
if flt(item.base_net_amount):
|
||||||
@@ -858,44 +876,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||||
|
|
||||||
if provisional_accounting_for_non_stock_items:
|
if provisional_accounting_for_non_stock_items:
|
||||||
if item.purchase_receipt:
|
self.make_provisional_gl_entry(gl_entries, item)
|
||||||
provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
|
|
||||||
"Purchase Receipt Item",
|
|
||||||
item.pr_detail,
|
|
||||||
["provisional_expense_account", "qty", "base_rate"],
|
|
||||||
)
|
|
||||||
provisional_account = provisional_account or self.get_company_default(
|
|
||||||
"default_provisional_account"
|
|
||||||
)
|
|
||||||
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
|
|
||||||
|
|
||||||
if not purchase_receipt_doc:
|
|
||||||
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
|
|
||||||
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
|
|
||||||
|
|
||||||
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
|
|
||||||
expense_booked_in_pr = frappe.db.get_value(
|
|
||||||
"GL Entry",
|
|
||||||
{
|
|
||||||
"is_cancelled": 0,
|
|
||||||
"voucher_type": "Purchase Receipt",
|
|
||||||
"voucher_no": item.purchase_receipt,
|
|
||||||
"voucher_detail_no": item.pr_detail,
|
|
||||||
"account": provisional_account,
|
|
||||||
},
|
|
||||||
"name",
|
|
||||||
)
|
|
||||||
|
|
||||||
if expense_booked_in_pr:
|
|
||||||
# Intentionally passing purchase invoice item to handle partial billing
|
|
||||||
purchase_receipt_doc.add_provisional_gl_entry(
|
|
||||||
item,
|
|
||||||
gl_entries,
|
|
||||||
self.posting_date,
|
|
||||||
provisional_account,
|
|
||||||
reverse=1,
|
|
||||||
item_amount=(min(item.qty, pr_qty) * pr_base_rate),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.is_internal_transfer():
|
if not self.is_internal_transfer():
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
@@ -992,6 +973,58 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||||
self.update_gross_purchase_amount_for_linked_assets(item)
|
self.update_gross_purchase_amount_for_linked_assets(item)
|
||||||
|
|
||||||
|
def get_provisional_accounts(self):
|
||||||
|
self.provisional_accounts = frappe._dict()
|
||||||
|
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
|
||||||
|
pr_items = frappe.get_all(
|
||||||
|
"Purchase Receipt Item",
|
||||||
|
filters={"parent": ("in", linked_purchase_receipts)},
|
||||||
|
fields=["name", "provisional_expense_account", "qty", "base_rate"],
|
||||||
|
)
|
||||||
|
default_provisional_account = self.get_company_default("default_provisional_account")
|
||||||
|
for item in pr_items:
|
||||||
|
self.provisional_accounts[item.name] = {
|
||||||
|
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||||
|
"qty": item.qty,
|
||||||
|
"base_rate": item.base_rate,
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_provisional_gl_entry(self, gl_entries, item):
|
||||||
|
if item.purchase_receipt:
|
||||||
|
if not self.provisional_enpenses_booked_in_pr:
|
||||||
|
pr_item = self.provisional_accounts.get(item.pr_detail, {})
|
||||||
|
provisional_account = pr_item.get("provisional_account")
|
||||||
|
pr_qty = pr_item.get("qty")
|
||||||
|
pr_base_rate = pr_item.get("base_rate")
|
||||||
|
|
||||||
|
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
|
||||||
|
provision_gle_against_pr = frappe.db.get_value(
|
||||||
|
"GL Entry",
|
||||||
|
{
|
||||||
|
"is_cancelled": 0,
|
||||||
|
"voucher_type": "Purchase Receipt",
|
||||||
|
"voucher_no": item.purchase_receipt,
|
||||||
|
"voucher_detail_no": item.pr_detail,
|
||||||
|
"account": provisional_account,
|
||||||
|
},
|
||||||
|
["name"],
|
||||||
|
)
|
||||||
|
if provision_gle_against_pr:
|
||||||
|
self.provisional_enpenses_booked_in_pr = True
|
||||||
|
|
||||||
|
if self.provisional_enpenses_booked_in_pr:
|
||||||
|
purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt)
|
||||||
|
|
||||||
|
# Intentionally passing purchase invoice item to handle partial billing
|
||||||
|
purchase_receipt_doc.add_provisional_gl_entry(
|
||||||
|
item,
|
||||||
|
gl_entries,
|
||||||
|
self.posting_date,
|
||||||
|
provisional_account,
|
||||||
|
reverse=1,
|
||||||
|
item_amount=(min(item.qty, pr_qty) * pr_base_rate),
|
||||||
|
)
|
||||||
|
|
||||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||||
assets = frappe.db.get_all(
|
assets = frappe.db.get_all(
|
||||||
"Asset",
|
"Asset",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"is_consolidated",
|
"is_consolidated",
|
||||||
"is_return",
|
"is_return",
|
||||||
"return_against",
|
"return_against",
|
||||||
|
"update_outstanding_for_self",
|
||||||
"update_billed_amount_in_sales_order",
|
"update_billed_amount_in_sales_order",
|
||||||
"update_billed_amount_in_delivery_note",
|
"update_billed_amount_in_delivery_note",
|
||||||
"is_debit_note",
|
"is_debit_note",
|
||||||
@@ -2161,6 +2162,14 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Don't Create Loyalty Points",
|
"label": "Don't Create Loyalty Points",
|
||||||
"no_copy": 1
|
"no_copy": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||||
|
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
|
||||||
|
"fieldname": "update_outstanding_for_self",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Update Outstanding for Self"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
@@ -2173,7 +2182,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-01-02 17:25:46.027523",
|
"modified": "2024-03-11 14:20:34.874192",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -1059,6 +1059,10 @@ class SalesInvoice(SellingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if grand_total and not self.is_internal_transfer():
|
if grand_total and not self.is_internal_transfer():
|
||||||
|
against_voucher = self.name
|
||||||
|
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||||
|
against_voucher = self.return_against
|
||||||
|
|
||||||
# Did not use base_grand_total to book rounding loss gle
|
# Did not use base_grand_total to book rounding loss gle
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
@@ -1072,7 +1076,7 @@ class SalesInvoice(SellingController):
|
|||||||
"debit_in_account_currency": base_grand_total
|
"debit_in_account_currency": base_grand_total
|
||||||
if self.party_account_currency == self.company_currency
|
if self.party_account_currency == self.company_currency
|
||||||
else grand_total,
|
else grand_total,
|
||||||
"against_voucher": self.name,
|
"against_voucher": against_voucher,
|
||||||
"against_voucher_type": self.doctype,
|
"against_voucher_type": self.doctype,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"project": self.project,
|
"project": self.project,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import copy
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.utils import cint, cstr, flt, formatdate, getdate, now
|
from frappe.utils import cint, flt, formatdate, getdate, now
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -234,11 +234,13 @@ def get_cost_center_allocation_data(company, posting_date):
|
|||||||
def merge_similar_entries(gl_map, precision=None):
|
def merge_similar_entries(gl_map, precision=None):
|
||||||
merged_gl_map = []
|
merged_gl_map = []
|
||||||
accounting_dimensions = get_accounting_dimensions()
|
accounting_dimensions = get_accounting_dimensions()
|
||||||
|
merge_properties = get_merge_properties(accounting_dimensions)
|
||||||
|
|
||||||
for entry in gl_map:
|
for entry in gl_map:
|
||||||
|
entry.merge_key = get_merge_key(entry, merge_properties)
|
||||||
# if there is already an entry in this account then just add it
|
# if there is already an entry in this account then just add it
|
||||||
# to that entry
|
# to that entry
|
||||||
same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions)
|
same_head = check_if_in_list(entry, merged_gl_map)
|
||||||
if same_head:
|
if same_head:
|
||||||
same_head.debit = flt(same_head.debit) + flt(entry.debit)
|
same_head.debit = flt(same_head.debit) + flt(entry.debit)
|
||||||
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
||||||
@@ -273,33 +275,34 @@ def merge_similar_entries(gl_map, precision=None):
|
|||||||
return merged_gl_map
|
return merged_gl_map
|
||||||
|
|
||||||
|
|
||||||
def check_if_in_list(gle, gl_map, dimensions=None):
|
def get_merge_properties(dimensions=None):
|
||||||
account_head_fieldnames = [
|
merge_properties = [
|
||||||
"voucher_detail_no",
|
"account",
|
||||||
"party",
|
|
||||||
"against_voucher",
|
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"against_voucher_type",
|
"party",
|
||||||
"party_type",
|
"party_type",
|
||||||
|
"voucher_detail_no",
|
||||||
|
"against_voucher",
|
||||||
|
"against_voucher_type",
|
||||||
"project",
|
"project",
|
||||||
"finance_book",
|
"finance_book",
|
||||||
]
|
]
|
||||||
|
|
||||||
if dimensions:
|
if dimensions:
|
||||||
account_head_fieldnames = account_head_fieldnames + dimensions
|
merge_properties.extend(dimensions)
|
||||||
|
return merge_properties
|
||||||
|
|
||||||
|
|
||||||
|
def get_merge_key(entry, merge_properties):
|
||||||
|
merge_key = []
|
||||||
|
for fieldname in merge_properties:
|
||||||
|
merge_key.append(entry.get(fieldname, ""))
|
||||||
|
|
||||||
|
return tuple(merge_key)
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_in_list(gle, gl_map):
|
||||||
for e in gl_map:
|
for e in gl_map:
|
||||||
same_head = True
|
if e.merge_key == gle.merge_key:
|
||||||
if e.account != gle.account:
|
|
||||||
same_head = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
for fieldname in account_head_fieldnames:
|
|
||||||
if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
|
|
||||||
same_head = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if same_head:
|
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -692,7 +692,12 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def get_return_entries(self):
|
def get_return_entries(self):
|
||||||
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
|
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
|
||||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
filters = {
|
||||||
|
"is_return": 1,
|
||||||
|
"docstatus": 1,
|
||||||
|
"company": self.filters.company,
|
||||||
|
"update_outstanding_for_self": 0,
|
||||||
|
}
|
||||||
or_filters = {}
|
or_filters = {}
|
||||||
for party_type in self.party_type:
|
for party_type in self.party_type:
|
||||||
party_field = scrub(party_type)
|
party_field = scrub(party_type)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
pe.submit()
|
||||||
|
|
||||||
def create_credit_note(self, docname):
|
def create_credit_note(self, docname, do_not_submit=False):
|
||||||
credit_note = create_sales_invoice(
|
credit_note = create_sales_invoice(
|
||||||
company=self.company,
|
company=self.company,
|
||||||
customer=self.customer,
|
customer=self.customer,
|
||||||
@@ -72,6 +72,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
cost_center=self.cost_center,
|
cost_center=self.cost_center,
|
||||||
is_return=1,
|
is_return=1,
|
||||||
return_against=docname,
|
return_against=docname,
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
)
|
)
|
||||||
|
|
||||||
return credit_note
|
return credit_note
|
||||||
@@ -149,7 +150,9 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||||
self.create_credit_note(si.name)
|
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||||
|
cr_note.update_outstanding_for_self = False
|
||||||
|
cr_note.save().submit()
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||||
@@ -167,6 +170,82 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_cr_note_flag_to_update_self(self):
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
"show_remarks": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||||
|
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||||
|
si.set_posting_time = True
|
||||||
|
si.posting_date = add_days(today(), -1)
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [100, 100, "No Remarks"]
|
||||||
|
|
||||||
|
self.assertEqual(len(report[1]), 1)
|
||||||
|
row = report[1][0]
|
||||||
|
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||||
|
|
||||||
|
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||||
|
self.create_payment_entry(si.name)
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data_after_payment = [100, 100, 40, 60]
|
||||||
|
self.assertEqual(len(report[1]), 1)
|
||||||
|
row = report[1][0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data_after_payment,
|
||||||
|
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||||
|
)
|
||||||
|
|
||||||
|
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||||
|
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||||
|
cr_note.update_outstanding_for_self = True
|
||||||
|
cr_note.save().submit()
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data_after_credit_note = [
|
||||||
|
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
|
||||||
|
[0, 0, 100.0, 0.0, -100.0, cr_note.name],
|
||||||
|
]
|
||||||
|
self.assertEqual(len(report[1]), 2)
|
||||||
|
si_row = [
|
||||||
|
[
|
||||||
|
row.invoice_grand_total,
|
||||||
|
row.invoiced,
|
||||||
|
row.paid,
|
||||||
|
row.credit_note,
|
||||||
|
row.outstanding,
|
||||||
|
row.voucher_no,
|
||||||
|
]
|
||||||
|
for row in report[1]
|
||||||
|
if row.voucher_no == si.name
|
||||||
|
][0]
|
||||||
|
|
||||||
|
cr_note_row = [
|
||||||
|
[
|
||||||
|
row.invoice_grand_total,
|
||||||
|
row.invoiced,
|
||||||
|
row.paid,
|
||||||
|
row.credit_note,
|
||||||
|
row.outstanding,
|
||||||
|
row.voucher_no,
|
||||||
|
]
|
||||||
|
for row in report[1]
|
||||||
|
if row.voucher_no == cr_note.name
|
||||||
|
][0]
|
||||||
|
self.assertEqual(expected_data_after_credit_note[0], si_row)
|
||||||
|
self.assertEqual(expected_data_after_credit_note[1], cr_note_row)
|
||||||
|
|
||||||
def test_payment_againt_po_in_receivable_report(self):
|
def test_payment_againt_po_in_receivable_report(self):
|
||||||
"""
|
"""
|
||||||
Payments made against Purchase Order will show up as outstanding amount
|
Payments made against Purchase Order will show up as outstanding amount
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class Asset(AccountsController):
|
|||||||
self.validate_in_use_date()
|
self.validate_in_use_date()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.make_asset_movement()
|
self.make_asset_movement()
|
||||||
|
self.reload()
|
||||||
if not self.booked_fixed_asset and self.validate_make_gl_entry():
|
if not self.booked_fixed_asset and self.validate_make_gl_entry():
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ class Asset(AccountsController):
|
|||||||
self.validate_cancellation()
|
self.validate_cancellation()
|
||||||
self.cancel_movement_entries()
|
self.cancel_movement_entries()
|
||||||
self.cancel_capitalization()
|
self.cancel_capitalization()
|
||||||
|
self.reload()
|
||||||
self.delete_depreciation_entries()
|
self.delete_depreciation_entries()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||||
@@ -428,7 +430,7 @@ class Asset(AccountsController):
|
|||||||
schedule_date = get_last_day(schedule_date)
|
schedule_date = get_last_day(schedule_date)
|
||||||
|
|
||||||
# if asset is being sold
|
# if asset is being sold
|
||||||
if date_of_disposal:
|
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
|
||||||
from_date = self.get_from_date_for_disposal(finance_book)
|
from_date = self.get_from_date_for_disposal(finance_book)
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||||
finance_book,
|
finance_book,
|
||||||
@@ -1009,7 +1011,9 @@ class Asset(AccountsController):
|
|||||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()
|
purchase_document
|
||||||
|
and self.purchase_receipt_amount
|
||||||
|
and getdate(self.available_for_use_date) <= getdate()
|
||||||
):
|
):
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
|
|||||||
@@ -210,9 +210,7 @@ def make_depreciation_entry(
|
|||||||
debit_account,
|
debit_account,
|
||||||
accounting_dimensions,
|
accounting_dimensions,
|
||||||
)
|
)
|
||||||
frappe.db.commit()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
|
||||||
depreciation_posting_error = e
|
depreciation_posting_error = e
|
||||||
|
|
||||||
asset.set_status()
|
asset.set_status()
|
||||||
@@ -474,6 +472,7 @@ def depreciate_asset(asset, date):
|
|||||||
|
|
||||||
make_depreciation_entry(asset.name, date)
|
make_depreciation_entry(asset.name, date)
|
||||||
|
|
||||||
|
asset.reload()
|
||||||
cancel_depreciation_entries(asset, date)
|
cancel_depreciation_entries(asset, date)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ class AssetCapitalization(StockController):
|
|||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.restore_consumed_asset_items()
|
self.restore_consumed_asset_items()
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||||
|
super(AssetCapitalization, self).on_trash()
|
||||||
|
|
||||||
def cancel_target_asset(self):
|
def cancel_target_asset(self):
|
||||||
if self.entry_type == "Capitalization" and self.target_asset:
|
if self.entry_type == "Capitalization" and self.target_asset:
|
||||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ force_item_fields = (
|
|||||||
"weight_per_unit",
|
"weight_per_unit",
|
||||||
"weight_uom",
|
"weight_uom",
|
||||||
"total_weight",
|
"total_weight",
|
||||||
|
"valuation_rate",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -203,17 +204,18 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
|
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
|
||||||
# if self.get("is_return") and self.get("return_against"):
|
if self.get("update_outstanding_for_self"):
|
||||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_(
|
_(
|
||||||
"{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}."
|
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
|
||||||
).format(
|
).format(
|
||||||
document_type,
|
frappe.bold(document_type),
|
||||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
get_link_to_form(self.doctype, self.get("return_against")),
|
||||||
get_link_to_form(self.doctype, self.get("return_against")),
|
frappe.bold("Update Outstanding for Self"),
|
||||||
|
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||||
@@ -1605,8 +1607,8 @@ class AccountsController(TransactionBase):
|
|||||||
item_allowance = {}
|
item_allowance = {}
|
||||||
global_qty_allowance, global_amount_allowance = None, None
|
global_qty_allowance, global_amount_allowance = None, None
|
||||||
|
|
||||||
role_allowed_to_over_bill = frappe.db.get_single_value(
|
role_allowed_to_over_bill = frappe.get_cached_value(
|
||||||
"Accounts Settings", "role_allowed_to_over_bill"
|
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||||
)
|
)
|
||||||
user_roles = frappe.get_roles()
|
user_roles = frappe.get_roles()
|
||||||
|
|
||||||
|
|||||||
@@ -573,6 +573,7 @@ class StatusUpdater(Document):
|
|||||||
ref_doc.set_status(update=True)
|
ref_doc.set_status(update=True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.request_cache
|
||||||
def get_allowance_for(
|
def get_allowance_for(
|
||||||
item_code,
|
item_code,
|
||||||
item_allowance=None,
|
item_allowance=None,
|
||||||
@@ -602,20 +603,20 @@ def get_allowance_for(
|
|||||||
global_amount_allowance,
|
global_amount_allowance,
|
||||||
)
|
)
|
||||||
|
|
||||||
qty_allowance, over_billing_allowance = frappe.db.get_value(
|
qty_allowance, over_billing_allowance = frappe.get_cached_value(
|
||||||
"Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"]
|
"Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if qty_or_amount == "qty" and not qty_allowance:
|
if qty_or_amount == "qty" and not qty_allowance:
|
||||||
if global_qty_allowance == None:
|
if global_qty_allowance == None:
|
||||||
global_qty_allowance = flt(
|
global_qty_allowance = flt(
|
||||||
frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")
|
frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance")
|
||||||
)
|
)
|
||||||
qty_allowance = global_qty_allowance
|
qty_allowance = global_qty_allowance
|
||||||
elif qty_or_amount == "amount" and not over_billing_allowance:
|
elif qty_or_amount == "amount" and not over_billing_allowance:
|
||||||
if global_amount_allowance == None:
|
if global_amount_allowance == None:
|
||||||
global_amount_allowance = flt(
|
global_amount_allowance = flt(
|
||||||
frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
frappe.get_cached_value("Accounts Settings", None, "over_billing_allowance")
|
||||||
)
|
)
|
||||||
over_billing_allowance = global_amount_allowance
|
over_billing_allowance = global_amount_allowance
|
||||||
|
|
||||||
|
|||||||
@@ -354,9 +354,11 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency"
|
|||||||
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
||||||
erpnext.patches.v14_0.update_total_asset_cost_field
|
erpnext.patches.v14_0.update_total_asset_cost_field
|
||||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||||
|
erpnext.patches.v14_0.update_flag_for_return_invoices
|
||||||
# below migration patch should always run last
|
# below migration patch should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||||
execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1)
|
execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1)
|
||||||
erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
|
erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
|
||||||
|
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
cancelled_asset_capitalizations = frappe.get_all(
|
||||||
|
"Asset Capitalization",
|
||||||
|
filters={"docstatus": 2},
|
||||||
|
fields=["name", "target_asset"],
|
||||||
|
)
|
||||||
|
for asset_capitalization in cancelled_asset_capitalizations:
|
||||||
|
frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None)
|
||||||
62
erpnext/patches/v14_0/update_flag_for_return_invoices.py
Normal file
62
erpnext/patches/v14_0/update_flag_for_return_invoices.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from frappe import qb
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
# Set "update_outstanding_for_self" flag in Credit/Debit Notes
|
||||||
|
# Fetch Credit/Debit notes that does have 'return_against' but still post ledger entries against themselves.
|
||||||
|
|
||||||
|
gle = qb.DocType("GL Entry")
|
||||||
|
|
||||||
|
# Use hardcoded 'creation' date to isolate Credit/Debit notes created post v14 backport
|
||||||
|
# https://github.com/frappe/erpnext/pull/39497
|
||||||
|
creation_date = "2024-01-25"
|
||||||
|
|
||||||
|
si = qb.DocType("Sales Invoice")
|
||||||
|
if cr_notes := (
|
||||||
|
qb.from_(si)
|
||||||
|
.select(si.name)
|
||||||
|
.where(
|
||||||
|
(si.creation.gte(creation_date))
|
||||||
|
& (si.docstatus == 1)
|
||||||
|
& (si.is_return == True)
|
||||||
|
& (si.return_against.notnull())
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
):
|
||||||
|
cr_notes = [x[0] for x in cr_notes]
|
||||||
|
if docs_that_require_update := (
|
||||||
|
qb.from_(gle)
|
||||||
|
.select(gle.voucher_no)
|
||||||
|
.distinct()
|
||||||
|
.where((gle.voucher_no.isin(cr_notes)) & (gle.voucher_no == gle.against_voucher))
|
||||||
|
.run()
|
||||||
|
):
|
||||||
|
docs_that_require_update = [x[0] for x in docs_that_require_update]
|
||||||
|
qb.update(si).set(si.update_outstanding_for_self, True).where(
|
||||||
|
si.name.isin(docs_that_require_update)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
pi = qb.DocType("Purchase Invoice")
|
||||||
|
if dr_notes := (
|
||||||
|
qb.from_(pi)
|
||||||
|
.select(pi.name)
|
||||||
|
.where(
|
||||||
|
(pi.creation.gte(creation_date))
|
||||||
|
& (pi.docstatus == 1)
|
||||||
|
& (pi.is_return == True)
|
||||||
|
& (pi.return_against.notnull())
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
):
|
||||||
|
dr_notes = [x[0] for x in dr_notes]
|
||||||
|
if docs_that_require_update := (
|
||||||
|
qb.from_(gle)
|
||||||
|
.select(gle.voucher_no)
|
||||||
|
.distinct()
|
||||||
|
.where((gle.voucher_no.isin(dr_notes)) & (gle.voucher_no == gle.against_voucher))
|
||||||
|
.run()
|
||||||
|
):
|
||||||
|
docs_that_require_update = [x[0] for x in docs_that_require_update]
|
||||||
|
qb.update(pi).set(pi.update_outstanding_for_self, True).where(
|
||||||
|
pi.name.isin(docs_that_require_update)
|
||||||
|
).run()
|
||||||
@@ -30,6 +30,39 @@ class TestQuotation(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertTrue(sales_order.get("payment_schedule"))
|
self.assertTrue(sales_order.get("payment_schedule"))
|
||||||
|
|
||||||
|
def test_gross_profit(self):
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
from erpnext.stock.get_item_details import insert_item_price
|
||||||
|
|
||||||
|
item_doc = make_item("_Test Item for Gross Profit", {"is_stock_item": 1})
|
||||||
|
item_code = item_doc.name
|
||||||
|
make_stock_entry(item_code=item_code, qty=10, rate=100, target="_Test Warehouse - _TC")
|
||||||
|
|
||||||
|
selling_price_list = frappe.get_all("Price List", filters={"selling": 1}, limit=1)[0].name
|
||||||
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
|
||||||
|
insert_item_price(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": item_code,
|
||||||
|
"price_list": selling_price_list,
|
||||||
|
"price_list_rate": 300,
|
||||||
|
"rate": 300,
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"discount_amount": 0.0,
|
||||||
|
"currency": frappe.db.get_value("Price List", selling_price_list, "currency"),
|
||||||
|
"uom": item_doc.stock_uom,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
quotation = make_quotation(
|
||||||
|
item_code=item_code, qty=1, rate=300, selling_price_list=selling_price_list
|
||||||
|
)
|
||||||
|
self.assertEqual(quotation.items[0].valuation_rate, 100)
|
||||||
|
self.assertEqual(quotation.items[0].gross_profit, 200)
|
||||||
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||||
|
|
||||||
def test_maintain_rate_in_sales_cycle_is_enforced(self):
|
def test_maintain_rate_in_sales_cycle_is_enforced(self):
|
||||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,8 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Price List",
|
"label": "Price List",
|
||||||
"options": "Price List",
|
"options": "Price List",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bold": 1,
|
"bold": 1,
|
||||||
@@ -220,7 +221,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-15 08:26:04.041861",
|
"modified": "2024-03-13 12:23:39.630290",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item Price",
|
"name": "Item Price",
|
||||||
|
|||||||
@@ -104,22 +104,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
|||||||
if args.customer and cint(args.is_pos):
|
if args.customer and cint(args.is_pos):
|
||||||
out.update(get_pos_profile_item_details(args.company, args, update_data=True))
|
out.update(get_pos_profile_item_details(args.company, args, update_data=True))
|
||||||
|
|
||||||
if (
|
if item.is_stock_item:
|
||||||
args.get("doctype") == "Material Request"
|
update_bin_details(args, out, doc)
|
||||||
and args.get("material_request_type") == "Material Transfer"
|
|
||||||
):
|
|
||||||
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
|
|
||||||
|
|
||||||
elif out.get("warehouse"):
|
|
||||||
if doc and doc.get("doctype") == "Purchase Order":
|
|
||||||
# calculate company_total_stock only for po
|
|
||||||
bin_details = get_bin_details(
|
|
||||||
args.item_code, out.warehouse, args.company, include_child_warehouses=True
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True)
|
|
||||||
|
|
||||||
out.update(bin_details)
|
|
||||||
|
|
||||||
# update args with out, if key or value not exists
|
# update args with out, if key or value not exists
|
||||||
for key, value in out.items():
|
for key, value in out.items():
|
||||||
@@ -202,6 +188,24 @@ def set_valuation_rate(out, args):
|
|||||||
out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse")))
|
out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse")))
|
||||||
|
|
||||||
|
|
||||||
|
def update_bin_details(args, out, doc):
|
||||||
|
if (
|
||||||
|
args.get("doctype") == "Material Request"
|
||||||
|
and args.get("material_request_type") == "Material Transfer"
|
||||||
|
):
|
||||||
|
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
|
||||||
|
|
||||||
|
elif out.get("warehouse"):
|
||||||
|
company = args.company if (doc and doc.get("doctype") == "Purchase Order") else None
|
||||||
|
|
||||||
|
# calculate company_total_stock only for po
|
||||||
|
bin_details = get_bin_details(
|
||||||
|
args.item_code, out.warehouse, company, include_child_warehouses=True
|
||||||
|
)
|
||||||
|
|
||||||
|
out.update(bin_details)
|
||||||
|
|
||||||
|
|
||||||
def process_args(args):
|
def process_args(args):
|
||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|||||||
Reference in New Issue
Block a user