Merge pull request #42142 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-07-03 10:33:25 +05:30
committed by GitHub
58 changed files with 1100 additions and 415 deletions

View File

@@ -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
}
}

View File

@@ -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):

View File

@@ -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():

View File

@@ -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):

View File

@@ -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()

View File

@@ -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

View File

@@ -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": []
}

View File

@@ -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": []
}
}

View File

@@ -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))

View File

@@ -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 = {

View File

@@ -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)

View File

@@ -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 (

View File

@@ -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()

View File

@@ -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):

View File

@@ -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",

View File

@@ -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(

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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"):

View File

@@ -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):

View File

@@ -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,

View File

@@ -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",

View File

@@ -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,

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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",

View File

@@ -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);
}
}
};

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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,

View File

@@ -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, '') != ''"""
)

View File

@@ -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)

View File

@@ -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")

View File

@@ -107,7 +107,8 @@
"in_standard_filter": 1,
"label": "Price List",
"options": "Price List",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"bold": 1,

View File

@@ -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"),
}
)
)

View File

@@ -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(

View File

@@ -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

View File

@@ -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();
},
});
},

View File

@@ -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": []
}
}

View File

@@ -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):

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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": []
}
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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 (

View File

@@ -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")