mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-15 19:19:17 +00:00
Merge pull request #42142 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -121,7 +121,8 @@
|
|||||||
"label": "Account Type",
|
"label": "Account Type",
|
||||||
"oldfieldname": "account_type",
|
"oldfieldname": "account_type",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Rate at which this tax is applied",
|
"description": "Rate at which this tax is applied",
|
||||||
@@ -190,7 +191,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-20 18:18:44.405723",
|
"modified": "2024-06-27 16:23:04.444354",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Account",
|
"name": "Account",
|
||||||
@@ -251,4 +252,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,14 +255,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():
|
||||||
|
|||||||
@@ -66,37 +66,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,
|
p.allow_or_restrict, a.is_mandatory
|
||||||
`tabAccounting Dimension Filter` p
|
FROM
|
||||||
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
|
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
||||||
WHERE
|
`tabAccounting Dimension Filter` p
|
||||||
p.name = a.parent
|
WHERE
|
||||||
AND p.disabled = 0
|
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()
|
||||||
|
|||||||
@@ -142,6 +142,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]
|
||||||
@@ -149,6 +151,9 @@ 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")
|
||||||
|
|
||||||
@@ -175,12 +180,12 @@ 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 = f"""and exists(select name from `tab{doctype}`
|
condition = f"""and exists(select name from `tab{doctype}`
|
||||||
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
||||||
args.is_tree = True
|
args.is_tree = True
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,8 @@
|
|||||||
"fieldname": "voucher_detail_no",
|
"fieldname": "voucher_detail_no",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Voucher Detail No",
|
"label": "Voucher Detail No",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
@@ -290,7 +291,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-18 15:38:14.006208",
|
"modified": "2024-07-02 14:31:51.496466",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "GL Entry",
|
"name": "GL Entry",
|
||||||
@@ -322,7 +323,7 @@
|
|||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||||
"sort_field": "modified",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,6 @@ class GLEntry(Document):
|
|||||||
account: DF.Link | None
|
account: DF.Link | None
|
||||||
account_currency: DF.Link | None
|
account_currency: DF.Link | None
|
||||||
against: DF.Text | None
|
against: DF.Text | None
|
||||||
against_link: DF.DynamicLink | None
|
|
||||||
against_type: DF.Link | None
|
|
||||||
against_voucher: DF.DynamicLink | None
|
against_voucher: DF.DynamicLink | None
|
||||||
against_voucher_type: DF.Link | None
|
against_voucher_type: DF.Link | None
|
||||||
company: DF.Link | None
|
company: DF.Link | None
|
||||||
@@ -328,7 +326,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 = f"and account in ({frappe.db.escape(account)}, {frappe.db.escape(party_account)})"
|
account_condition = f"and account in ({frappe.db.escape(account)}, {frappe.db.escape(party_account)})"
|
||||||
else:
|
else:
|
||||||
account_condition = f" and account = {frappe.db.escape(account)}"
|
account_condition = f" and account = {frappe.db.escape(account)}"
|
||||||
@@ -392,7 +390,9 @@ 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("Accounts Settings", None, "frozen_accounts_modifier")
|
frozen_accounts_modifier = frappe.get_cached_value(
|
||||||
|
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||||
|
)
|
||||||
|
|
||||||
if not frozen_accounts_modifier:
|
if not frozen_accounts_modifier:
|
||||||
frappe.throw(_("Account {0} is frozen").format(account))
|
frappe.throw(_("Account {0} is frozen").format(account))
|
||||||
|
|||||||
@@ -1217,13 +1217,21 @@ class PaymentEntry(AccountsController):
|
|||||||
if reference.reference_doctype == "Sales Invoice":
|
if reference.reference_doctype == "Sales Invoice":
|
||||||
return "credit", reference.account
|
return "credit", reference.account
|
||||||
|
|
||||||
|
if reference.reference_doctype == "Purchase Invoice":
|
||||||
|
return "debit", reference.account
|
||||||
|
|
||||||
if reference.reference_doctype == "Payment Entry":
|
if reference.reference_doctype == "Payment Entry":
|
||||||
|
# reference.account_type and reference.payment_type is only available for Reverse payments
|
||||||
if reference.account_type == "Receivable" and reference.payment_type == "Pay":
|
if reference.account_type == "Receivable" and reference.payment_type == "Pay":
|
||||||
return "credit", self.party_account
|
return "credit", self.party_account
|
||||||
else:
|
else:
|
||||||
return "debit", self.party_account
|
return "debit", self.party_account
|
||||||
|
|
||||||
return "debit", reference.account
|
if reference.reference_doctype == "Journal Entry":
|
||||||
|
if self.party_type == "Customer" and self.payment_type == "Receive":
|
||||||
|
return "credit", reference.account
|
||||||
|
else:
|
||||||
|
return "debit", reference.account
|
||||||
|
|
||||||
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
||||||
args_dict = {
|
args_dict = {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
|
|
||||||
expected_gle = dict(
|
expected_gle = dict(
|
||||||
(d[0], d)
|
(d[0], d)
|
||||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]]
|
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.validate_gl_entries(pe.name, expected_gle)
|
self.validate_gl_entries(pe.name, expected_gle)
|
||||||
|
|||||||
@@ -161,11 +161,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 (
|
||||||
|
|||||||
@@ -509,7 +509,11 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
|||||||
|
|
||||||
@change_settings(
|
@change_settings(
|
||||||
"Accounts Settings",
|
"Accounts Settings",
|
||||||
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
|
{
|
||||||
|
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||||
|
"delete_linked_ledger_entries": 1,
|
||||||
|
"unlink_advance_payment_on_cancelation_of_order": 1,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
def test_advance_payment_unlink_on_order_cancellation(self):
|
def test_advance_payment_unlink_on_order_cancellation(self):
|
||||||
transaction_date = nowdate()
|
transaction_date = nowdate()
|
||||||
|
|||||||
@@ -109,6 +109,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
"account_currency": "INR",
|
"account_currency": "INR",
|
||||||
"account_type": "Payable",
|
"account_type": "Payable",
|
||||||
},
|
},
|
||||||
|
# 'Receivable' account for capturing advance received, under 'Liabilities' group
|
||||||
|
{
|
||||||
|
"attribute": "advance_receivable_account",
|
||||||
|
"account_name": "Advance Received",
|
||||||
|
"parent_account": "Current Liabilities - _PR",
|
||||||
|
"account_currency": "INR",
|
||||||
|
"account_type": "Receivable",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for x in accounts:
|
for x in accounts:
|
||||||
@@ -1574,6 +1582,229 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(len(pl_entries), 3)
|
self.assertEqual(len(pl_entries), 3)
|
||||||
|
|
||||||
|
def test_advance_payment_reconciliation_against_journal_for_customer(self):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
self.company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 1,
|
||||||
|
"default_advance_received_account": self.advance_receivable_account,
|
||||||
|
"reconcile_on_advance_payment_date": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
amount = 200.0
|
||||||
|
je = self.create_journal_entry(self.debit_to, self.bank, amount)
|
||||||
|
je.accounts[0].cost_center = self.main_cc.name
|
||||||
|
je.accounts[0].party_type = "Customer"
|
||||||
|
je.accounts[0].party = self.customer
|
||||||
|
je.accounts[1].cost_center = self.main_cc.name
|
||||||
|
je = je.save().submit()
|
||||||
|
|
||||||
|
pe = self.create_payment_entry(amount=amount).save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.default_advance_account = self.advance_receivable_account
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||||
|
payments = [payment.as_dict() for payment in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# Assert Ledger Entries
|
||||||
|
gl_entries = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(gl_entries), 4)
|
||||||
|
pl_entries = frappe.db.get_all(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"voucher_no": pe.name, "delinked": 0},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(pl_entries), 3)
|
||||||
|
|
||||||
|
gl_entries = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||||
|
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||||
|
order_by="account, against_voucher, debit",
|
||||||
|
)
|
||||||
|
expected_gle = [
|
||||||
|
{
|
||||||
|
"account": self.advance_receivable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": pe.name,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_receivable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": pe.name,
|
||||||
|
"debit": amount,
|
||||||
|
"credit": 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.debit_to,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": je.name,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.bank,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": None,
|
||||||
|
"debit": amount,
|
||||||
|
"credit": 0.0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(gl_entries, expected_gle)
|
||||||
|
|
||||||
|
pl_entries = frappe.db.get_all(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"voucher_no": pe.name},
|
||||||
|
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||||
|
order_by="account, against_voucher_no, amount",
|
||||||
|
)
|
||||||
|
expected_ple = [
|
||||||
|
{
|
||||||
|
"account": self.advance_receivable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": pe.name,
|
||||||
|
"amount": -amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_receivable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": pe.name,
|
||||||
|
"amount": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.debit_to,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": je.name,
|
||||||
|
"amount": -amount,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(pl_entries, expected_ple)
|
||||||
|
|
||||||
|
def test_advance_payment_reconciliation_against_journal_for_supplier(self):
|
||||||
|
self.supplier = make_supplier("_Test Supplier")
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
self.company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 1,
|
||||||
|
"default_advance_paid_account": self.advance_payable_account,
|
||||||
|
"reconcile_on_advance_payment_date": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
amount = 200.0
|
||||||
|
je = self.create_journal_entry(self.creditors, self.bank, -amount)
|
||||||
|
je.accounts[0].cost_center = self.main_cc.name
|
||||||
|
je.accounts[0].party_type = "Supplier"
|
||||||
|
je.accounts[0].party = self.supplier
|
||||||
|
je.accounts[1].cost_center = self.main_cc.name
|
||||||
|
je = je.save().submit()
|
||||||
|
|
||||||
|
pe = self.create_payment_entry(amount=amount)
|
||||||
|
pe.payment_type = "Pay"
|
||||||
|
pe.party_type = "Supplier"
|
||||||
|
pe.paid_from = self.bank
|
||||||
|
pe.paid_to = self.creditors
|
||||||
|
pe.party = self.supplier
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||||
|
pr.default_advance_account = self.advance_payable_account
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||||
|
payments = [payment.as_dict() for payment in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# Assert Ledger Entries
|
||||||
|
gl_entries = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(gl_entries), 4)
|
||||||
|
pl_entries = frappe.db.get_all(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"voucher_no": pe.name, "delinked": 0},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(pl_entries), 3)
|
||||||
|
|
||||||
|
gl_entries = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||||
|
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||||
|
order_by="account, against_voucher, debit",
|
||||||
|
)
|
||||||
|
expected_gle = [
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": pe.name,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": pe.name,
|
||||||
|
"debit": amount,
|
||||||
|
"credit": 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.creditors,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": je.name,
|
||||||
|
"debit": amount,
|
||||||
|
"credit": 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.bank,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": None,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": amount,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(gl_entries, expected_gle)
|
||||||
|
|
||||||
|
pl_entries = frappe.db.get_all(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"voucher_no": pe.name},
|
||||||
|
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||||
|
order_by="account, against_voucher_no, amount",
|
||||||
|
)
|
||||||
|
expected_ple = [
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": pe.name,
|
||||||
|
"amount": -amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": pe.name,
|
||||||
|
"amount": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.creditors,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": je.name,
|
||||||
|
"amount": -amount,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(pl_entries, expected_ple)
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer_name, currency=None):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
|||||||
@@ -549,6 +549,21 @@ class PurchaseInvoice(BuyingController):
|
|||||||
item.expense_account = stock_not_billed_account
|
item.expense_account = stock_not_billed_account
|
||||||
elif item.is_fixed_asset:
|
elif item.is_fixed_asset:
|
||||||
account = None
|
account = None
|
||||||
|
if not item.pr_detail and item.po_detail:
|
||||||
|
receipt_item = frappe.get_cached_value(
|
||||||
|
"Purchase Receipt Item",
|
||||||
|
{
|
||||||
|
"purchase_order": item.purchase_order,
|
||||||
|
"purchase_order_item": item.po_detail,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
["name", "parent"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
if receipt_item:
|
||||||
|
item.pr_detail = receipt_item.name
|
||||||
|
item.purchase_receipt = receipt_item.parent
|
||||||
|
|
||||||
if item.pr_detail:
|
if item.pr_detail:
|
||||||
if not self.asset_received_but_not_billed:
|
if not self.asset_received_but_not_billed:
|
||||||
self.asset_received_but_not_billed = self.get_company_default(
|
self.asset_received_but_not_billed = self.get_company_default(
|
||||||
@@ -795,13 +810,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,
|
||||||
@@ -809,32 +823,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)
|
||||||
@@ -947,8 +972,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if provisional_accounting_for_non_stock_items:
|
||||||
purchase_receipt_doc_map = {}
|
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):
|
||||||
@@ -1087,49 +1112,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, pr_rate = frappe.get_cached_value(
|
|
||||||
"Purchase Receipt Item",
|
|
||||||
item.pr_detail,
|
|
||||||
["provisional_expense_account", "qty", "base_rate", "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_rate)
|
|
||||||
* purchase_receipt_doc.get("conversion_rate")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.is_internal_transfer():
|
if not self.is_internal_transfer():
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
@@ -1225,6 +1208,59 @@ 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")
|
||||||
|
provisional_accounts = set(
|
||||||
|
[
|
||||||
|
d.provisional_expense_account
|
||||||
|
if d.provisional_expense_account
|
||||||
|
else default_provisional_account
|
||||||
|
for d in pr_items
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
provisional_gl_entries = frappe.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={
|
||||||
|
"voucher_type": "Purchase Receipt",
|
||||||
|
"voucher_no": ("in", linked_purchase_receipts),
|
||||||
|
"account": ("in", provisional_accounts),
|
||||||
|
"is_cancelled": 0,
|
||||||
|
},
|
||||||
|
fields=["voucher_detail_no"],
|
||||||
|
)
|
||||||
|
rows_with_provisional_entries = [d.voucher_detail_no for d in provisional_gl_entries]
|
||||||
|
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,
|
||||||
|
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_provisional_gl_entry(self, gl_entries, item):
|
||||||
|
if item.purchase_receipt:
|
||||||
|
pr_item = self.provisional_accounts.get(item.pr_detail, {})
|
||||||
|
if pr_item.get("has_provisional_entry"):
|
||||||
|
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,
|
||||||
|
pr_item.get("provisional_account"),
|
||||||
|
reverse=1,
|
||||||
|
item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("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",
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import erpnext
|
|||||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice as make_pi_from_po
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||||
|
create_pr_against_po,
|
||||||
|
create_purchase_order,
|
||||||
|
)
|
||||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||||
@@ -2185,6 +2189,56 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
|
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
|
||||||
self.assertEqual(row.rejected_serial_no, serial_nos[2])
|
self.assertEqual(row.rejected_serial_no, serial_nos[2])
|
||||||
|
|
||||||
|
def test_make_pr_and_pi_from_po(self):
|
||||||
|
from erpnext.assets.doctype.asset.test_asset import create_asset_category
|
||||||
|
|
||||||
|
if not frappe.db.exists("Asset Category", "Computers"):
|
||||||
|
create_asset_category()
|
||||||
|
|
||||||
|
item = create_item(
|
||||||
|
item_code="_Test_Item", is_stock_item=0, is_fixed_asset=1, asset_category="Computers"
|
||||||
|
)
|
||||||
|
po = create_purchase_order(item_code=item.item_code)
|
||||||
|
pr = create_pr_against_po(po.name, 10)
|
||||||
|
pi = make_pi_from_po(po.name)
|
||||||
|
pi.insert()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pr_gl_entries = frappe.db.sql(
|
||||||
|
"""select account, debit, credit
|
||||||
|
from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no=%s
|
||||||
|
order by account asc""",
|
||||||
|
pr.name,
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr_expected_values = [
|
||||||
|
["Asset Received But Not Billed - _TC", 0, 5000],
|
||||||
|
["CWIP Account - _TC", 5000, 0],
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, gle in enumerate(pr_gl_entries):
|
||||||
|
self.assertEqual(pr_expected_values[i][0], gle.account)
|
||||||
|
self.assertEqual(pr_expected_values[i][1], gle.debit)
|
||||||
|
self.assertEqual(pr_expected_values[i][2], gle.credit)
|
||||||
|
|
||||||
|
pi_gl_entries = frappe.db.sql(
|
||||||
|
"""select account, debit, credit
|
||||||
|
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
|
||||||
|
order by account asc""",
|
||||||
|
pi.name,
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
pi_expected_values = [
|
||||||
|
["Asset Received But Not Billed - _TC", 5000, 0],
|
||||||
|
["Creditors - _TC", 0, 5000],
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, gle in enumerate(pi_gl_entries):
|
||||||
|
self.assertEqual(pi_expected_values[i][0], gle.account)
|
||||||
|
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
||||||
|
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
||||||
|
|
||||||
|
|
||||||
def set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -2202,13 +2202,14 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||||
|
|
||||||
expected_values = [
|
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
expected_values = {
|
||||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
"_Test Account Service Tax - _TC": [0.0, 114.41],
|
||||||
[si.debit_to, 1500, 0.0],
|
"_Test Account VAT - _TC": [0.0, 114.41],
|
||||||
["Round Off - _TC", 0.01, 0.01],
|
si.debit_to: [1500, 0.0],
|
||||||
["Sales - _TC", 0.0, 1271.18],
|
round_off_account: [0.01, 0.01],
|
||||||
]
|
"Sales - _TC": [0.0, 1271.18],
|
||||||
|
}
|
||||||
|
|
||||||
gl_entries = frappe.db.sql(
|
gl_entries = frappe.db.sql(
|
||||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||||
@@ -2219,10 +2220,10 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, gle in enumerate(gl_entries):
|
for gle in gl_entries:
|
||||||
self.assertEqual(expected_values[i][0], gle.account)
|
expected_account_values = expected_values[gle.account]
|
||||||
self.assertEqual(expected_values[i][1], gle.debit)
|
self.assertEqual(expected_account_values[0], gle.debit)
|
||||||
self.assertEqual(expected_values[i][2], gle.credit)
|
self.assertEqual(expected_account_values[1], gle.credit)
|
||||||
|
|
||||||
def test_rounding_adjustment_3(self):
|
def test_rounding_adjustment_3(self):
|
||||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||||
@@ -2270,6 +2271,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||||
|
|
||||||
|
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||||
expected_values = dict(
|
expected_values = dict(
|
||||||
(d[0], d)
|
(d[0], d)
|
||||||
for d in [
|
for d in [
|
||||||
@@ -2277,7 +2279,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||||
["Sales - _TC", 0.0, 4007.15],
|
["Sales - _TC", 0.0, 4007.15],
|
||||||
["Round Off - _TC", 0.02, 0.01],
|
[round_off_account, 0.02, 0.01],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2306,8 +2308,9 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
if round_off_gle:
|
||||||
self.assertEqual(round_off_gle.location, "Block 1")
|
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||||
|
self.assertEqual(round_off_gle.location, "Block 1")
|
||||||
|
|
||||||
disable_dimension()
|
disable_dimension()
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
|||||||
self.assertEqual(len(pe.references), 1)
|
self.assertEqual(len(pe.references), 1)
|
||||||
self.assertEqual(pe.unallocated_amount, 100)
|
self.assertEqual(pe.unallocated_amount, 100)
|
||||||
|
|
||||||
def test_02_unreconcile_one_payment_from_multi_payments(self):
|
def test_02_unreconcile_one_payment_among_multi_payments(self):
|
||||||
"""
|
"""
|
||||||
Scenario: 2 payments, both split against 2 different invoices
|
Scenario: 2 payments, both split against 2 different invoices
|
||||||
Unreconcile only one payment from one invoice
|
Unreconcile only one payment from one invoice
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -228,11 +228,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,34 +275,35 @@ 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",
|
||||||
"voucher_no",
|
"voucher_no",
|
||||||
]
|
]
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import frappe.defaults
|
|||||||
from frappe import _, qb, throw
|
from frappe import _, qb, throw
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder import AliasedQuery, Criterion, Table
|
from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Count, Sum
|
||||||
from frappe.query_builder.utils import DocType
|
from frappe.query_builder.utils import DocType
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
@@ -1492,24 +1492,39 @@ def get_stock_accounts(company, voucher_type=None, voucher_no=None):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
return stock_accounts
|
return list(set(stock_accounts))
|
||||||
|
|
||||||
|
|
||||||
def get_stock_and_account_balance(account=None, posting_date=None, company=None):
|
def get_stock_and_account_balance(account=None, posting_date=None, company=None):
|
||||||
if not posting_date:
|
if not posting_date:
|
||||||
posting_date = nowdate()
|
posting_date = nowdate()
|
||||||
|
|
||||||
warehouse_account = get_warehouse_account_map(company)
|
|
||||||
|
|
||||||
account_balance = get_balance_on(
|
account_balance = get_balance_on(
|
||||||
account, posting_date, in_account_currency=False, ignore_account_permission=True
|
account, posting_date, in_account_currency=False, ignore_account_permission=True
|
||||||
)
|
)
|
||||||
|
|
||||||
related_warehouses = [
|
account_table = frappe.qb.DocType("Account")
|
||||||
wh
|
query = (
|
||||||
for wh, wh_details in warehouse_account.items()
|
frappe.qb.from_(account_table)
|
||||||
if wh_details.account == account and not wh_details.is_group
|
.select(Count(account_table.name))
|
||||||
]
|
.where(
|
||||||
|
(account_table.account_type == "Stock")
|
||||||
|
& (account_table.company == company)
|
||||||
|
& (account_table.is_group == 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
no_of_stock_accounts = cint(query.run()[0][0])
|
||||||
|
|
||||||
|
related_warehouses = []
|
||||||
|
if no_of_stock_accounts > 1:
|
||||||
|
warehouse_account = get_warehouse_account_map(company)
|
||||||
|
|
||||||
|
related_warehouses = [
|
||||||
|
wh
|
||||||
|
for wh, wh_details in warehouse_account.items()
|
||||||
|
if wh_details.account == account and not wh_details.is_group
|
||||||
|
]
|
||||||
|
|
||||||
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
|
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ class AssetMaintenance(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
from erpnext.assets.doctype.asset_maintenance_task.asset_maintenance_task import (
|
from erpnext.assets.doctype.asset_maintenance_task.asset_maintenance_task import AssetMaintenanceTask
|
||||||
AssetMaintenanceTask,
|
|
||||||
)
|
|
||||||
|
|
||||||
asset_category: DF.ReadOnly | None
|
asset_category: DF.ReadOnly | None
|
||||||
asset_maintenance_tasks: DF.Table[AssetMaintenanceTask]
|
asset_maintenance_tasks: DF.Table[AssetMaintenanceTask]
|
||||||
@@ -47,6 +45,11 @@ class AssetMaintenance(Document):
|
|||||||
assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date)
|
assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date)
|
||||||
self.sync_maintenance_tasks()
|
self.sync_maintenance_tasks()
|
||||||
|
|
||||||
|
def after_delete(self):
|
||||||
|
asset = frappe.get_doc("Asset", self.asset_name)
|
||||||
|
if asset.status == "In Maintenance":
|
||||||
|
asset.set_status()
|
||||||
|
|
||||||
def sync_maintenance_tasks(self):
|
def sync_maintenance_tasks(self):
|
||||||
tasks_names = []
|
tasks_names = []
|
||||||
for task in self.get("asset_maintenance_tasks"):
|
for task in self.get("asset_maintenance_tasks"):
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import getdate, nowdate
|
from frappe.query_builder import DocType
|
||||||
|
from frappe.utils import getdate, nowdate, today
|
||||||
|
|
||||||
from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate_next_due_date
|
from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate_next_due_date
|
||||||
|
|
||||||
@@ -75,6 +76,17 @@ class AssetMaintenanceLog(Document):
|
|||||||
asset_maintenance_doc.save()
|
asset_maintenance_doc.save()
|
||||||
|
|
||||||
|
|
||||||
|
def update_asset_maintenance_log_status():
|
||||||
|
AssetMaintenanceLog = DocType("Asset Maintenance Log")
|
||||||
|
(
|
||||||
|
frappe.qb.update(AssetMaintenanceLog)
|
||||||
|
.set(AssetMaintenanceLog.maintenance_status, "Overdue")
|
||||||
|
.where(
|
||||||
|
(AssetMaintenanceLog.maintenance_status == "Planned") & (AssetMaintenanceLog.due_date < today())
|
||||||
|
)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_maintenance_tasks(doctype, txt, searchfield, start, page_len, filters):
|
def get_maintenance_tasks(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ frappe.ui.form.on("Asset Repair", {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
frm.fields_dict.warehouse.get_query = function (doc) {
|
frm.set_query("warehouse", "stock_items", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
is_group: 0,
|
is_group: 0,
|
||||||
company: doc.company,
|
company: frm.doc.company,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
|
|
||||||
frm.set_query("serial_and_batch_bundle", "stock_items", (doc, cdt, cdn) => {
|
frm.set_query("serial_and_batch_bundle", "stock_items", (doc, cdt, cdn) => {
|
||||||
let row = locals[cdt][cdn];
|
let row = locals[cdt][cdn];
|
||||||
@@ -79,7 +79,7 @@ frappe.ui.form.on("Asset Repair", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.repair_status == "Completed") {
|
if (frm.doc.repair_status == "Completed" && !frm.doc.completion_date) {
|
||||||
frm.set_value("completion_date", frappe.datetime.now_datetime());
|
frm.set_value("completion_date", frappe.datetime.now_datetime());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -87,15 +87,48 @@ frappe.ui.form.on("Asset Repair", {
|
|||||||
stock_items_on_form_rendered() {
|
stock_items_on_form_rendered() {
|
||||||
erpnext.setup_serial_or_batch_no();
|
erpnext.setup_serial_or_batch_no();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stock_consumption: function (frm) {
|
||||||
|
if (!frm.doc.stock_consumption) {
|
||||||
|
frm.clear_table("stock_items");
|
||||||
|
frm.refresh_field("stock_items");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
purchase_invoice: function (frm) {
|
||||||
|
if (frm.doc.purchase_invoice) {
|
||||||
|
frappe.call({
|
||||||
|
method: "frappe.client.get_value",
|
||||||
|
args: {
|
||||||
|
doctype: "Purchase Invoice",
|
||||||
|
fieldname: "base_net_total",
|
||||||
|
filters: { name: frm.doc.purchase_invoice },
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
frm.set_value("repair_cost", r.message.base_net_total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frm.set_value("repair_cost", 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Asset Repair Consumed Item", {
|
frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||||
item_code: function (frm, cdt, cdn) {
|
warehouse: function (frm, cdt, cdn) {
|
||||||
var item = locals[cdt][cdn];
|
var item = locals[cdt][cdn];
|
||||||
|
|
||||||
|
if (!item.item_code) {
|
||||||
|
frappe.msgprint(__("Please select an item code before setting the warehouse."));
|
||||||
|
frappe.model.set_value(cdt, cdn, "warehouse", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let item_args = {
|
let item_args = {
|
||||||
item_code: item.item_code,
|
item_code: item.item_code,
|
||||||
warehouse: frm.doc.warehouse,
|
warehouse: item.warehouse,
|
||||||
qty: item.consumed_quantity,
|
qty: item.consumed_quantity,
|
||||||
serial_no: item.serial_no,
|
serial_no: item.serial_no,
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
|
|||||||
@@ -22,16 +22,14 @@
|
|||||||
"column_break_14",
|
"column_break_14",
|
||||||
"project",
|
"project",
|
||||||
"accounting_details",
|
"accounting_details",
|
||||||
"repair_cost",
|
"purchase_invoice",
|
||||||
"capitalize_repair_cost",
|
"capitalize_repair_cost",
|
||||||
"stock_consumption",
|
"stock_consumption",
|
||||||
"column_break_8",
|
"column_break_8",
|
||||||
"purchase_invoice",
|
"repair_cost",
|
||||||
"stock_consumption_details_section",
|
"stock_consumption_details_section",
|
||||||
"warehouse",
|
|
||||||
"stock_items",
|
"stock_items",
|
||||||
"total_repair_cost",
|
"total_repair_cost",
|
||||||
"stock_entry",
|
|
||||||
"asset_depreciation_details_section",
|
"asset_depreciation_details_section",
|
||||||
"increase_in_asset_life",
|
"increase_in_asset_life",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
@@ -122,7 +120,8 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "repair_cost",
|
"fieldname": "repair_cost",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Repair Cost"
|
"label": "Repair Cost",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
@@ -218,13 +217,6 @@
|
|||||||
"label": "Total Repair Cost",
|
"label": "Total Repair Cost",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "stock_consumption",
|
|
||||||
"fieldname": "warehouse",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Warehouse",
|
|
||||||
"options": "Warehouse"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "capitalize_repair_cost",
|
"depends_on": "capitalize_repair_cost",
|
||||||
"fieldname": "asset_depreciation_details_section",
|
"fieldname": "asset_depreciation_details_section",
|
||||||
@@ -251,20 +243,12 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company"
|
"options": "Company"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "stock_entry",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Stock Entry",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "Stock Entry",
|
|
||||||
"read_only": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-08-16 15:55:25.023471",
|
"modified": "2024-06-13 16:14:14.398356",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Repair",
|
"name": "Asset Repair",
|
||||||
|
|||||||
@@ -47,20 +47,25 @@ class AssetRepair(AccountsController):
|
|||||||
repair_cost: DF.Currency
|
repair_cost: DF.Currency
|
||||||
repair_status: DF.Literal["Pending", "Completed", "Cancelled"]
|
repair_status: DF.Literal["Pending", "Completed", "Cancelled"]
|
||||||
stock_consumption: DF.Check
|
stock_consumption: DF.Check
|
||||||
stock_entry: DF.Link | None
|
|
||||||
stock_items: DF.Table[AssetRepairConsumedItem]
|
stock_items: DF.Table[AssetRepairConsumedItem]
|
||||||
total_repair_cost: DF.Currency
|
total_repair_cost: DF.Currency
|
||||||
warehouse: DF.Link | None
|
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||||
|
self.validate_dates()
|
||||||
self.update_status()
|
self.update_status()
|
||||||
|
|
||||||
if self.get("stock_items"):
|
if self.get("stock_items"):
|
||||||
self.set_stock_items_cost()
|
self.set_stock_items_cost()
|
||||||
self.calculate_total_repair_cost()
|
self.calculate_total_repair_cost()
|
||||||
|
|
||||||
|
def validate_dates(self):
|
||||||
|
if self.completion_date and (self.failure_date > self.completion_date):
|
||||||
|
frappe.throw(
|
||||||
|
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
|
||||||
|
)
|
||||||
|
|
||||||
def update_status(self):
|
def update_status(self):
|
||||||
if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
|
if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
|
||||||
frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
|
frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
|
||||||
@@ -105,22 +110,22 @@ class AssetRepair(AccountsController):
|
|||||||
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||||
self.modify_depreciation_schedule()
|
self.modify_depreciation_schedule()
|
||||||
|
|
||||||
notes = _(
|
notes = _(
|
||||||
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
|
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
|
||||||
).format(
|
).format(
|
||||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||||
get_link_to_form(self.doctype, self.name),
|
get_link_to_form(self.doctype, self.name),
|
||||||
)
|
)
|
||||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||||
self.asset_doc.save()
|
self.asset_doc.save()
|
||||||
|
|
||||||
add_asset_activity(
|
add_asset_activity(
|
||||||
self.asset,
|
self.asset,
|
||||||
_("Asset updated after completion of Asset Repair {0}").format(
|
_("Asset updated after completion of Asset Repair {0}").format(
|
||||||
get_link_to_form("Asset Repair", self.name)
|
get_link_to_form("Asset Repair", self.name)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def before_cancel(self):
|
def before_cancel(self):
|
||||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||||
@@ -136,29 +141,28 @@ class AssetRepair(AccountsController):
|
|||||||
self.asset_doc.total_asset_cost -= self.repair_cost
|
self.asset_doc.total_asset_cost -= self.repair_cost
|
||||||
self.asset_doc.additional_asset_cost -= self.repair_cost
|
self.asset_doc.additional_asset_cost -= self.repair_cost
|
||||||
|
|
||||||
if self.get("stock_consumption"):
|
|
||||||
self.increase_stock_quantity()
|
|
||||||
if self.get("capitalize_repair_cost"):
|
if self.get("capitalize_repair_cost"):
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||||
self.make_gl_entries(cancel=True)
|
self.make_gl_entries(cancel=True)
|
||||||
self.db_set("stock_entry", None)
|
|
||||||
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||||
self.revert_depreciation_schedule_on_cancellation()
|
self.revert_depreciation_schedule_on_cancellation()
|
||||||
|
|
||||||
notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format(
|
notes = _(
|
||||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
"This schedule was created when Asset {0}'s Asset Repair {1} was cancelled."
|
||||||
get_link_to_form(self.doctype, self.name),
|
).format(
|
||||||
)
|
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
get_link_to_form(self.doctype, self.name),
|
||||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
)
|
||||||
self.asset_doc.save()
|
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||||
|
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||||
|
self.asset_doc.save()
|
||||||
|
|
||||||
add_asset_activity(
|
add_asset_activity(
|
||||||
self.asset,
|
self.asset,
|
||||||
_("Asset updated after cancellation of Asset Repair {0}").format(
|
_("Asset updated after cancellation of Asset Repair {0}").format(
|
||||||
get_link_to_form("Asset Repair", self.name)
|
get_link_to_form("Asset Repair", self.name)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def after_delete(self):
|
def after_delete(self):
|
||||||
frappe.get_doc("Asset", self.asset).set_status()
|
frappe.get_doc("Asset", self.asset).set_status()
|
||||||
@@ -170,11 +174,6 @@ class AssetRepair(AccountsController):
|
|||||||
def check_for_stock_items_and_warehouse(self):
|
def check_for_stock_items_and_warehouse(self):
|
||||||
if not self.get("stock_items"):
|
if not self.get("stock_items"):
|
||||||
frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
|
frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
|
||||||
if not self.warehouse:
|
|
||||||
frappe.throw(
|
|
||||||
_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."),
|
|
||||||
title=_("Missing Warehouse"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def increase_asset_value(self):
|
def increase_asset_value(self):
|
||||||
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
|
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
|
||||||
@@ -208,6 +207,7 @@ class AssetRepair(AccountsController):
|
|||||||
stock_entry = frappe.get_doc(
|
stock_entry = frappe.get_doc(
|
||||||
{"doctype": "Stock Entry", "stock_entry_type": "Material Issue", "company": self.company}
|
{"doctype": "Stock Entry", "stock_entry_type": "Material Issue", "company": self.company}
|
||||||
)
|
)
|
||||||
|
stock_entry.asset_repair = self.name
|
||||||
|
|
||||||
for stock_item in self.get("stock_items"):
|
for stock_item in self.get("stock_items"):
|
||||||
self.validate_serial_no(stock_item)
|
self.validate_serial_no(stock_item)
|
||||||
@@ -215,7 +215,7 @@ class AssetRepair(AccountsController):
|
|||||||
stock_entry.append(
|
stock_entry.append(
|
||||||
"items",
|
"items",
|
||||||
{
|
{
|
||||||
"s_warehouse": self.warehouse,
|
"s_warehouse": stock_item.warehouse,
|
||||||
"item_code": stock_item.item_code,
|
"item_code": stock_item.item_code,
|
||||||
"qty": stock_item.consumed_quantity,
|
"qty": stock_item.consumed_quantity,
|
||||||
"basic_rate": stock_item.valuation_rate,
|
"basic_rate": stock_item.valuation_rate,
|
||||||
@@ -228,8 +228,6 @@ class AssetRepair(AccountsController):
|
|||||||
stock_entry.insert()
|
stock_entry.insert()
|
||||||
stock_entry.submit()
|
stock_entry.submit()
|
||||||
|
|
||||||
self.db_set("stock_entry", stock_entry.name)
|
|
||||||
|
|
||||||
def validate_serial_no(self, stock_item):
|
def validate_serial_no(self, stock_item):
|
||||||
if not stock_item.serial_and_batch_bundle and frappe.get_cached_value(
|
if not stock_item.serial_and_batch_bundle and frappe.get_cached_value(
|
||||||
"Item", stock_item.item_code, "has_serial_no"
|
"Item", stock_item.item_code, "has_serial_no"
|
||||||
@@ -247,12 +245,6 @@ class AssetRepair(AccountsController):
|
|||||||
"Serial and Batch Bundle", stock_item.serial_and_batch_bundle, values_to_update
|
"Serial and Batch Bundle", stock_item.serial_and_batch_bundle, values_to_update
|
||||||
)
|
)
|
||||||
|
|
||||||
def increase_stock_quantity(self):
|
|
||||||
if self.stock_entry:
|
|
||||||
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)
|
|
||||||
stock_entry.flags.ignore_links = True
|
|
||||||
stock_entry.cancel()
|
|
||||||
|
|
||||||
def make_gl_entries(self, cancel=False):
|
def make_gl_entries(self, cancel=False):
|
||||||
if flt(self.total_repair_cost) > 0:
|
if flt(self.total_repair_cost) > 0:
|
||||||
gl_entries = self.get_gl_entries()
|
gl_entries = self.get_gl_entries()
|
||||||
@@ -316,7 +308,7 @@ class AssetRepair(AccountsController):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
|
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
|
||||||
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)
|
stock_entry = frappe.get_doc("Stock Entry", {"asset_repair": self.name})
|
||||||
|
|
||||||
default_expense_account = None
|
default_expense_account = None
|
||||||
if not erpnext.is_perpetual_inventory_enabled(self.company):
|
if not erpnext.is_perpetual_inventory_enabled(self.company):
|
||||||
@@ -357,7 +349,7 @@ class AssetRepair(AccountsController):
|
|||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"posting_date": getdate(),
|
"posting_date": getdate(),
|
||||||
"against_voucher_type": "Stock Entry",
|
"against_voucher_type": "Stock Entry",
|
||||||
"against_voucher": self.stock_entry,
|
"against_voucher": stock_entry.name,
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
},
|
},
|
||||||
item=self,
|
item=self,
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ class TestAssetRepair(unittest.TestCase):
|
|||||||
def test_warehouse(self):
|
def test_warehouse(self):
|
||||||
asset_repair = create_asset_repair(stock_consumption=1)
|
asset_repair = create_asset_repair(stock_consumption=1)
|
||||||
self.assertTrue(asset_repair.stock_consumption)
|
self.assertTrue(asset_repair.stock_consumption)
|
||||||
self.assertTrue(asset_repair.warehouse)
|
self.assertTrue(asset_repair.stock_items[0].warehouse)
|
||||||
|
|
||||||
def test_decrease_stock_quantity(self):
|
def test_decrease_stock_quantity(self):
|
||||||
asset_repair = create_asset_repair(stock_consumption=1, submit=1)
|
asset_repair = create_asset_repair(stock_consumption=1, submit=1)
|
||||||
stock_entry = frappe.get_last_doc("Stock Entry")
|
stock_entry = frappe.get_last_doc("Stock Entry")
|
||||||
|
|
||||||
self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
|
self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
|
||||||
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
|
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.stock_items[0].warehouse)
|
||||||
self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code)
|
self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code)
|
||||||
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
|
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
|
||||||
|
|
||||||
@@ -114,14 +114,14 @@ class TestAssetRepair(unittest.TestCase):
|
|||||||
asset_repair.repair_status = "Completed"
|
asset_repair.repair_status = "Completed"
|
||||||
self.assertRaises(frappe.ValidationError, asset_repair.submit)
|
self.assertRaises(frappe.ValidationError, asset_repair.submit)
|
||||||
|
|
||||||
def test_increase_in_asset_value_due_to_stock_consumption(self):
|
def test_no_increase_in_asset_value_when_not_capitalized(self):
|
||||||
asset = create_asset(calculate_depreciation=1, submit=1)
|
asset = create_asset(calculate_depreciation=1, submit=1)
|
||||||
initial_asset_value = get_asset_value_after_depreciation(asset.name)
|
initial_asset_value = get_asset_value_after_depreciation(asset.name)
|
||||||
asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1)
|
create_asset_repair(asset=asset, stock_consumption=1, submit=1)
|
||||||
asset.reload()
|
asset.reload()
|
||||||
|
|
||||||
increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value
|
increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value
|
||||||
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
|
self.assertEqual(increase_in_asset_value, 0)
|
||||||
|
|
||||||
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
|
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
|
||||||
asset = create_asset(calculate_depreciation=1, submit=1)
|
asset = create_asset(calculate_depreciation=1, submit=1)
|
||||||
@@ -185,7 +185,7 @@ class TestAssetRepair(unittest.TestCase):
|
|||||||
frappe.get_doc("Purchase Invoice", asset_repair.purchase_invoice).items[0].expense_account
|
frappe.get_doc("Purchase Invoice", asset_repair.purchase_invoice).items[0].expense_account
|
||||||
)
|
)
|
||||||
stock_entry_expense_account = (
|
stock_entry_expense_account = (
|
||||||
frappe.get_doc("Stock Entry", asset_repair.stock_entry).get("items")[0].expense_account
|
frappe.get_doc("Stock Entry", {"asset_repair": asset_repair.name}).get("items")[0].expense_account
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_values = {
|
expected_values = {
|
||||||
@@ -260,6 +260,12 @@ class TestAssetRepair(unittest.TestCase):
|
|||||||
asset.finance_books[0].value_after_depreciation,
|
asset.finance_books[0].value_after_depreciation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_asset_repiar_link_in_stock_entry(self):
|
||||||
|
asset = create_asset(calculate_depreciation=1, submit=1)
|
||||||
|
asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1)
|
||||||
|
stock_entry = frappe.get_last_doc("Stock Entry")
|
||||||
|
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
|
||||||
|
|
||||||
|
|
||||||
def num_of_depreciations(asset):
|
def num_of_depreciations(asset):
|
||||||
return asset.finance_books[0].total_number_of_depreciations
|
return asset.finance_books[0].total_number_of_depreciations
|
||||||
@@ -289,7 +295,7 @@ def create_asset_repair(**args):
|
|||||||
|
|
||||||
if args.stock_consumption:
|
if args.stock_consumption:
|
||||||
asset_repair.stock_consumption = 1
|
asset_repair.stock_consumption = 1
|
||||||
asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company=asset.company)
|
warehouse = args.warehouse or create_warehouse("Test Warehouse", company=asset.company)
|
||||||
|
|
||||||
bundle = None
|
bundle = None
|
||||||
if args.serial_no:
|
if args.serial_no:
|
||||||
@@ -297,8 +303,8 @@ def create_asset_repair(**args):
|
|||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
"item_code": args.item_code,
|
"item_code": args.item_code,
|
||||||
"warehouse": asset_repair.warehouse,
|
"warehouse": warehouse,
|
||||||
"company": frappe.get_cached_value("Warehouse", asset_repair.warehouse, "company"),
|
"company": frappe.get_cached_value("Warehouse", warehouse, "company"),
|
||||||
"qty": (flt(args.stock_qty) or 1) * -1,
|
"qty": (flt(args.stock_qty) or 1) * -1,
|
||||||
"voucher_type": "Asset Repair",
|
"voucher_type": "Asset Repair",
|
||||||
"type_of_transaction": "Asset Repair",
|
"type_of_transaction": "Asset Repair",
|
||||||
@@ -314,6 +320,7 @@ def create_asset_repair(**args):
|
|||||||
"stock_items",
|
"stock_items",
|
||||||
{
|
{
|
||||||
"item_code": args.item_code or "_Test Stock Item",
|
"item_code": args.item_code or "_Test Stock Item",
|
||||||
|
"warehouse": warehouse,
|
||||||
"valuation_rate": args.rate if args.get("rate") is not None else 100,
|
"valuation_rate": args.rate if args.get("rate") is not None else 100,
|
||||||
"consumed_quantity": args.qty or 1,
|
"consumed_quantity": args.qty or 1,
|
||||||
"serial_and_batch_bundle": bundle,
|
"serial_and_batch_bundle": bundle,
|
||||||
@@ -333,7 +340,7 @@ def create_asset_repair(**args):
|
|||||||
stock_entry.append(
|
stock_entry.append(
|
||||||
"items",
|
"items",
|
||||||
{
|
{
|
||||||
"t_warehouse": asset_repair.warehouse,
|
"t_warehouse": asset_repair.stock_items[0].warehouse,
|
||||||
"item_code": asset_repair.stock_items[0].item_code,
|
"item_code": asset_repair.stock_items[0].item_code,
|
||||||
"qty": asset_repair.stock_items[0].consumed_quantity,
|
"qty": asset_repair.stock_items[0].consumed_quantity,
|
||||||
"basic_rate": args.rate if args.get("rate") is not None else 100,
|
"basic_rate": args.rate if args.get("rate") is not None else 100,
|
||||||
@@ -351,7 +358,7 @@ def create_asset_repair(**args):
|
|||||||
company=asset.company,
|
company=asset.company,
|
||||||
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
|
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
|
||||||
cost_center=asset_repair.cost_center,
|
cost_center=asset_repair.cost_center,
|
||||||
warehouse=asset_repair.warehouse,
|
warehouse=args.warehouse or create_warehouse("Test Warehouse", company=asset.company),
|
||||||
)
|
)
|
||||||
asset_repair.purchase_invoice = pi.name
|
asset_repair.purchase_invoice = pi.name
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"item_code",
|
"item_code",
|
||||||
|
"warehouse",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
"consumed_quantity",
|
"consumed_quantity",
|
||||||
"total_value",
|
"total_value",
|
||||||
@@ -44,19 +45,28 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Item",
|
"label": "Item",
|
||||||
"options": "Item"
|
"options": "Item",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
"options": "Serial and Batch Bundle"
|
"options": "Serial and Batch Bundle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Warehouse",
|
||||||
|
"options": "Warehouse",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-06 02:24:20.375870",
|
"modified": "2024-06-13 12:01:47.147333",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Repair Consumed Item",
|
"name": "Asset Repair Consumed Item",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class AssetRepairConsumedItem(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
consumed_quantity: DF.Data | None
|
consumed_quantity: DF.Data | None
|
||||||
item_code: DF.Link | None
|
item_code: DF.Link
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
@@ -23,6 +23,7 @@ class AssetRepairConsumedItem(Document):
|
|||||||
serial_no: DF.SmallText | None
|
serial_no: DF.SmallText | None
|
||||||
total_value: DF.Currency
|
total_value: DF.Currency
|
||||||
valuation_rate: DF.Currency
|
valuation_rate: DF.Currency
|
||||||
|
warehouse: DF.Link
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1764,8 +1764,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()
|
||||||
|
|
||||||
|
|||||||
@@ -554,6 +554,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,
|
||||||
@@ -583,20 +584,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 is None:
|
if global_qty_allowance is 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 is None:
|
if global_amount_allowance is 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
|
||||||
|
|
||||||
|
|||||||
@@ -442,6 +442,7 @@ scheduler_events = {
|
|||||||
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
|
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
|
||||||
"erpnext.accounts.utils.run_ledger_health_checks",
|
"erpnext.accounts.utils.run_ledger_health_checks",
|
||||||
|
"erpnext.assets.doctype.asset.asset_maintenance_log.update_asset_maintenance_log_status",
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionC
|
|||||||
item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item));
|
item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item));
|
||||||
refresh_field("stock_qty", item.name, item.parentfield);
|
refresh_field("stock_qty", item.name, item.parentfield);
|
||||||
this.toggle_conversion_factor(item);
|
this.toggle_conversion_factor(item);
|
||||||
this.frm.events.update_cost(this.frm);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
"column_break_3",
|
"column_break_3",
|
||||||
"production_capacity",
|
"production_capacity",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"production_capacity_section",
|
|
||||||
"parts_per_hour",
|
|
||||||
"workstation_status_tab",
|
"workstation_status_tab",
|
||||||
"status",
|
"status",
|
||||||
"column_break_glcv",
|
"column_break_glcv",
|
||||||
@@ -210,16 +208,6 @@
|
|||||||
"label": "Warehouse",
|
"label": "Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "production_capacity_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Production Capacity"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "parts_per_hour",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"label": "Parts Per Hour"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "total_working_hours",
|
"fieldname": "total_working_hours",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
@@ -252,7 +240,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"image_field": "on_status_image",
|
"image_field": "on_status_image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-30 12:43:35.808845",
|
"modified": "2024-06-20 14:17:13.806609",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Workstation",
|
"name": "Workstation",
|
||||||
@@ -277,4 +265,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ class Workstation(Document):
|
|||||||
hour_rate_electricity: DF.Currency
|
hour_rate_electricity: DF.Currency
|
||||||
hour_rate_labour: DF.Currency
|
hour_rate_labour: DF.Currency
|
||||||
hour_rate_rent: DF.Currency
|
hour_rate_rent: DF.Currency
|
||||||
|
off_status_image: DF.AttachImage | None
|
||||||
|
on_status_image: DF.AttachImage | None
|
||||||
|
plant_floor: DF.Link | None
|
||||||
production_capacity: DF.Int
|
production_capacity: DF.Int
|
||||||
working_hours: DF.Table[WorkstationWorkingHour]
|
working_hours: DF.Table[WorkstationWorkingHour]
|
||||||
workstation_name: DF.Data
|
workstation_name: DF.Data
|
||||||
|
|||||||
@@ -366,4 +366,6 @@ erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
|||||||
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||||
erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations
|
erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations
|
||||||
|
erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_doctype
|
||||||
|
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
|
||||||
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
|
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.query_builder import DocType
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.db.has_column("Asset Repair", "stock_entry"):
|
||||||
|
AssetRepair = DocType("Asset Repair")
|
||||||
|
StockEntry = DocType("Stock Entry")
|
||||||
|
|
||||||
|
(
|
||||||
|
frappe.qb.update(StockEntry)
|
||||||
|
.join(AssetRepair)
|
||||||
|
.on(StockEntry.name == AssetRepair.stock_entry)
|
||||||
|
.set(StockEntry.asset_repair, AssetRepair.name)
|
||||||
|
).run()
|
||||||
@@ -18,9 +18,10 @@ def execute():
|
|||||||
depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book)
|
depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book)
|
||||||
total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations or 0
|
total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations or 0
|
||||||
|
|
||||||
for je in depr_schedule:
|
if depr_schedule:
|
||||||
if je.journal_entry:
|
for je in depr_schedule:
|
||||||
total_number_of_booked_depreciations += 1
|
if je.journal_entry:
|
||||||
|
total_number_of_booked_depreciations += 1
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Asset Finance Book",
|
"Asset Finance Book",
|
||||||
fb_row.name,
|
fb_row.name,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
# not able to use frappe.qb because of this bug https://github.com/frappe/frappe/issues/20292
|
||||||
|
def execute():
|
||||||
|
if frappe.db.has_column("Asset Repair", "warehouse"):
|
||||||
|
# nosemgrep
|
||||||
|
frappe.db.sql(
|
||||||
|
"""UPDATE `tabAsset Repair Consumed Item` ar_item
|
||||||
|
JOIN `tabAsset Repair` ar
|
||||||
|
ON ar.name = ar_item.parent
|
||||||
|
SET ar_item.warehouse = ar.warehouse
|
||||||
|
WHERE ifnull(ar.warehouse, '') != ''"""
|
||||||
|
)
|
||||||
@@ -496,6 +496,10 @@ class SalesOrder(SellingController):
|
|||||||
def update_status(self, status):
|
def update_status(self, status):
|
||||||
self.check_modified_date()
|
self.check_modified_date()
|
||||||
self.set_status(update=True, status=status)
|
self.set_status(update=True, status=status)
|
||||||
|
# Upon Sales Order Re-open, check for credit limit.
|
||||||
|
# Limit should be checked after the 'Hold/Closed' status is reset.
|
||||||
|
if status == "Draft" and self.docstatus == 1:
|
||||||
|
self.check_credit_limit()
|
||||||
self.update_reserved_qty()
|
self.update_reserved_qty()
|
||||||
self.notify_update()
|
self.notify_update()
|
||||||
clear_doctype_notifications(self)
|
clear_doctype_notifications(self)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user
|
|||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||||
|
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||||
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
||||||
make_maintenance_schedule,
|
make_maintenance_schedule,
|
||||||
@@ -31,7 +32,7 @@ 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.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
|
||||||
class TestSalesOrder(FrappeTestCase):
|
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
@@ -49,6 +50,9 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.create_customer("_Test Customer Credit")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
@@ -2086,6 +2090,28 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
||||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||||
|
|
||||||
|
def test_credit_limit_on_so_reopning(self):
|
||||||
|
# set credit limit
|
||||||
|
company = "_Test Company"
|
||||||
|
customer = frappe.get_doc("Customer", self.customer)
|
||||||
|
customer.credit_limits = []
|
||||||
|
customer.append(
|
||||||
|
"credit_limits", {"company": company, "credit_limit": 1000, "bypass_credit_limit_check": False}
|
||||||
|
)
|
||||||
|
customer.save()
|
||||||
|
|
||||||
|
so1 = make_sales_order(qty=9, rate=100, do_not_submit=True)
|
||||||
|
so1.customer = self.customer
|
||||||
|
so1.save().submit()
|
||||||
|
|
||||||
|
so1.update_status("Closed")
|
||||||
|
|
||||||
|
so2 = make_sales_order(qty=9, rate=100, do_not_submit=True)
|
||||||
|
so2.customer = self.customer
|
||||||
|
so2.save().submit()
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
||||||
|
|
||||||
|
|
||||||
def automatically_fetch_payment_terms(enable=1):
|
def automatically_fetch_payment_terms(enable=1):
|
||||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||||
|
|||||||
@@ -107,7 +107,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,
|
||||||
|
|||||||
@@ -963,6 +963,7 @@ def get_available_item_locations_for_batched_item(
|
|||||||
{
|
{
|
||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
"warehouse": from_warehouses,
|
"warehouse": from_warehouses,
|
||||||
|
"based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
|
|
||||||
if not (
|
if not (
|
||||||
(erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items)
|
(erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items)
|
||||||
or d.is_fixed_asset
|
or (d.is_fixed_asset and not d.purchase_invoice)
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -776,6 +776,13 @@ class PurchaseReceipt(BuyingController):
|
|||||||
posting_date=posting_date,
|
posting_date=posting_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_landed_cost_booked_for_any_item(self) -> bool:
|
||||||
|
for x in self.items:
|
||||||
|
if x.landed_cost_voucher_amount != 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False):
|
def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False):
|
||||||
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
|
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
|
||||||
is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
|
is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
|
||||||
@@ -811,7 +818,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
i = 1
|
i = 1
|
||||||
for tax in self.get("taxes"):
|
for tax in self.get("taxes"):
|
||||||
if valuation_tax.get(tax.name):
|
if valuation_tax.get(tax.name):
|
||||||
if via_landed_cost_voucher:
|
if via_landed_cost_voucher or self.is_landed_cost_booked_for_any_item():
|
||||||
account = tax.account_head
|
account = tax.account_head
|
||||||
else:
|
else:
|
||||||
negative_expense_booked_in_pi = frappe.db.sql(
|
negative_expense_booked_in_pi = frappe.db.sql(
|
||||||
|
|||||||
@@ -3020,6 +3020,156 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(pr_return.items[0].rejected_qty, 0.0)
|
self.assertEqual(pr_return.items[0].rejected_qty, 0.0)
|
||||||
self.assertEqual(pr_return.items[0].rejected_warehouse, "")
|
self.assertEqual(pr_return.items[0].rejected_warehouse, "")
|
||||||
|
|
||||||
|
def test_tax_account_heads_on_lcv_and_item_repost(self):
|
||||||
|
"""
|
||||||
|
PO -> PR -> PI
|
||||||
|
PR -> LCV
|
||||||
|
Backdated `Repost Item valuation` should not merge tax account heads into stock_rbnb
|
||||||
|
"""
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||||
|
create_purchase_order,
|
||||||
|
make_pr_against_po,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||||
|
|
||||||
|
stock_rbnb = "Stock Received But Not Billed - _TC"
|
||||||
|
stock_in_hand = "Stock In Hand - _TC"
|
||||||
|
test_cc = "_Test Cost Center - _TC"
|
||||||
|
test_company = "_Test Company"
|
||||||
|
creditors = "Creditors - _TC"
|
||||||
|
lcv_expense_account = "Expenses Included In Valuation - _TC"
|
||||||
|
|
||||||
|
company_doc = frappe.get_doc("Company", test_company)
|
||||||
|
company_doc.enable_perpetual_inventory = True
|
||||||
|
company_doc.stock_received_but_not_billed = stock_rbnb
|
||||||
|
company_doc.default_inventory_account = stock_in_hand
|
||||||
|
company_doc.save()
|
||||||
|
|
||||||
|
packaging_charges_account = create_account(
|
||||||
|
account_name="Packaging Charges",
|
||||||
|
parent_account="Indirect Expenses - _TC",
|
||||||
|
company=test_company,
|
||||||
|
account_type="Tax",
|
||||||
|
)
|
||||||
|
|
||||||
|
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||||
|
po.taxes = []
|
||||||
|
po.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"category": "Valuation and Total",
|
||||||
|
"account_head": packaging_charges_account,
|
||||||
|
"cost_center": test_cc,
|
||||||
|
"description": "Test",
|
||||||
|
"add_deduct_tax": "Add",
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"tax_amount": 250,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
po.save().submit()
|
||||||
|
|
||||||
|
pr = make_pr_against_po(po.name, received_qty=10)
|
||||||
|
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
|
expected_pr_gles = [
|
||||||
|
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
|
{"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||||
|
]
|
||||||
|
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||||
|
|
||||||
|
# Make PI against Purchase Receipt
|
||||||
|
pi = make_purchase_invoice(pr.name).save().submit()
|
||||||
|
pi_gl_entries = get_gl_entries(pi.doctype, pi.name, skip_cancelled=True)
|
||||||
|
expected_pi_gles = [
|
||||||
|
{"account": stock_rbnb, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": packaging_charges_account, "debit": 250.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": creditors, "debit": 0.0, "credit": 1250.0, "cost_center": None},
|
||||||
|
]
|
||||||
|
self.assertEqual(expected_pi_gles, pi_gl_entries)
|
||||||
|
|
||||||
|
lcv = self.create_lcv(pr.doctype, pr.name, test_company, lcv_expense_account)
|
||||||
|
pr_gles_after_lcv = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
|
expected_pr_gles_after_lcv = [
|
||||||
|
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
|
{"account": stock_in_hand, "debit": 1300.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||||
|
{"account": lcv_expense_account, "debit": 0.0, "credit": 50.0, "cost_center": test_cc},
|
||||||
|
]
|
||||||
|
self.assertEqual(expected_pr_gles_after_lcv, pr_gles_after_lcv)
|
||||||
|
|
||||||
|
# Trigger Repost Item Valudation on a older date
|
||||||
|
repost_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Repost Item Valuation",
|
||||||
|
"based_on": "Item and Warehouse",
|
||||||
|
"item_code": pr.items[0].item_code,
|
||||||
|
"warehouse": pr.items[0].warehouse,
|
||||||
|
"posting_date": add_days(pr.posting_date, -1),
|
||||||
|
"posting_time": "00:00:00",
|
||||||
|
"company": pr.company,
|
||||||
|
"allow_negative_stock": 1,
|
||||||
|
"via_landed_cost_voucher": 0,
|
||||||
|
"allow_zero_rate": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
repost_doc.save().submit()
|
||||||
|
|
||||||
|
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
|
expected_pr_gles_after_repost = [
|
||||||
|
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
|
{"account": stock_in_hand, "debit": 1300.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||||
|
{"account": lcv_expense_account, "debit": 0.0, "credit": 50.0, "cost_center": test_cc},
|
||||||
|
]
|
||||||
|
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||||
|
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
lcv.reload()
|
||||||
|
lcv.cancel()
|
||||||
|
pi.reload()
|
||||||
|
pi.cancel()
|
||||||
|
pr.reload()
|
||||||
|
pr.cancel()
|
||||||
|
|
||||||
|
company_doc.enable_perpetual_inventory = False
|
||||||
|
company_doc.stock_received_but_not_billed = None
|
||||||
|
company_doc.default_inventory_account = None
|
||||||
|
company_doc.save()
|
||||||
|
|
||||||
|
def create_lcv(self, receipt_document_type, receipt_document, company, expense_account, charges=50):
|
||||||
|
ref_doc = frappe.get_doc(receipt_document_type, receipt_document)
|
||||||
|
|
||||||
|
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||||
|
lcv.company = company
|
||||||
|
lcv.distribute_charges_based_on = "Qty"
|
||||||
|
lcv.set(
|
||||||
|
"purchase_receipts",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"receipt_document_type": receipt_document_type,
|
||||||
|
"receipt_document": receipt_document,
|
||||||
|
"supplier": ref_doc.supplier,
|
||||||
|
"posting_date": ref_doc.posting_date,
|
||||||
|
"grand_total": ref_doc.base_grand_total,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
lcv.set(
|
||||||
|
"taxes",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"description": "Testing",
|
||||||
|
"expense_account": expense_account,
|
||||||
|
"amount": charges,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
lcv.save().submit()
|
||||||
|
return lcv
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -128,9 +128,7 @@ frappe.ui.form.on("Repost Item Valuation", {
|
|||||||
method: "restart_reposting",
|
method: "restart_reposting",
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (!r.exc) {
|
frm.reload_doc();
|
||||||
frm.refresh();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -218,13 +218,14 @@
|
|||||||
"fieldname": "reposting_data_file",
|
"fieldname": "reposting_data_file",
|
||||||
"fieldtype": "Attach",
|
"fieldtype": "Attach",
|
||||||
"label": "Reposting Data File",
|
"label": "Reposting Data File",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-31 12:48:57.138693",
|
"modified": "2024-06-27 16:55:23.150146",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Repost Item Valuation",
|
"name": "Repost Item Valuation",
|
||||||
@@ -276,4 +277,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class RepostItemValuation(Document):
|
|||||||
def clear_attachment(self):
|
def clear_attachment(self):
|
||||||
if attachments := get_attachments(self.doctype, self.name):
|
if attachments := get_attachments(self.doctype, self.name):
|
||||||
attachment = attachments[0]
|
attachment = attachments[0]
|
||||||
frappe.delete_doc("File", attachment.name)
|
frappe.delete_doc("File", attachment.name, ignore_permissions=True)
|
||||||
|
|
||||||
if self.reposting_data_file:
|
if self.reposting_data_file:
|
||||||
self.db_set("reposting_data_file", None)
|
self.db_set("reposting_data_file", None)
|
||||||
@@ -219,6 +219,7 @@ class RepostItemValuation(Document):
|
|||||||
self.distinct_item_and_warehouse = None
|
self.distinct_item_and_warehouse = None
|
||||||
self.items_to_be_repost = None
|
self.items_to_be_repost = None
|
||||||
self.gl_reposting_index = 0
|
self.gl_reposting_index = 0
|
||||||
|
self.clear_attachment()
|
||||||
self.db_update()
|
self.db_update()
|
||||||
|
|
||||||
def deduplicate_similar_repost(self):
|
def deduplicate_similar_repost(self):
|
||||||
@@ -271,6 +272,7 @@ def repost(doc):
|
|||||||
repost_gl_entries(doc)
|
repost_gl_entries(doc)
|
||||||
|
|
||||||
doc.set_status("Completed")
|
doc.set_status("Completed")
|
||||||
|
doc.db_set("reposting_data_file", None)
|
||||||
remove_attached_file(doc.name)
|
remove_attached_file(doc.name)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -315,7 +317,7 @@ def remove_attached_file(docname):
|
|||||||
if file_name := frappe.db.get_value(
|
if file_name := frappe.db.get_value(
|
||||||
"File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name"
|
"File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name"
|
||||||
):
|
):
|
||||||
frappe.delete_doc("File", file_name, delete_permanently=True)
|
frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
|
||||||
|
|
||||||
|
|
||||||
def repost_sl_entries(doc):
|
def repost_sl_entries(doc):
|
||||||
|
|||||||
@@ -646,6 +646,61 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(flt(stock_value_difference, 2), 353.0 * -1)
|
self.assertEqual(flt(stock_value_difference, 2), 353.0 * -1)
|
||||||
|
|
||||||
|
def test_pick_serial_nos_for_batch_item(self):
|
||||||
|
item_code = make_item(
|
||||||
|
"Test Pick Serial Nos for Batch Item 1",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_no_series": "PSNBI-TSNVL-.#####",
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "SN-PSNBI-TSNVL-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=10,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
rate=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch1 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
serial_nos1 = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=10,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
rate=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch2 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
serial_nos2 = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=10,
|
||||||
|
source="_Test Warehouse - _TC",
|
||||||
|
use_serial_batch_fields=True,
|
||||||
|
batch_no=batch2,
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
self.assertEqual(serial_nos, serial_nos2)
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=10,
|
||||||
|
source="_Test Warehouse - _TC",
|
||||||
|
use_serial_batch_fields=True,
|
||||||
|
batch_no=batch1,
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
self.assertEqual(serial_nos, serial_nos1)
|
||||||
|
|
||||||
|
|
||||||
def get_batch_from_bundle(bundle):
|
def get_batch_from_bundle(bundle):
|
||||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"sales_invoice_no",
|
"sales_invoice_no",
|
||||||
"pick_list",
|
"pick_list",
|
||||||
"purchase_receipt_no",
|
"purchase_receipt_no",
|
||||||
|
"asset_repair",
|
||||||
"col2",
|
"col2",
|
||||||
"company",
|
"company",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
@@ -674,6 +675,14 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_eaoa",
|
"fieldname": "column_break_eaoa",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.asset_repair",
|
||||||
|
"fieldname": "asset_repair",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Asset Repair",
|
||||||
|
"options": "Asset Repair",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
@@ -681,7 +690,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-12 11:56:58.644882",
|
"modified": "2024-06-26 19:12:17.937088",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry",
|
"name": "Stock Entry",
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class StockEntry(StockController):
|
|||||||
address_display: DF.SmallText | None
|
address_display: DF.SmallText | None
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
apply_putaway_rule: DF.Check
|
apply_putaway_rule: DF.Check
|
||||||
|
asset_repair: DF.Link | None
|
||||||
bom_no: DF.Link | None
|
bom_no: DF.Link | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
credit_note: DF.Link | None
|
credit_note: DF.Link | None
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
"oldfieldtype": "Date",
|
"oldfieldtype": "Date",
|
||||||
"print_width": "100px",
|
"print_width": "100px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
|
"search_index": 1,
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -360,7 +361,7 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-13 09:56:13.021696",
|
"modified": "2024-06-27 16:23:18.820049",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Ledger Entry",
|
"name": "Stock Ledger Entry",
|
||||||
@@ -384,4 +385,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,19 +103,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 args.get("doctype") == "Material Request" and args.get("material_request_type") == "Material Transfer":
|
if item.is_stock_item:
|
||||||
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
|
update_bin_details(args, out, doc)
|
||||||
|
|
||||||
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():
|
||||||
@@ -166,6 +155,19 @@ 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)
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ frappe.query_reports["Stock Balance"] = {
|
|||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "include_zero_stock_items",
|
||||||
|
label: __("Include Zero Stock Items"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
formatter: function (value, row, column, data, default_formatter) {
|
formatter: function (value, row, column, data, default_formatter) {
|
||||||
|
|||||||
@@ -138,7 +138,12 @@ class StockBalanceReport:
|
|||||||
{"reserved_stock": sre_details.get((report_data.item_code, report_data.warehouse), 0.0)}
|
{"reserved_stock": sre_details.get((report_data.item_code, report_data.warehouse), 0.0)}
|
||||||
)
|
)
|
||||||
|
|
||||||
if report_data and report_data.bal_qty == 0 and report_data.bal_val == 0:
|
if (
|
||||||
|
not self.filters.get("include_zero_stock_items")
|
||||||
|
and report_data
|
||||||
|
and report_data.bal_qty == 0
|
||||||
|
and report_data.bal_val == 0
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.data.append(report_data)
|
self.data.append(report_data)
|
||||||
|
|||||||
@@ -950,7 +950,17 @@ class SerialBatchCreation:
|
|||||||
if self.get("ignore_serial_nos"):
|
if self.get("ignore_serial_nos"):
|
||||||
kwargs["ignore_serial_nos"] = self.ignore_serial_nos
|
kwargs["ignore_serial_nos"] = self.ignore_serial_nos
|
||||||
|
|
||||||
if self.has_serial_no and not self.get("serial_nos"):
|
if (
|
||||||
|
self.has_serial_no
|
||||||
|
and self.has_batch_no
|
||||||
|
and not self.get("serial_nos")
|
||||||
|
and self.get("batches")
|
||||||
|
and len(self.get("batches")) == 1
|
||||||
|
):
|
||||||
|
# If only one batch is available and no serial no is available
|
||||||
|
kwargs["batches"] = next(iter(self.get("batches").keys()))
|
||||||
|
self.serial_nos = get_serial_nos_for_outward(kwargs)
|
||||||
|
elif self.has_serial_no and not self.get("serial_nos"):
|
||||||
self.serial_nos = get_serial_nos_for_outward(kwargs)
|
self.serial_nos = get_serial_nos_for_outward(kwargs)
|
||||||
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
|
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
|
||||||
self.batches = get_available_batches(kwargs)
|
self.batches = get_available_batches(kwargs)
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ def get_stock_value_on(
|
|||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.select(IfNull(Sum(sle.stock_value_difference), 0))
|
.select(IfNull(Sum(sle.stock_value_difference), 0))
|
||||||
.where((sle.posting_date <= posting_date) & (sle.is_cancelled == 0))
|
.where((sle.posting_date <= posting_date) & (sle.is_cancelled == 0))
|
||||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=frappe.qb.desc)
|
|
||||||
.orderby(sle.creation, order=frappe.qb.desc)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if warehouses:
|
if warehouses:
|
||||||
|
|||||||
@@ -426,6 +426,12 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_available_qty_for_consumption(self):
|
def validate_available_qty_for_consumption(self):
|
||||||
|
if (
|
||||||
|
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||||
|
== "BOM"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
for item in self.get("supplied_items"):
|
for item in self.get("supplied_items"):
|
||||||
precision = item.precision("consumed_qty")
|
precision = item.precision("consumed_qty")
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
|
self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
|
||||||
|
|
||||||
def test_available_qty_for_consumption(self):
|
def test_available_qty_for_consumption(self):
|
||||||
|
set_backflush_based_on("BOM")
|
||||||
make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
|
make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
|
||||||
make_stock_entry(
|
make_stock_entry(
|
||||||
item_code="_Test Item Home Desktop 100",
|
item_code="_Test Item Home Desktop 100",
|
||||||
@@ -125,7 +126,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
scr = make_subcontracting_receipt(sco.name)
|
scr = make_subcontracting_receipt(sco.name)
|
||||||
scr.save()
|
scr.save()
|
||||||
self.assertRaises(frappe.ValidationError, scr.submit)
|
scr.submit()
|
||||||
|
|
||||||
def test_subcontracting_gle_fg_item_rate_zero(self):
|
def test_subcontracting_gle_fg_item_rate_zero(self):
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
||||||
@@ -476,6 +477,21 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
# consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6
|
# consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6
|
||||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
|
self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
|
||||||
|
|
||||||
|
# Do not transfer materials to the supplier warehouse and check whether system allows to consumed directly from the supplier's warehouse
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
|
||||||
|
# Transfer RM's
|
||||||
|
rm_items = get_rm_items(sco.supplied_items)
|
||||||
|
itemwise_details = make_stock_in_entry(rm_items=rm_items, warehouse="_Test Warehouse 1 - _TC")
|
||||||
|
|
||||||
|
# Create Subcontracting Receipt
|
||||||
|
scr = make_subcontracting_receipt(sco.name)
|
||||||
|
scr.submit()
|
||||||
|
self.assertEqual(scr.docstatus, 1)
|
||||||
|
|
||||||
|
for item in scr.supplied_items:
|
||||||
|
self.assertFalse(item.available_qty_for_consumption)
|
||||||
|
|
||||||
def test_supplied_items_cost_after_reposting(self):
|
def test_supplied_items_cost_after_reposting(self):
|
||||||
# Set Backflush Based On as "BOM"
|
# Set Backflush Based On as "BOM"
|
||||||
set_backflush_based_on("BOM")
|
set_backflush_based_on("BOM")
|
||||||
|
|||||||
Reference in New Issue
Block a user