mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-19 15:02:12 +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",
|
||||
"oldfieldname": "account_type",
|
||||
"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",
|
||||
@@ -190,7 +191,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-20 18:18:44.405723",
|
||||
"modified": "2024-06-27 16:23:04.444354",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -251,4 +252,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,14 +255,16 @@ def get_accounting_dimensions(as_list=True, filters=None):
|
||||
|
||||
|
||||
def get_checks_for_pl_and_bs_accounts():
|
||||
dimensions = frappe.db.sql(
|
||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
||||
WHERE p.name = c.parent""",
|
||||
as_dict=1,
|
||||
)
|
||||
if frappe.flags.accounting_dimensions_details is None:
|
||||
# nosemgrep
|
||||
frappe.flags.accounting_dimensions_details = frappe.db.sql(
|
||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
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):
|
||||
|
||||
@@ -78,6 +78,8 @@ class TestAccountingDimension(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
disable_dimension()
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
frappe.flags.dimension_filter_map = None
|
||||
|
||||
|
||||
def create_dimension():
|
||||
|
||||
@@ -66,37 +66,41 @@ class AccountingDimensionFilter(Document):
|
||||
|
||||
|
||||
def get_dimension_filter_map():
|
||||
filters = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
|
||||
WHERE
|
||||
p.name = a.parent
|
||||
AND p.disabled = 0
|
||||
""",
|
||||
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,
|
||||
if not frappe.flags.get("dimension_filter_map"):
|
||||
# nosemgrep
|
||||
filters = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
||||
`tabAccounting Dimension Filter` p
|
||||
WHERE
|
||||
p.name = a.parent
|
||||
AND p.disabled = 0
|
||||
AND p.name = d.parent
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
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):
|
||||
|
||||
@@ -47,6 +47,8 @@ class TestAccountingDimensionFilter(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
disable_dimension_filter()
|
||||
disable_dimension()
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
frappe.flags.dimension_filter_map = None
|
||||
|
||||
for si in self.invoice_list:
|
||||
si.load_from_db()
|
||||
|
||||
@@ -142,6 +142,8 @@ class Budget(Document):
|
||||
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
if not frappe.get_all("Budget", limit=1):
|
||||
return
|
||||
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
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"
|
||||
)
|
||||
|
||||
if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec
|
||||
return
|
||||
|
||||
if not args.account:
|
||||
args.account = args.get("expense_account")
|
||||
|
||||
@@ -175,12 +180,12 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
if (
|
||||
args.get(budget_against)
|
||||
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")
|
||||
|
||||
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}`
|
||||
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
||||
args.is_tree = True
|
||||
|
||||
@@ -1,94 +1,42 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2016-05-16 11:54:09.286135",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2016-05-16 11:54:09.286135",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account",
|
||||
"budget_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "budget_amount",
|
||||
"fieldtype": "Currency",
|
||||
"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
|
||||
"fieldname": "budget_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Budget Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-01-02 17:02:53.339420",
|
||||
"modified_by": "Administrator",
|
||||
"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
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-04 15:43:27.016947",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -179,7 +179,8 @@
|
||||
"fieldname": "voucher_detail_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher Detail No",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
@@ -290,7 +291,7 @@
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-18 15:38:14.006208",
|
||||
"modified": "2024-07-02 14:31:51.496466",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
@@ -322,7 +323,7 @@
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "modified",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,6 @@ class GLEntry(Document):
|
||||
account: DF.Link | None
|
||||
account_currency: DF.Link | None
|
||||
against: DF.Text | None
|
||||
against_link: DF.DynamicLink | None
|
||||
against_type: DF.Link | None
|
||||
against_voucher: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
company: DF.Link | None
|
||||
@@ -328,7 +326,7 @@ def update_outstanding_amt(
|
||||
party_condition = ""
|
||||
|
||||
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)})"
|
||||
else:
|
||||
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):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
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:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
|
||||
@@ -1217,13 +1217,21 @@ class PaymentEntry(AccountsController):
|
||||
if reference.reference_doctype == "Sales Invoice":
|
||||
return "credit", reference.account
|
||||
|
||||
if reference.reference_doctype == "Purchase Invoice":
|
||||
return "debit", reference.account
|
||||
|
||||
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":
|
||||
return "credit", self.party_account
|
||||
else:
|
||||
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):
|
||||
args_dict = {
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
expected_gle = dict(
|
||||
(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)
|
||||
|
||||
@@ -161,11 +161,12 @@ class PaymentLedgerEntry(Document):
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
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)
|
||||
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
|
||||
if (
|
||||
|
||||
@@ -509,7 +509,11 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
|
||||
@change_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):
|
||||
transaction_date = nowdate()
|
||||
|
||||
@@ -109,6 +109,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
"account_currency": "INR",
|
||||
"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:
|
||||
@@ -1574,6 +1582,229 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
)
|
||||
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):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -549,6 +549,21 @@ class PurchaseInvoice(BuyingController):
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset:
|
||||
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 not self.asset_received_but_not_billed:
|
||||
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)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries()
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
if self.docstatus == 1:
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries()
|
||||
|
||||
if gl_entries:
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
|
||||
if self.docstatus == 1:
|
||||
if gl_entries:
|
||||
make_gl_entries(
|
||||
gl_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
@@ -809,32 +823,43 @@ class PurchaseInvoice(BuyingController):
|
||||
from_repost=from_repost,
|
||||
)
|
||||
self.make_exchange_gain_loss_journal()
|
||||
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:
|
||||
elif self.docstatus == 2:
|
||||
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):
|
||||
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"
|
||||
)
|
||||
)
|
||||
|
||||
purchase_receipt_doc_map = {}
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.get_provisional_accounts()
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
@@ -1087,49 +1112,7 @@ class PurchaseInvoice(BuyingController):
|
||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
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")
|
||||
),
|
||||
)
|
||||
self.make_provisional_gl_entry(gl_entries, item)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
@@ -1225,6 +1208,59 @@ class PurchaseInvoice(BuyingController):
|
||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||
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):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
|
||||
@@ -10,7 +10,11 @@ import erpnext
|
||||
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.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.controllers.accounts_controller import get_payment_terms
|
||||
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.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):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -2202,13 +2202,14 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
|
||||
expected_values = [
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["Round Off - _TC", 0.01, 0.01],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
]
|
||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||
expected_values = {
|
||||
"_Test Account Service Tax - _TC": [0.0, 114.41],
|
||||
"_Test Account VAT - _TC": [0.0, 114.41],
|
||||
si.debit_to: [1500, 0.0],
|
||||
round_off_account: [0.01, 0.01],
|
||||
"Sales - _TC": [0.0, 1271.18],
|
||||
}
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
@@ -2219,10 +2220,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[i][0], gle.account)
|
||||
self.assertEqual(expected_values[i][1], gle.debit)
|
||||
self.assertEqual(expected_values[i][2], gle.credit)
|
||||
for gle in gl_entries:
|
||||
expected_account_values = expected_values[gle.account]
|
||||
self.assertEqual(expected_account_values[0], gle.debit)
|
||||
self.assertEqual(expected_account_values[1], gle.credit)
|
||||
|
||||
def test_rounding_adjustment_3(self):
|
||||
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.rounding_adjustment, -0.02)
|
||||
|
||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
@@ -2277,7 +2279,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["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,
|
||||
)
|
||||
|
||||
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||
self.assertEqual(round_off_gle.location, "Block 1")
|
||||
if round_off_gle:
|
||||
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||
self.assertEqual(round_off_gle.location, "Block 1")
|
||||
|
||||
disable_dimension()
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
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
|
||||
Unreconcile only one payment from one invoice
|
||||
|
||||
@@ -7,7 +7,7 @@ import copy
|
||||
import frappe
|
||||
from frappe import _
|
||||
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
|
||||
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):
|
||||
merged_gl_map = []
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
merge_properties = get_merge_properties(accounting_dimensions)
|
||||
|
||||
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
|
||||
# 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:
|
||||
same_head.debit = flt(same_head.debit) + flt(entry.debit)
|
||||
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
|
||||
|
||||
|
||||
def check_if_in_list(gle, gl_map, dimensions=None):
|
||||
account_head_fieldnames = [
|
||||
"voucher_detail_no",
|
||||
"party",
|
||||
"against_voucher",
|
||||
def get_merge_properties(dimensions=None):
|
||||
merge_properties = [
|
||||
"account",
|
||||
"cost_center",
|
||||
"against_voucher_type",
|
||||
"party",
|
||||
"party_type",
|
||||
"voucher_detail_no",
|
||||
"against_voucher",
|
||||
"against_voucher_type",
|
||||
"project",
|
||||
"finance_book",
|
||||
"voucher_no",
|
||||
]
|
||||
|
||||
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:
|
||||
same_head = True
|
||||
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:
|
||||
if e.merge_key == gle.merge_key:
|
||||
return e
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
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.utils import (
|
||||
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):
|
||||
if not posting_date:
|
||||
posting_date = nowdate()
|
||||
|
||||
warehouse_account = get_warehouse_account_map(company)
|
||||
|
||||
account_balance = get_balance_on(
|
||||
account, posting_date, in_account_currency=False, ignore_account_permission=True
|
||||
)
|
||||
|
||||
related_warehouses = [
|
||||
wh
|
||||
for wh, wh_details in warehouse_account.items()
|
||||
if wh_details.account == account and not wh_details.is_group
|
||||
]
|
||||
account_table = frappe.qb.DocType("Account")
|
||||
query = (
|
||||
frappe.qb.from_(account_table)
|
||||
.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)
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ class AssetMaintenance(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.assets.doctype.asset_maintenance_task.asset_maintenance_task import (
|
||||
AssetMaintenanceTask,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_maintenance_task.asset_maintenance_task import AssetMaintenanceTask
|
||||
|
||||
asset_category: DF.ReadOnly | None
|
||||
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)
|
||||
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):
|
||||
tasks_names = []
|
||||
for task in self.get("asset_maintenance_tasks"):
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
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
|
||||
|
||||
@@ -75,6 +76,17 @@ class AssetMaintenanceLog(Document):
|
||||
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.validate_and_sanitize_search_inputs
|
||||
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 {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("serial_and_batch_bundle", "stock_items", (doc, 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());
|
||||
}
|
||||
},
|
||||
@@ -87,15 +87,48 @@ frappe.ui.form.on("Asset Repair", {
|
||||
stock_items_on_form_rendered() {
|
||||
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", {
|
||||
item_code: function (frm, cdt, cdn) {
|
||||
warehouse: function (frm, 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 = {
|
||||
item_code: item.item_code,
|
||||
warehouse: frm.doc.warehouse,
|
||||
warehouse: item.warehouse,
|
||||
qty: item.consumed_quantity,
|
||||
serial_no: item.serial_no,
|
||||
company: frm.doc.company,
|
||||
|
||||
@@ -22,16 +22,14 @@
|
||||
"column_break_14",
|
||||
"project",
|
||||
"accounting_details",
|
||||
"repair_cost",
|
||||
"purchase_invoice",
|
||||
"capitalize_repair_cost",
|
||||
"stock_consumption",
|
||||
"column_break_8",
|
||||
"purchase_invoice",
|
||||
"repair_cost",
|
||||
"stock_consumption_details_section",
|
||||
"warehouse",
|
||||
"stock_items",
|
||||
"total_repair_cost",
|
||||
"stock_entry",
|
||||
"asset_depreciation_details_section",
|
||||
"increase_in_asset_life",
|
||||
"section_break_9",
|
||||
@@ -122,7 +120,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "repair_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Repair Cost"
|
||||
"label": "Repair Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
@@ -218,13 +217,6 @@
|
||||
"label": "Total Repair Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "stock_consumption",
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "capitalize_repair_cost",
|
||||
"fieldname": "asset_depreciation_details_section",
|
||||
@@ -251,20 +243,12 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-16 15:55:25.023471",
|
||||
"modified": "2024-06-13 16:14:14.398356",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
|
||||
@@ -47,20 +47,25 @@ class AssetRepair(AccountsController):
|
||||
repair_cost: DF.Currency
|
||||
repair_status: DF.Literal["Pending", "Completed", "Cancelled"]
|
||||
stock_consumption: DF.Check
|
||||
stock_entry: DF.Link | None
|
||||
stock_items: DF.Table[AssetRepairConsumedItem]
|
||||
total_repair_cost: DF.Currency
|
||||
warehouse: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
self.validate_dates()
|
||||
self.update_status()
|
||||
|
||||
if self.get("stock_items"):
|
||||
self.set_stock_items_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):
|
||||
if self.repair_status == "Pending" and self.asset_doc.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:
|
||||
self.modify_depreciation_schedule()
|
||||
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
|
||||
).format(
|
||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
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()
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
|
||||
).format(
|
||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
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(
|
||||
self.asset,
|
||||
_("Asset updated after completion of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset updated after completion of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
def before_cancel(self):
|
||||
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.additional_asset_cost -= self.repair_cost
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
self.increase_stock_quantity()
|
||||
if self.get("capitalize_repair_cost"):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.make_gl_entries(cancel=True)
|
||||
self.db_set("stock_entry", None)
|
||||
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||
self.revert_depreciation_schedule_on_cancellation()
|
||||
|
||||
notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format(
|
||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
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()
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0}'s Asset Repair {1} was cancelled."
|
||||
).format(
|
||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
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(
|
||||
self.asset,
|
||||
_("Asset updated after cancellation of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset updated after cancellation of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
def after_delete(self):
|
||||
frappe.get_doc("Asset", self.asset).set_status()
|
||||
@@ -170,11 +174,6 @@ class AssetRepair(AccountsController):
|
||||
def check_for_stock_items_and_warehouse(self):
|
||||
if not self.get("stock_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):
|
||||
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
|
||||
@@ -208,6 +207,7 @@ class AssetRepair(AccountsController):
|
||||
stock_entry = frappe.get_doc(
|
||||
{"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"):
|
||||
self.validate_serial_no(stock_item)
|
||||
@@ -215,7 +215,7 @@ class AssetRepair(AccountsController):
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"s_warehouse": self.warehouse,
|
||||
"s_warehouse": stock_item.warehouse,
|
||||
"item_code": stock_item.item_code,
|
||||
"qty": stock_item.consumed_quantity,
|
||||
"basic_rate": stock_item.valuation_rate,
|
||||
@@ -228,8 +228,6 @@ class AssetRepair(AccountsController):
|
||||
stock_entry.insert()
|
||||
stock_entry.submit()
|
||||
|
||||
self.db_set("stock_entry", stock_entry.name)
|
||||
|
||||
def validate_serial_no(self, stock_item):
|
||||
if not stock_item.serial_and_batch_bundle and frappe.get_cached_value(
|
||||
"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
|
||||
)
|
||||
|
||||
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):
|
||||
if flt(self.total_repair_cost) > 0:
|
||||
gl_entries = self.get_gl_entries()
|
||||
@@ -316,7 +308,7 @@ class AssetRepair(AccountsController):
|
||||
return
|
||||
|
||||
# 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
|
||||
if not erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
@@ -357,7 +349,7 @@ class AssetRepair(AccountsController):
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"against_voucher_type": "Stock Entry",
|
||||
"against_voucher": self.stock_entry,
|
||||
"against_voucher": stock_entry.name,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
|
||||
@@ -76,14 +76,14 @@ class TestAssetRepair(unittest.TestCase):
|
||||
def test_warehouse(self):
|
||||
asset_repair = create_asset_repair(stock_consumption=1)
|
||||
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):
|
||||
asset_repair = create_asset_repair(stock_consumption=1, submit=1)
|
||||
stock_entry = frappe.get_last_doc("Stock Entry")
|
||||
|
||||
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].qty, asset_repair.stock_items[0].consumed_quantity)
|
||||
|
||||
@@ -114,14 +114,14 @@ class TestAssetRepair(unittest.TestCase):
|
||||
asset_repair.repair_status = "Completed"
|
||||
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)
|
||||
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()
|
||||
|
||||
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):
|
||||
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
|
||||
)
|
||||
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 = {
|
||||
@@ -260,6 +260,12 @@ class TestAssetRepair(unittest.TestCase):
|
||||
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):
|
||||
return asset.finance_books[0].total_number_of_depreciations
|
||||
@@ -289,7 +295,7 @@ def create_asset_repair(**args):
|
||||
|
||||
if args.stock_consumption:
|
||||
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
|
||||
if args.serial_no:
|
||||
@@ -297,8 +303,8 @@ def create_asset_repair(**args):
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": args.item_code,
|
||||
"warehouse": asset_repair.warehouse,
|
||||
"company": frappe.get_cached_value("Warehouse", asset_repair.warehouse, "company"),
|
||||
"warehouse": warehouse,
|
||||
"company": frappe.get_cached_value("Warehouse", warehouse, "company"),
|
||||
"qty": (flt(args.stock_qty) or 1) * -1,
|
||||
"voucher_type": "Asset Repair",
|
||||
"type_of_transaction": "Asset Repair",
|
||||
@@ -314,6 +320,7 @@ def create_asset_repair(**args):
|
||||
"stock_items",
|
||||
{
|
||||
"item_code": args.item_code or "_Test Stock Item",
|
||||
"warehouse": warehouse,
|
||||
"valuation_rate": args.rate if args.get("rate") is not None else 100,
|
||||
"consumed_quantity": args.qty or 1,
|
||||
"serial_and_batch_bundle": bundle,
|
||||
@@ -333,7 +340,7 @@ def create_asset_repair(**args):
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"t_warehouse": asset_repair.warehouse,
|
||||
"t_warehouse": asset_repair.stock_items[0].warehouse,
|
||||
"item_code": asset_repair.stock_items[0].item_code,
|
||||
"qty": asset_repair.stock_items[0].consumed_quantity,
|
||||
"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,
|
||||
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
|
||||
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
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"warehouse",
|
||||
"valuation_rate",
|
||||
"consumed_quantity",
|
||||
"total_value",
|
||||
@@ -44,19 +45,28 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item",
|
||||
"options": "Item"
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-06 02:24:20.375870",
|
||||
"modified": "2024-06-13 12:01:47.147333",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair Consumed Item",
|
||||
|
||||
@@ -15,7 +15,7 @@ class AssetRepairConsumedItem(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
consumed_quantity: DF.Data | None
|
||||
item_code: DF.Link | None
|
||||
item_code: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
@@ -23,6 +23,7 @@ class AssetRepairConsumedItem(Document):
|
||||
serial_no: DF.SmallText | None
|
||||
total_value: DF.Currency
|
||||
valuation_rate: DF.Currency
|
||||
warehouse: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -1764,8 +1764,8 @@ class AccountsController(TransactionBase):
|
||||
item_allowance = {}
|
||||
global_qty_allowance, global_amount_allowance = None, None
|
||||
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value(
|
||||
"Accounts Settings", "role_allowed_to_over_bill"
|
||||
role_allowed_to_over_bill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
user_roles = frappe.get_roles()
|
||||
|
||||
|
||||
@@ -554,6 +554,7 @@ class StatusUpdater(Document):
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_allowance_for(
|
||||
item_code,
|
||||
item_allowance=None,
|
||||
@@ -583,20 +584,20 @@ def get_allowance_for(
|
||||
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"]
|
||||
)
|
||||
|
||||
if qty_or_amount == "qty" and not qty_allowance:
|
||||
if global_qty_allowance is None:
|
||||
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
|
||||
elif qty_or_amount == "amount" and not over_billing_allowance:
|
||||
if global_amount_allowance is None:
|
||||
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
|
||||
|
||||
|
||||
@@ -442,6 +442,7 @@ scheduler_events = {
|
||||
"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.run_ledger_health_checks",
|
||||
"erpnext.assets.doctype.asset.asset_maintenance_log.update_asset_maintenance_log_status",
|
||||
],
|
||||
"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));
|
||||
refresh_field("stock_qty", item.name, item.parentfield);
|
||||
this.toggle_conversion_factor(item);
|
||||
this.frm.events.update_cost(this.frm);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
"column_break_3",
|
||||
"production_capacity",
|
||||
"warehouse",
|
||||
"production_capacity_section",
|
||||
"parts_per_hour",
|
||||
"workstation_status_tab",
|
||||
"status",
|
||||
"column_break_glcv",
|
||||
@@ -210,16 +208,6 @@
|
||||
"label": "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",
|
||||
"fieldtype": "Float",
|
||||
@@ -252,7 +240,7 @@
|
||||
"idx": 1,
|
||||
"image_field": "on_status_image",
|
||||
"links": [],
|
||||
"modified": "2023-11-30 12:43:35.808845",
|
||||
"modified": "2024-06-20 14:17:13.806609",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Workstation",
|
||||
@@ -277,4 +265,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ class Workstation(Document):
|
||||
hour_rate_electricity: DF.Currency
|
||||
hour_rate_labour: 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
|
||||
working_hours: DF.Table[WorkstationWorkingHour]
|
||||
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.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.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
|
||||
|
||||
@@ -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)
|
||||
total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations or 0
|
||||
|
||||
for je in depr_schedule:
|
||||
if je.journal_entry:
|
||||
total_number_of_booked_depreciations += 1
|
||||
if depr_schedule:
|
||||
for je in depr_schedule:
|
||||
if je.journal_entry:
|
||||
total_number_of_booked_depreciations += 1
|
||||
frappe.db.set_value(
|
||||
"Asset Finance Book",
|
||||
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):
|
||||
self.check_modified_date()
|
||||
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.notify_update()
|
||||
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.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.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
||||
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
|
||||
|
||||
|
||||
class TestSalesOrder(FrappeTestCase):
|
||||
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
@@ -49,6 +50,9 @@ class TestSalesOrder(FrappeTestCase):
|
||||
)
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.create_customer("_Test Customer Credit")
|
||||
|
||||
def tearDown(self):
|
||||
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", "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):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Price List",
|
||||
"options": "Price List",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
|
||||
@@ -963,6 +963,7 @@ def get_available_item_locations_for_batched_item(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"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 (
|
||||
(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
|
||||
|
||||
@@ -776,6 +776,13 @@ class PurchaseReceipt(BuyingController):
|
||||
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):
|
||||
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"))
|
||||
@@ -811,7 +818,7 @@ class PurchaseReceipt(BuyingController):
|
||||
i = 1
|
||||
for tax in self.get("taxes"):
|
||||
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
|
||||
else:
|
||||
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_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():
|
||||
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",
|
||||
doc: frm.doc,
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frm.refresh();
|
||||
}
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -218,13 +218,14 @@
|
||||
"fieldname": "reposting_data_file",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Reposting Data File",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-31 12:48:57.138693",
|
||||
"modified": "2024-06-27 16:55:23.150146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Repost Item Valuation",
|
||||
@@ -276,4 +277,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ class RepostItemValuation(Document):
|
||||
def clear_attachment(self):
|
||||
if attachments := get_attachments(self.doctype, self.name):
|
||||
attachment = attachments[0]
|
||||
frappe.delete_doc("File", attachment.name)
|
||||
frappe.delete_doc("File", attachment.name, ignore_permissions=True)
|
||||
|
||||
if self.reposting_data_file:
|
||||
self.db_set("reposting_data_file", None)
|
||||
@@ -219,6 +219,7 @@ class RepostItemValuation(Document):
|
||||
self.distinct_item_and_warehouse = None
|
||||
self.items_to_be_repost = None
|
||||
self.gl_reposting_index = 0
|
||||
self.clear_attachment()
|
||||
self.db_update()
|
||||
|
||||
def deduplicate_similar_repost(self):
|
||||
@@ -271,6 +272,7 @@ def repost(doc):
|
||||
repost_gl_entries(doc)
|
||||
|
||||
doc.set_status("Completed")
|
||||
doc.db_set("reposting_data_file", None)
|
||||
remove_attached_file(doc.name)
|
||||
|
||||
except Exception as e:
|
||||
@@ -315,7 +317,7 @@ def remove_attached_file(docname):
|
||||
if file_name := frappe.db.get_value(
|
||||
"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):
|
||||
|
||||
@@ -646,6 +646,61 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
|
||||
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):
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"sales_invoice_no",
|
||||
"pick_list",
|
||||
"purchase_receipt_no",
|
||||
"asset_repair",
|
||||
"col2",
|
||||
"company",
|
||||
"posting_date",
|
||||
@@ -674,6 +675,14 @@
|
||||
{
|
||||
"fieldname": "column_break_eaoa",
|
||||
"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",
|
||||
@@ -681,7 +690,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-12 11:56:58.644882",
|
||||
"modified": "2024-06-26 19:12:17.937088",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry",
|
||||
|
||||
@@ -98,6 +98,7 @@ class StockEntry(StockController):
|
||||
address_display: DF.SmallText | None
|
||||
amended_from: DF.Link | None
|
||||
apply_putaway_rule: DF.Check
|
||||
asset_repair: DF.Link | None
|
||||
bom_no: DF.Link | None
|
||||
company: DF.Link
|
||||
credit_note: DF.Link | None
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"oldfieldtype": "Date",
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -360,7 +361,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-13 09:56:13.021696",
|
||||
"modified": "2024-06-27 16:23:18.820049",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Ledger Entry",
|
||||
@@ -384,4 +385,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"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):
|
||||
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":
|
||||
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
|
||||
|
||||
elif out.get("warehouse"):
|
||||
if doc and doc.get("doctype") == "Purchase Order":
|
||||
# calculate company_total_stock only for po
|
||||
bin_details = get_bin_details(
|
||||
args.item_code, out.warehouse, args.company, include_child_warehouses=True
|
||||
)
|
||||
else:
|
||||
bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True)
|
||||
|
||||
out.update(bin_details)
|
||||
if item.is_stock_item:
|
||||
update_bin_details(args, out, doc)
|
||||
|
||||
# update args with out, if key or value not exists
|
||||
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")))
|
||||
|
||||
|
||||
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):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
@@ -101,6 +101,12 @@ frappe.query_reports["Stock Balance"] = {
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
fieldname: "include_zero_stock_items",
|
||||
label: __("Include Zero Stock Items"),
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
|
||||
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)}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
self.data.append(report_data)
|
||||
|
||||
@@ -950,7 +950,17 @@ class SerialBatchCreation:
|
||||
if self.get("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)
|
||||
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
|
||||
self.batches = get_available_batches(kwargs)
|
||||
|
||||
@@ -68,8 +68,6 @@ def get_stock_value_on(
|
||||
frappe.qb.from_(sle)
|
||||
.select(IfNull(Sum(sle.stock_value_difference), 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:
|
||||
|
||||
@@ -426,6 +426,12 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
)
|
||||
|
||||
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"):
|
||||
precision = item.precision("consumed_qty")
|
||||
if (
|
||||
|
||||
@@ -81,6 +81,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
|
||||
|
||||
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 Home Desktop 100",
|
||||
@@ -125,7 +126,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
self.assertRaises(frappe.ValidationError, scr.submit)
|
||||
scr.submit()
|
||||
|
||||
def test_subcontracting_gle_fg_item_rate_zero(self):
|
||||
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
|
||||
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):
|
||||
# Set Backflush Based On as "BOM"
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
Reference in New Issue
Block a user