mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-02 21:18:27 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into multi_pr_pi
This commit is contained in:
@@ -54,6 +54,7 @@ class BankAccount(Document):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
@@ -100,23 +101,51 @@ class BankAccount(Document):
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
frappe.db.set_value(
|
||||
"Bank Account",
|
||||
{
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"is_company_account": self.is_company_account,
|
||||
"is_default": 1,
|
||||
"disabled": 0,
|
||||
},
|
||||
"is_default",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_bank_account(doctype, docname):
|
||||
doc = frappe.new_doc("Bank Account")
|
||||
doc.party_type = doctype
|
||||
doc.party = docname
|
||||
doc.is_default = 1
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value(party_type, party, "default_bank_account")
|
||||
return frappe.db.get_value(
|
||||
"Bank Account",
|
||||
{"party_type": party_type, "party": party, "is_default": 1, "disabled": 0},
|
||||
"name",
|
||||
)
|
||||
|
||||
|
||||
def get_default_company_bank_account(company):
|
||||
return frappe.db.get_value("Bank Account", {"company": company, "is_company_account": 1, "is_default": 1})
|
||||
def get_default_company_bank_account(company, party_type, party):
|
||||
default_company_bank_account = frappe.db.get_value(party_type, party, "default_bank_account")
|
||||
if default_company_bank_account:
|
||||
if company != frappe.get_cached_value("Bank Account", default_company_bank_account, "company"):
|
||||
default_company_bank_account = None
|
||||
|
||||
if not default_company_bank_account:
|
||||
default_company_bank_account = frappe.db.get_value(
|
||||
"Bank Account", {"company": company, "is_company_account": 1, "is_default": 1}
|
||||
)
|
||||
|
||||
return default_company_bank_account
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -146,6 +146,10 @@ class Dunning(AccountsController):
|
||||
)
|
||||
row.dunning_level = len(past_dunnings) + 1
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
|
||||
@@ -182,6 +182,7 @@ class GLEntry(Document):
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
@@ -195,6 +196,7 @@ class GLEntry(Document):
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
|
||||
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Ledger Health", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2024-03-26 17:01:47.443986",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"checked_on",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "debit_credit_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Debit-Credit mismatch"
|
||||
},
|
||||
{
|
||||
"fieldname": "checked_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Checked On"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "general_and_payment_ledger_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "General and Payment Ledger mismatch"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-09 11:16:07.044484",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealth(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
checked_on: DF.Datetime | None
|
||||
debit_credit_mismatch: DF.Check
|
||||
general_and_payment_ledger_mismatch: DF.Check
|
||||
name: DF.Int | None
|
||||
voucher_no: DF.Data | None
|
||||
voucher_type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import run_ledger_health_checks
|
||||
|
||||
|
||||
class TestLedgerHealth(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.configure_monitoring_tool()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def configure_monitoring_tool(self):
|
||||
monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
monitor_settings.enable_health_monitor = True
|
||||
monitor_settings.enable_for_last_x_days = 60
|
||||
monitor_settings.debit_credit_mismatch = True
|
||||
monitor_settings.general_and_payment_ledger_mismatch = True
|
||||
exists = [x for x in monitor_settings.companies if x.company == self.company]
|
||||
if not exists:
|
||||
monitor_settings.append("companies", {"company": self.company})
|
||||
monitor_settings.save()
|
||||
|
||||
def clear_old_entries(self):
|
||||
super().clear_old_entries()
|
||||
lh = qb.DocType("Ledger Health")
|
||||
qb.from_(lh).delete().run()
|
||||
|
||||
def create_journal(self):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = self.company
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.posting_date = nowdate()
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 10000,
|
||||
},
|
||||
)
|
||||
je.append("accounts", {"account": self.income_account, "credit_in_account_currency": 10000})
|
||||
je.save().submit()
|
||||
self.je = je
|
||||
|
||||
def test_debit_credit_mismatch(self):
|
||||
self.create_journal()
|
||||
|
||||
# manually cause debit-credit mismatch
|
||||
gle = frappe.db.get_all(
|
||||
"GL Entry", filters={"voucher_no": self.je.name, "account": self.income_account}
|
||||
)[0]
|
||||
frappe.db.set_value("GL Entry", gle.name, "credit", 8000)
|
||||
|
||||
run_ledger_health_checks()
|
||||
expected = {
|
||||
"voucher_type": self.je.doctype,
|
||||
"voucher_no": self.je.name,
|
||||
"debit_credit_mismatch": True,
|
||||
"general_and_payment_ledger_mismatch": False,
|
||||
}
|
||||
actual = frappe.db.get_all(
|
||||
"Ledger Health",
|
||||
fields=[
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual[0])
|
||||
|
||||
def test_gl_and_pl_mismatch(self):
|
||||
self.create_journal()
|
||||
|
||||
# manually cause GL and PL discrepancy
|
||||
ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": self.je.name})[0]
|
||||
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", 11000)
|
||||
|
||||
run_ledger_health_checks()
|
||||
expected = {
|
||||
"voucher_type": self.je.doctype,
|
||||
"voucher_no": self.je.name,
|
||||
"debit_credit_mismatch": False,
|
||||
"general_and_payment_ledger_mismatch": True,
|
||||
}
|
||||
actual = frappe.db.get_all(
|
||||
"Ledger Health",
|
||||
fields=[
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual[0])
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Ledger Health Monitor", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-03-27 09:38:07.427997",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable_health_monitor",
|
||||
"monitor_section",
|
||||
"monitor_for_last_x_days",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
"section_break_xdsp",
|
||||
"companies"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_health_monitor",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Health Monitor"
|
||||
},
|
||||
{
|
||||
"fieldname": "monitor_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Configuration"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "debit_credit_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Debit-Credit Mismatch"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "general_and_payment_ledger_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Discrepancy between General and Payment Ledger"
|
||||
},
|
||||
{
|
||||
"default": "60",
|
||||
"fieldname": "monitor_for_last_x_days",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Monitor for Last 'X' days",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xdsp",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Companies"
|
||||
},
|
||||
{
|
||||
"fieldname": "companies",
|
||||
"fieldtype": "Table",
|
||||
"options": "Ledger Health Monitor Company"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 10:14:16.511681",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health Monitor",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealthMonitor(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.ledger_health_monitor_company.ledger_health_monitor_company import (
|
||||
LedgerHealthMonitorCompany,
|
||||
)
|
||||
|
||||
companies: DF.Table[LedgerHealthMonitorCompany]
|
||||
debit_credit_mismatch: DF.Check
|
||||
enable_health_monitor: DF.Check
|
||||
general_and_payment_ledger_mismatch: DF.Check
|
||||
monitor_for_last_x_days: DF.Int
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLedgerHealthMonitor(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-03-27 10:04:45.727054",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 10:06:22.806155",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health Monitor Company",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealthMonitorCompany(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -1665,6 +1665,8 @@ frappe.ui.form.on("Payment Entry Reference", {
|
||||
frm.doc.payment_type == "Receive"
|
||||
? frm.doc.paid_from_account_currency
|
||||
: frm.doc.paid_to_account_currency,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
|
||||
@@ -188,6 +188,7 @@ class PaymentEntry(AccountsController):
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
self.update_payment_schedule()
|
||||
self.set_payment_req_status()
|
||||
self.set_status()
|
||||
|
||||
def set_liability_account(self):
|
||||
@@ -484,7 +485,7 @@ class PaymentEntry(AccountsController):
|
||||
self,
|
||||
force: bool = False,
|
||||
update_ref_details_only_for: list | None = None,
|
||||
ref_exchange_rate: float | None = None,
|
||||
reference_exchange_details: dict | None = None,
|
||||
) -> None:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
@@ -495,12 +496,20 @@ class PaymentEntry(AccountsController):
|
||||
continue
|
||||
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype, d.reference_name, self.party_account_currency
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
self.party_account_currency,
|
||||
self.party_type,
|
||||
self.party,
|
||||
)
|
||||
|
||||
# Only update exchange rate when the reference is Journal Entry
|
||||
if ref_exchange_rate and d.reference_doctype == "Journal Entry":
|
||||
ref_details.update({"exchange_rate": ref_exchange_rate})
|
||||
if (
|
||||
reference_exchange_details
|
||||
and d.reference_doctype == reference_exchange_details.reference_doctype
|
||||
and d.reference_name == reference_exchange_details.reference_name
|
||||
):
|
||||
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
|
||||
|
||||
for field, value in ref_details.items():
|
||||
if d.exchange_gain_loss:
|
||||
@@ -1120,9 +1129,11 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
remarks = [
|
||||
_("Amount {0} {1} {2} {3}").format(
|
||||
_(self.party_account_currency),
|
||||
_(self.paid_to_account_currency)
|
||||
if self.payment_type == "Receive"
|
||||
else _(self.paid_from_account_currency),
|
||||
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
|
||||
_("received from") if self.payment_type == "Receive" else _("to"),
|
||||
_("received from") if self.payment_type == "Receive" else _("paid to"),
|
||||
self.party,
|
||||
)
|
||||
]
|
||||
@@ -1311,6 +1322,18 @@ class PaymentEntry(AccountsController):
|
||||
):
|
||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||
|
||||
def get_dr_and_account_for_advances(self, reference):
|
||||
if reference.reference_doctype == "Sales Invoice":
|
||||
return "credit", reference.account
|
||||
|
||||
if reference.reference_doctype == "Payment Entry":
|
||||
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
|
||||
|
||||
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
||||
args_dict = {
|
||||
"party_type": self.party_type,
|
||||
@@ -1330,8 +1353,8 @@ class PaymentEntry(AccountsController):
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
|
||||
dr_or_cr = "credit" if invoice.reference_doctype in ["Sales Invoice", "Payment Entry"] else "debit"
|
||||
args_dict["account"] = invoice.account
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
args_dict["account"] = account
|
||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict.update(
|
||||
@@ -2115,8 +2138,7 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
|
||||
bank_account = get_default_company_bank_account(company)
|
||||
|
||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||
return {
|
||||
"party_account": party_account,
|
||||
"party_name": party_name,
|
||||
@@ -2157,49 +2179,75 @@ def get_company_defaults(company):
|
||||
return frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||
|
||||
|
||||
def get_outstanding_on_journal_entry(name):
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
res = (
|
||||
frappe.qb.from_(gl)
|
||||
.select(
|
||||
Case()
|
||||
.when(
|
||||
gl.party_type == "Customer",
|
||||
Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0),
|
||||
)
|
||||
.else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0))
|
||||
.as_("outstanding_amount")
|
||||
)
|
||||
def get_outstanding_on_journal_entry(voucher_no, party_type, party):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
|
||||
outstanding = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(Sum(ple.amount_in_account_currency))
|
||||
.where(
|
||||
(Coalesce(gl.party_type, "") != "")
|
||||
& (gl.is_cancelled == 0)
|
||||
& ((gl.voucher_no == name) | (gl.against_voucher == name))
|
||||
(ple.against_voucher_no == voucher_no)
|
||||
& (ple.party_type == party_type)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
).run()
|
||||
|
||||
outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0
|
||||
outstanding_amount = outstanding[0][0] if outstanding else 0
|
||||
|
||||
return outstanding_amount
|
||||
total = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(Sum(ple.amount_in_account_currency))
|
||||
.where(
|
||||
(ple.voucher_no == voucher_no)
|
||||
& (ple.party_type == party_type)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
).run()
|
||||
|
||||
total_amount = total[0][0] if total else 0
|
||||
|
||||
return outstanding_amount, total_amount
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_reference_details(reference_doctype, reference_name, party_account_currency):
|
||||
def get_reference_details(
|
||||
reference_doctype, reference_name, party_account_currency, party_type=None, party=None
|
||||
):
|
||||
total_amount = outstanding_amount = exchange_rate = account = None
|
||||
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
||||
|
||||
# Only applies for Reverse Payment Entries
|
||||
account_type = None
|
||||
payment_type = None
|
||||
|
||||
if reference_doctype == "Dunning":
|
||||
total_amount = outstanding_amount = ref_doc.get("dunning_amount")
|
||||
exchange_rate = 1
|
||||
|
||||
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
||||
total_amount = ref_doc.get("total_amount")
|
||||
if ref_doc.multi_currency:
|
||||
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
|
||||
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
|
||||
reference_name, party_type, party
|
||||
)
|
||||
|
||||
elif reference_doctype == "Payment Entry":
|
||||
if reverse_payment_details := frappe.db.get_all(
|
||||
"Payment Entry",
|
||||
filters={"name": reference_name},
|
||||
fields=["payment_type", "party_type"],
|
||||
)[0]:
|
||||
payment_type = reverse_payment_details.payment_type
|
||||
account_type = frappe.db.get_value(
|
||||
"Party Type", reverse_payment_details.party_type, "account_type"
|
||||
)
|
||||
exchange_rate = 1
|
||||
|
||||
elif reference_doctype != "Journal Entry":
|
||||
if not total_amount:
|
||||
@@ -2245,6 +2293,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
"outstanding_amount": flt(outstanding_amount),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"bill_no": ref_doc.get("bill_no"),
|
||||
"account_type": account_type,
|
||||
"payment_type": payment_type,
|
||||
}
|
||||
)
|
||||
if account:
|
||||
|
||||
@@ -1074,9 +1074,13 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.source_exchange_rate = 50
|
||||
pe.save()
|
||||
|
||||
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
||||
ref_details = get_reference_details(
|
||||
so.doctype, so.name, pe.paid_from_account_currency, "Customer", so.customer
|
||||
)
|
||||
expected_response = {
|
||||
"account": get_party_account("Customer", so.customer, so.company),
|
||||
"account_type": None, # only applies for Reverse Payment Entry
|
||||
"payment_type": None, # only applies for Reverse Payment Entry
|
||||
"total_amount": 5000.0,
|
||||
"outstanding_amount": 5000.0,
|
||||
"exchange_rate": 1.0,
|
||||
@@ -1543,7 +1547,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
company = "_Test Company"
|
||||
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
||||
advance_account = create_account(
|
||||
parent_account="Current Assets - _TC",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Received",
|
||||
company=company,
|
||||
account_type="Receivable",
|
||||
@@ -1599,9 +1603,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
# assert General and Payment Ledger entries post partial reconciliation
|
||||
self.expected_gle = [
|
||||
{"account": "Debtors - _TC", "debit": 0.0, "credit": 400.0},
|
||||
{"account": advance_account, "debit": 400.0, "credit": 0.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 400.0},
|
||||
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
||||
]
|
||||
self.expected_ple = [
|
||||
@@ -1612,7 +1616,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
"amount": -1000.0,
|
||||
},
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": reverse_pe.name,
|
||||
"amount": -400.0,
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"due_date",
|
||||
"bill_no",
|
||||
"payment_term",
|
||||
"account_type",
|
||||
"payment_type",
|
||||
"column_break_4",
|
||||
"total_amount",
|
||||
"outstanding_amount",
|
||||
@@ -108,12 +110,22 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment Type"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:09.578312",
|
||||
"modified": "2024-04-05 09:44:08.310593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -15,6 +15,7 @@ class PaymentEntryReference(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
account: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
allocated_amount: DF.Float
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
@@ -25,6 +26,7 @@ class PaymentEntryReference(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
payment_term: DF.Link | None
|
||||
payment_type: DF.Data | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Float
|
||||
|
||||
@@ -5,6 +5,8 @@ import unittest
|
||||
|
||||
# test_records = frappe.get_test_records('Payment Gateway Account')
|
||||
|
||||
test_ignore = ["Payment Gateway"]
|
||||
|
||||
|
||||
class TestPaymentGatewayAccount(unittest.TestCase):
|
||||
pass
|
||||
|
||||
@@ -101,6 +101,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
"account_currency": "USD",
|
||||
"account_type": "Payable",
|
||||
},
|
||||
# 'Payable' account for capturing advance paid, under 'Assets' group
|
||||
{
|
||||
"attribute": "advance_payable_account",
|
||||
"account_name": "Advance Paid",
|
||||
"parent_account": "Current Assets - _PR",
|
||||
"account_currency": "INR",
|
||||
"account_type": "Payable",
|
||||
},
|
||||
]
|
||||
|
||||
for x in accounts:
|
||||
@@ -1335,6 +1343,188 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
def test_reverse_payment_against_payment_for_supplier(self):
|
||||
"""
|
||||
Reconcile a payment against a reverse payment, for a supplier.
|
||||
"""
|
||||
self.supplier = "_Test Supplier"
|
||||
amount = 4000
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.creditors
|
||||
pe.save().submit()
|
||||
|
||||
reverse_pe = self.create_payment_entry(amount=amount)
|
||||
reverse_pe.party_type = "Supplier"
|
||||
reverse_pe.party = self.supplier
|
||||
reverse_pe.payment_type = "Receive"
|
||||
reverse_pe.paid_from = self.creditors
|
||||
reverse_pe.paid_to = self.cash
|
||||
reverse_pe.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
self.assertEqual(pr.invoices[0].invoice_number, reverse_pe.name)
|
||||
self.assertEqual(pr.payments[0].reference_name, pe.name)
|
||||
|
||||
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()
|
||||
|
||||
pe.reload()
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].exchange_rate, 1)
|
||||
# There should not be any Exc Gain/Loss
|
||||
self.assertEqual(pe.references[0].exchange_gain_loss, 0)
|
||||
self.assertEqual(pe.references[0].reference_name, reverse_pe.name)
|
||||
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": ("in", [pe.name, reverse_pe.name]),
|
||||
},
|
||||
)
|
||||
# There should be no Exchange Gain/Loss created
|
||||
self.assertEqual(journals, [])
|
||||
|
||||
def test_advance_reverse_payment_against_payment_for_supplier(self):
|
||||
"""
|
||||
Reconcile an Advance payment against reverse payment, for a supplier.
|
||||
"""
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
amount = 4000
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
reverse_pe = self.create_payment_entry(amount=amount)
|
||||
reverse_pe.party_type = "Supplier"
|
||||
reverse_pe.party = self.supplier
|
||||
reverse_pe.payment_type = "Receive"
|
||||
reverse_pe.paid_from = self.advance_payable_account
|
||||
reverse_pe.paid_to = self.cash
|
||||
reverse_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)
|
||||
self.assertEqual(pr.invoices[0].invoice_number, reverse_pe.name)
|
||||
self.assertEqual(pr.payments[0].reference_name, pe.name)
|
||||
|
||||
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()
|
||||
|
||||
pe.reload()
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].exchange_rate, 1)
|
||||
# There should not be any Exc Gain/Loss
|
||||
self.assertEqual(pe.references[0].exchange_gain_loss, 0)
|
||||
self.assertEqual(pe.references[0].reference_name, reverse_pe.name)
|
||||
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": ("in", [pe.name, reverse_pe.name]),
|
||||
},
|
||||
)
|
||||
# There should be no Exchange Gain/Loss created
|
||||
self.assertEqual(journals, [])
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name},
|
||||
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.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": reverse_pe.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": "Cash - _PR",
|
||||
"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.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": reverse_pe.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -149,35 +149,37 @@ class PaymentRequest(Document):
|
||||
).format(self.grand_total, amount)
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
if self.payment_request_type == "Outward":
|
||||
self.db_set("status", "Initiated")
|
||||
return
|
||||
elif self.payment_request_type == "Inward":
|
||||
self.db_set("status", "Requested")
|
||||
|
||||
send_mail = self.payment_gateway_validation() if self.payment_gateway else None
|
||||
def on_change(self):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
|
||||
if (
|
||||
hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
|
||||
) or self.flags.mute_email:
|
||||
send_mail = False
|
||||
|
||||
if send_mail and self.payment_channel != "Phone":
|
||||
self.set_payment_request_url()
|
||||
self.send_email()
|
||||
self.make_communication_entry()
|
||||
|
||||
elif self.payment_channel == "Phone":
|
||||
self.request_phone_payment()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if self.reference_doctype in advance_payment_doctypes:
|
||||
# set advance payment status
|
||||
ref_doc.set_total_advance_paid()
|
||||
ref_doc.set_advance_payment_status()
|
||||
|
||||
def on_submit(self):
|
||||
if self.payment_request_type == "Outward":
|
||||
self.db_set("status", "Initiated")
|
||||
elif self.payment_request_type == "Inward":
|
||||
self.db_set("status", "Requested")
|
||||
|
||||
if self.payment_request_type == "Inward":
|
||||
send_mail = self.payment_gateway_validation() if self.payment_gateway else None
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
|
||||
if (
|
||||
hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
|
||||
) or self.flags.mute_email:
|
||||
send_mail = False
|
||||
|
||||
if send_mail and self.payment_channel != "Phone":
|
||||
self.set_payment_request_url()
|
||||
self.send_email()
|
||||
self.make_communication_entry()
|
||||
|
||||
elif self.payment_channel == "Phone":
|
||||
self.request_phone_payment()
|
||||
|
||||
def request_phone_payment(self):
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
@@ -217,14 +219,6 @@ class PaymentRequest(Document):
|
||||
self.check_if_payment_entry_exists()
|
||||
self.set_as_cancelled()
|
||||
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if self.reference_doctype in advance_payment_doctypes:
|
||||
# set advance payment status
|
||||
ref_doc.set_total_advance_paid()
|
||||
|
||||
def make_invoice(self):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart":
|
||||
@@ -641,7 +635,11 @@ def update_payment_req_status(doc, method):
|
||||
|
||||
if payment_request_name:
|
||||
ref_details = get_reference_details(
|
||||
ref.reference_doctype, ref.reference_name, doc.party_account_currency
|
||||
ref.reference_doctype,
|
||||
ref.reference_name,
|
||||
doc.party_account_currency,
|
||||
doc.party_type,
|
||||
doc.party,
|
||||
)
|
||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
||||
status = pay_req_doc.status
|
||||
|
||||
@@ -35,10 +35,11 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:11.676098",
|
||||
"modified": "2024-04-07 11:26:42.021585",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Terms Template",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -76,6 +77,15 @@
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
|
||||
@@ -441,6 +441,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
if self.is_return:
|
||||
invoice_total = self.rounded_total or self.grand_total
|
||||
total_amount_in_payments = flt(total_amount_in_payments, self.precision("grand_total"))
|
||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
|
||||
@@ -136,7 +136,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
if (!doc.is_return && doc.docstatus == 1) {
|
||||
if (doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
||||
this.frm.add_custom_button(__("Return / Debit Note"), this.make_debit_note, __("Create"));
|
||||
this.frm.add_custom_button(
|
||||
__("Return / Debit Note"),
|
||||
this.make_debit_note.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"col_break1",
|
||||
"account_head",
|
||||
"description",
|
||||
"is_tax_withholding_account",
|
||||
"section_break_10",
|
||||
"rate",
|
||||
"accounting_dimensions_section",
|
||||
@@ -225,15 +226,23 @@
|
||||
"label": "Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:26.775139",
|
||||
"modified": "2024-04-08 19:51:36.678551",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
|
||||
@@ -33,6 +33,7 @@ class PurchaseTaxesandCharges(Document):
|
||||
description: DF.SmallText
|
||||
included_in_paid_amount: DF.Check
|
||||
included_in_print_rate: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
item_wise_tax_detail: DF.Code | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -118,7 +118,11 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
});
|
||||
|
||||
if (doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
||||
this.frm.add_custom_button(__("Return / Credit Note"), this.make_sales_return, __("Create"));
|
||||
this.frm.add_custom_button(
|
||||
__("Return / Credit Note"),
|
||||
this.make_sales_return.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
|
||||
@@ -3632,6 +3632,105 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(1, len(advances))
|
||||
self.assertEqual(advances[0].reference_name, pe.name)
|
||||
|
||||
def test_taxes_merging_from_delivery_note(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
dn1 = create_delivery_note(do_not_submit=1)
|
||||
dn1.items[0].qty = 10
|
||||
dn1.items[0].rate = 100
|
||||
dn1.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Freight and Forwarding Charges - _TC",
|
||||
"description": "movement charges",
|
||||
"tax_amount": 100,
|
||||
},
|
||||
)
|
||||
dn1.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Marketing Expenses - _TC",
|
||||
"description": "marketing",
|
||||
"tax_amount": 150,
|
||||
},
|
||||
)
|
||||
dn1.save().submit()
|
||||
|
||||
dn2 = create_delivery_note(do_not_submit=1)
|
||||
dn2.items[0].qty = 5
|
||||
dn2.items[0].rate = 100
|
||||
dn2.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Freight and Forwarding Charges - _TC",
|
||||
"description": "movement charges",
|
||||
"tax_amount": 20,
|
||||
},
|
||||
)
|
||||
dn2.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Miscellaneous Expenses - _TC",
|
||||
"description": "marketing",
|
||||
"tax_amount": 60,
|
||||
},
|
||||
)
|
||||
dn2.save().submit()
|
||||
|
||||
# si = make_sales_invoice(dn1.name)
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
si.customer = dn1.customer
|
||||
si.items.clear()
|
||||
|
||||
from frappe.model.mapper import map_docs
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
expected = [
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Freight and Forwarding Charges - _TC",
|
||||
"tax_amount": 120.0,
|
||||
"total": 1520.0,
|
||||
"base_total": 1520.0,
|
||||
},
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Marketing Expenses - _TC",
|
||||
"tax_amount": 150.0,
|
||||
"total": 1670.0,
|
||||
"base_total": 1670.0,
|
||||
},
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Miscellaneous Expenses - _TC",
|
||||
"tax_amount": 60.0,
|
||||
"total": 1610.0,
|
||||
"base_total": 1610.0,
|
||||
},
|
||||
]
|
||||
actual = [
|
||||
dict(
|
||||
charge_type=x.charge_type,
|
||||
account_head=x.account_head,
|
||||
tax_amount=x.tax_amount,
|
||||
total=x.total,
|
||||
base_total=x.base_total,
|
||||
)
|
||||
for x in si.taxes
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -146,7 +146,12 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||
|
||||
cost_center = get_cost_center(inv)
|
||||
tax_row.update({"cost_center": cost_center})
|
||||
tax_row.update(
|
||||
{
|
||||
"cost_center": cost_center,
|
||||
"is_tax_withholding_account": 1,
|
||||
}
|
||||
)
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
|
||||
@@ -55,10 +55,10 @@
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
{% } else { %}
|
||||
<td></td>
|
||||
|
||||
@@ -347,7 +347,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({})
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
@@ -359,7 +359,8 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# closing
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
data.append(acc_dict.totals.closing)
|
||||
data.append({})
|
||||
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
else:
|
||||
data += entries
|
||||
|
||||
@@ -380,6 +381,8 @@ def get_totals_dict():
|
||||
credit=0.0,
|
||||
debit_in_account_currency=0.0,
|
||||
credit_in_account_currency=0.0,
|
||||
debit_in_transaction_currency=None,
|
||||
credit_in_transaction_currency=None,
|
||||
)
|
||||
|
||||
return _dict(
|
||||
@@ -424,6 +427,10 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
data[key].debit_in_account_currency += gle.debit_in_account_currency
|
||||
data[key].credit_in_account_currency += gle.credit_in_account_currency
|
||||
|
||||
if filters.get("add_values_in_transaction_currency") and key not in ["opening", "closing", "total"]:
|
||||
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
|
||||
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
|
||||
|
||||
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
|
||||
"Receivable",
|
||||
"Payable",
|
||||
|
||||
@@ -61,10 +61,11 @@ def get_pos_entries(filters, group_by_field):
|
||||
order_by += f", p.{group_by_field}"
|
||||
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
p.posting_date, p.name as pos_invoice, p.pos_profile,
|
||||
p.posting_date, p.name as pos_invoice, p.pos_profile, p.company,
|
||||
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
|
||||
FROM
|
||||
`tabPOS Invoice` p {from_sales_invoice_payment}
|
||||
@@ -201,14 +202,14 @@ def get_columns(filters):
|
||||
"label": _("Grand Total"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Paid Amount"),
|
||||
"fieldname": "paid_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
@@ -218,6 +219,13 @@ def get_columns(filters):
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Is Return"), "fieldname": "is_return", "fieldtype": "Data", "width": 80},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
@@ -13,11 +13,13 @@ from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||
from frappe.query_builder.functions import Round, Sum
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
create_batch,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_datetime,
|
||||
get_number_format_info,
|
||||
getdate,
|
||||
now,
|
||||
@@ -720,7 +722,19 @@ def update_reference_in_payment_entry(
|
||||
payment_entry.setup_party_account_field()
|
||||
payment_entry.set_missing_values()
|
||||
if not skip_ref_details_update_for_pe:
|
||||
payment_entry.set_missing_ref_details(ref_exchange_rate=d.exchange_rate or None)
|
||||
reference_exchange_details = frappe._dict()
|
||||
if d.against_voucher_type == "Journal Entry" and d.exchange_rate:
|
||||
reference_exchange_details.update(
|
||||
{
|
||||
"reference_doctype": d.against_voucher_type,
|
||||
"reference_name": d.against_voucher,
|
||||
"exchange_rate": d.exchange_rate,
|
||||
}
|
||||
)
|
||||
payment_entry.set_missing_ref_details(
|
||||
update_ref_details_only_for=[(d.against_voucher_type, d.against_voucher)],
|
||||
reference_exchange_details=reference_exchange_details,
|
||||
)
|
||||
payment_entry.set_amounts()
|
||||
|
||||
payment_entry.make_exchange_gain_loss_journal(
|
||||
@@ -1709,6 +1723,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
)
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
ref_doc.notify_update()
|
||||
|
||||
|
||||
def delink_original_entry(pl_entry, partial_cancel=False):
|
||||
@@ -2059,3 +2074,44 @@ def create_gain_loss_journal(
|
||||
|
||||
def get_party_types_from_account_type(account_type):
|
||||
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
|
||||
|
||||
|
||||
def run_ledger_health_checks():
|
||||
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
if health_monitor_settings.enable_health_monitor:
|
||||
period_end = getdate()
|
||||
period_start = add_days(period_end, -abs(health_monitor_settings.monitor_for_last_x_days))
|
||||
|
||||
run_date = get_datetime()
|
||||
|
||||
# Debit-Credit mismatch report
|
||||
if health_monitor_settings.debit_credit_mismatch:
|
||||
for x in health_monitor_settings.companies:
|
||||
filters = {"company": x.company, "from_date": period_start, "to_date": period_end}
|
||||
voucher_wise = frappe.get_doc("Report", "Voucher-wise Balance")
|
||||
res = voucher_wise.execute_script_report(filters=filters)
|
||||
for x in res[1]:
|
||||
doc = frappe.new_doc("Ledger Health")
|
||||
doc.voucher_type = x.voucher_type
|
||||
doc.voucher_no = x.voucher_no
|
||||
doc.debit_credit_mismatch = True
|
||||
doc.checked_on = run_date
|
||||
doc.save()
|
||||
|
||||
# General Ledger and Payment Ledger discrepancy
|
||||
if health_monitor_settings.general_and_payment_ledger_mismatch:
|
||||
for x in health_monitor_settings.companies:
|
||||
filters = {
|
||||
"company": x.company,
|
||||
"period_start_date": period_start,
|
||||
"period_end_date": period_end,
|
||||
}
|
||||
gl_pl_comparison = frappe.get_doc("Report", "General and Payment Ledger Comparison")
|
||||
res = gl_pl_comparison.execute_script_report(filters=filters)
|
||||
for x in res[1]:
|
||||
doc = frappe.new_doc("Ledger Health")
|
||||
doc.voucher_type = x.voucher_type
|
||||
doc.voucher_no = x.voucher_no
|
||||
doc.general_and_payment_ledger_mismatch = True
|
||||
doc.checked_on = run_date
|
||||
doc.save()
|
||||
|
||||
@@ -232,7 +232,11 @@ class TestAsset(AssetSetup):
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
asset.finance_books[0],
|
||||
9000,
|
||||
get_last_day(add_months(purchase_date, 1)),
|
||||
date,
|
||||
original_schedule_date=get_last_day(nowdate()),
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
self.assertEqual(
|
||||
@@ -314,7 +318,11 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||
|
||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
asset.finance_books[0],
|
||||
9000,
|
||||
get_last_day(add_months(purchase_date, 1)),
|
||||
date,
|
||||
original_schedule_date=get_last_day(nowdate()),
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
|
||||
@@ -332,7 +340,6 @@ class TestAsset(AssetSetup):
|
||||
),
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Sales Invoice", si.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
@@ -378,7 +385,7 @@ class TestAsset(AssetSetup):
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
|
||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1737.7, 37737.7]]
|
||||
|
||||
second_asset_depr_schedule = get_depr_schedule(asset.name, "Active")
|
||||
|
||||
@@ -391,7 +398,7 @@ class TestAsset(AssetSetup):
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
37742.47,
|
||||
37737.7,
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
@@ -402,7 +409,7 @@ class TestAsset(AssetSetup):
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
0.0,
|
||||
17742.47,
|
||||
17737.7,
|
||||
),
|
||||
("Debtors - _TC", 40000.0, 0.0),
|
||||
)
|
||||
@@ -707,25 +714,24 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2023-01-31", 1021.98, 1021.98],
|
||||
["2023-02-28", 923.08, 1945.06],
|
||||
["2023-03-31", 1021.98, 2967.04],
|
||||
["2023-04-30", 989.01, 3956.05],
|
||||
["2023-05-31", 1021.98, 4978.03],
|
||||
["2023-06-30", 989.01, 5967.04],
|
||||
["2023-07-31", 1021.98, 6989.02],
|
||||
["2023-08-31", 1021.98, 8011.0],
|
||||
["2023-09-30", 989.01, 9000.01],
|
||||
["2023-10-31", 1021.98, 10021.99],
|
||||
["2023-11-30", 989.01, 11011.0],
|
||||
["2023-12-31", 989.0, 12000.0],
|
||||
["2023-01-31", 1019.18, 1019.18],
|
||||
["2023-02-28", 920.55, 1939.73],
|
||||
["2023-03-31", 1019.18, 2958.91],
|
||||
["2023-04-30", 986.3, 3945.21],
|
||||
["2023-05-31", 1019.18, 4964.39],
|
||||
["2023-06-30", 986.3, 5950.69],
|
||||
["2023-07-31", 1019.18, 6969.87],
|
||||
["2023-08-31", 1019.18, 7989.05],
|
||||
["2023-09-30", 986.3, 8975.35],
|
||||
["2023-10-31", 1019.18, 9994.53],
|
||||
["2023-11-30", 986.3, 10980.83],
|
||||
["2023-12-31", 1019.17, 12000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_for_straight_line_method_for_existing_asset(self):
|
||||
|
||||
@@ -313,7 +313,6 @@ class AssetDepreciationSchedule(Document):
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
number_of_pending_depreciations,
|
||||
)
|
||||
|
||||
if not has_pro_rata or (
|
||||
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
||||
):
|
||||
@@ -338,6 +337,7 @@ class AssetDepreciationSchedule(Document):
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
date_of_disposal,
|
||||
original_schedule_date=schedule_date,
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
@@ -565,23 +565,27 @@ def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=Fa
|
||||
)
|
||||
|
||||
|
||||
def _get_pro_rata_amt(row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False):
|
||||
def _get_pro_rata_amt(
|
||||
row,
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
to_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
original_schedule_date=None,
|
||||
):
|
||||
days = date_diff(to_date, from_date)
|
||||
months = month_diff(to_date, from_date)
|
||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||
total_days = get_total_days(to_date, 12)
|
||||
total_days = get_total_days(original_schedule_date or to_date, 12)
|
||||
else:
|
||||
total_days = get_total_days(to_date, row.frequency_of_depreciation)
|
||||
|
||||
total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation)
|
||||
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
||||
|
||||
|
||||
def get_total_days(date, frequency):
|
||||
period_start_date = add_months(date, cint(frequency) * -1)
|
||||
|
||||
if is_last_day_of_the_month(date):
|
||||
period_start_date = get_last_day(period_start_date)
|
||||
|
||||
return date_diff(date, period_start_date)
|
||||
|
||||
|
||||
@@ -632,33 +636,37 @@ def get_straight_line_or_manual_depr_amount(
|
||||
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
|
||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
if row.daily_prorata_based:
|
||||
daily_depr_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(
|
||||
row.total_number_of_depreciations
|
||||
- asset.number_of_depreciations_booked
|
||||
- number_of_pending_depreciations
|
||||
- 1
|
||||
)
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
1,
|
||||
),
|
||||
add_days(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(
|
||||
row.total_number_of_depreciations
|
||||
- asset.number_of_depreciations_booked
|
||||
- number_of_pending_depreciations
|
||||
- 1
|
||||
)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
|
||||
daily_depr_amount = amount / total_days
|
||||
|
||||
to_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
)
|
||||
@@ -679,24 +687,33 @@ def get_straight_line_or_manual_depr_amount(
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
if row.daily_prorata_based:
|
||||
daily_depr_amount = (
|
||||
amount = (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)),
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(
|
||||
add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
|
||||
daily_depr_amount = amount / total_days
|
||||
|
||||
to_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
)
|
||||
@@ -708,7 +725,6 @@ def get_straight_line_or_manual_depr_amount(
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
||||
else:
|
||||
return (
|
||||
@@ -980,32 +996,35 @@ def get_depr_schedule(asset_name, status, finance_book=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
|
||||
asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
||||
asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
||||
|
||||
if not asset_depr_schedule_name:
|
||||
if not asset_depr_schedule:
|
||||
return
|
||||
|
||||
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
|
||||
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule[0].name)
|
||||
|
||||
return asset_depr_schedule_doc
|
||||
|
||||
|
||||
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
|
||||
if finance_book is None:
|
||||
finance_book_filter = ["finance_book", "is", "not set"]
|
||||
else:
|
||||
finance_book_filter = ["finance_book", "=", finance_book]
|
||||
|
||||
if isinstance(status, str):
|
||||
status = [status]
|
||||
|
||||
return frappe.db.get_value(
|
||||
filters = [
|
||||
["asset", "=", asset_name],
|
||||
["status", "in", status],
|
||||
["docstatus", "<", 2],
|
||||
]
|
||||
|
||||
if finance_book:
|
||||
filters.append(["finance_book", "=", finance_book])
|
||||
else:
|
||||
filters.append(["finance_book", "is", "not set"])
|
||||
|
||||
return frappe.get_all(
|
||||
doctype="Asset Depreciation Schedule",
|
||||
filters=[
|
||||
["asset", "=", asset_name],
|
||||
finance_book_filter,
|
||||
["status", "in", status],
|
||||
],
|
||||
filters=filters,
|
||||
limit=1,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ def get_data(filters):
|
||||
"asset_category": asset.asset_category,
|
||||
"purchase_date": asset.purchase_date,
|
||||
"asset_value": asset_value,
|
||||
"company": asset.company,
|
||||
}
|
||||
data.append(row)
|
||||
|
||||
@@ -369,30 +370,37 @@ def get_columns(filters):
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Opening Accumulated Depreciation"),
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciated Amount"),
|
||||
"fieldname": "depreciated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Value"),
|
||||
"fieldname": "asset_value",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
return [
|
||||
@@ -423,28 +431,28 @@ def get_columns(filters):
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Value"),
|
||||
"fieldname": "asset_value",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Opening Accumulated Depreciation"),
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciated Amount"),
|
||||
"fieldname": "depreciated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
@@ -469,4 +477,11 @@ def get_columns(filters):
|
||||
"options": "Location",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1127,10 +1127,17 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
po = create_purchase_order()
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated")
|
||||
|
||||
pr = make_payment_request(dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True)
|
||||
pr = make_payment_request(
|
||||
dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True, payment_request_type="Outward"
|
||||
)
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
|
||||
|
||||
pe = get_payment_entry(po.doctype, po.name).save().submit()
|
||||
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid")
|
||||
|
||||
pe.reload()
|
||||
|
||||
@@ -22,9 +22,13 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
this.frm.set_value("valid_till", frappe.datetime.add_months(this.frm.doc.transaction_date, 1));
|
||||
}
|
||||
if (this.frm.doc.docstatus === 1) {
|
||||
this.frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
||||
this.frm.add_custom_button(
|
||||
__("Purchase Order"),
|
||||
this.make_purchase_order.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create"));
|
||||
} else if (this.frm.doc.docstatus === 0) {
|
||||
this.frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold, qb, throw
|
||||
@@ -1031,10 +1032,10 @@ class AccountsController(TransactionBase):
|
||||
"transaction_currency": self.get("currency") or self.company_currency,
|
||||
"transaction_exchange_rate": self.get("conversion_rate", 1),
|
||||
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
|
||||
account_currency, args, "debit"
|
||||
account_currency, gl_dict, "debit"
|
||||
),
|
||||
"credit_in_transaction_currency": self.get_value_in_transaction_currency(
|
||||
account_currency, args, "credit"
|
||||
account_currency, gl_dict, "credit"
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -1066,11 +1067,11 @@ class AccountsController(TransactionBase):
|
||||
return "Debit Note"
|
||||
return self.doctype
|
||||
|
||||
def get_value_in_transaction_currency(self, account_currency, args, field):
|
||||
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
|
||||
if account_currency == self.get("currency"):
|
||||
return args.get(field + "_in_account_currency")
|
||||
return gl_dict.get(field + "_in_account_currency")
|
||||
else:
|
||||
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
|
||||
return flt(gl_dict.get(field, 0) / self.get("conversion_rate", 1))
|
||||
|
||||
def validate_zero_qty_for_return_invoices_with_stock(self):
|
||||
rows = []
|
||||
@@ -1924,32 +1925,43 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.db_set("advance_paid", advance_paid)
|
||||
|
||||
self.set_advance_payment_status(advance_paid, order_total)
|
||||
self.set_advance_payment_status()
|
||||
|
||||
def set_advance_payment_status(self, advance_paid: float | None = None, order_total: float | None = None):
|
||||
def set_advance_payment_status(self):
|
||||
new_status = None
|
||||
# if money is paid set the paid states
|
||||
if advance_paid:
|
||||
new_status = "Partially Paid" if advance_paid < order_total else "Fully Paid"
|
||||
|
||||
if not new_status:
|
||||
prs = frappe.db.count(
|
||||
"Payment Request",
|
||||
{
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_name": self.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
)
|
||||
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
|
||||
new_status = "Requested" if prs else "Not Requested"
|
||||
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
|
||||
new_status = "Initiated" if prs else "Not Initiated"
|
||||
stati = frappe.get_list(
|
||||
"Payment Request",
|
||||
{
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_name": self.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
pluck="status",
|
||||
)
|
||||
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
|
||||
if not stati:
|
||||
new_status = "Not Requested"
|
||||
elif "Requested" in stati or "Failed" in stati:
|
||||
new_status = "Requested"
|
||||
elif "Partially Paid" in stati:
|
||||
new_status = "Partially Paid"
|
||||
elif "Paid" in stati:
|
||||
new_status = "Fully Paid"
|
||||
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
|
||||
if not stati:
|
||||
new_status = "Not Initiated"
|
||||
elif "Initiated" in stati or "Failed" in stati or "Payment Ordered" in stati:
|
||||
new_status = "Initiated"
|
||||
elif "Partially Paid" in stati:
|
||||
new_status = "Partially Paid"
|
||||
elif "Paid" in stati:
|
||||
new_status = "Fully Paid"
|
||||
|
||||
if new_status == self.advance_payment_status:
|
||||
return
|
||||
|
||||
self.db_set("advance_payment_status", new_status)
|
||||
self.db_set("advance_payment_status", new_status, update_modified=False)
|
||||
self.set_status(update=True)
|
||||
self.notify_update()
|
||||
|
||||
@@ -2078,21 +2090,26 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def group_similar_items(self):
|
||||
group_item_qty = {}
|
||||
group_item_amount = {}
|
||||
grouped_items = {}
|
||||
# to update serial number in print
|
||||
count = 0
|
||||
|
||||
fields_to_group = frappe.get_hooks("fields_for_group_similar_items")
|
||||
fields_to_group = set(fields_to_group)
|
||||
|
||||
for item in self.items:
|
||||
group_item_qty[item.item_code] = group_item_qty.get(item.item_code, 0) + item.qty
|
||||
group_item_amount[item.item_code] = group_item_amount.get(item.item_code, 0) + item.amount
|
||||
item_values = grouped_items.setdefault(item.item_code, defaultdict(int))
|
||||
|
||||
for field in fields_to_group:
|
||||
item_values[field] += item.get(field, 0)
|
||||
|
||||
duplicate_list = []
|
||||
for item in self.items:
|
||||
if item.item_code in group_item_qty:
|
||||
if item.item_code in grouped_items:
|
||||
count += 1
|
||||
item.qty = group_item_qty[item.item_code]
|
||||
item.amount = group_item_amount[item.item_code]
|
||||
|
||||
for field in fields_to_group:
|
||||
item.set(field, grouped_items[item.item_code][field])
|
||||
|
||||
if item.qty:
|
||||
item.rate = flt(flt(item.amount) / flt(item.qty), item.precision("rate"))
|
||||
@@ -2100,7 +2117,7 @@ class AccountsController(TransactionBase):
|
||||
item.rate = 0
|
||||
|
||||
item.idx = count
|
||||
del group_item_qty[item.item_code]
|
||||
del grouped_items[item.item_code]
|
||||
else:
|
||||
duplicate_list.append(item)
|
||||
for item in duplicate_list:
|
||||
@@ -3510,6 +3527,37 @@ def check_if_child_table_updated(child_table_before_update, child_table_after_up
|
||||
return False
|
||||
|
||||
|
||||
def merge_taxes(source_taxes, target_doc):
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
update_item_wise_tax_detail,
|
||||
)
|
||||
|
||||
existing_taxes = target_doc.get("taxes") or []
|
||||
idx = 1
|
||||
for tax in source_taxes:
|
||||
found = False
|
||||
for t in existing_taxes:
|
||||
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
||||
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
||||
update_item_wise_tax_detail(t, tax)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
tax.charge_type = "Actual"
|
||||
tax.idx = idx
|
||||
idx += 1
|
||||
tax.included_in_print_rate = 0
|
||||
tax.dont_recompute_tax = 1
|
||||
tax.row_id = ""
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
||||
tax.item_wise_tax_detail = tax.item_wise_tax_detail
|
||||
existing_taxes.append(tax)
|
||||
|
||||
target_doc.set("taxes", existing_taxes)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def validate_regional(doc):
|
||||
pass
|
||||
|
||||
@@ -441,7 +441,7 @@ class SellingController(StockController):
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||
|
||||
if (
|
||||
not d.incoming_rate
|
||||
|
||||
@@ -166,7 +166,7 @@ class StockController(AccountsController):
|
||||
# remove extra whitespace and store one serial no on each line
|
||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||
|
||||
def make_bundle_using_old_serial_batch_fields(self, table_name=None):
|
||||
def make_bundle_using_old_serial_batch_fields(self, table_name=None, via_landed_cost_voucher=False):
|
||||
if self.get("_action") == "update_after_submit":
|
||||
return
|
||||
|
||||
@@ -205,7 +205,7 @@ class StockController(AccountsController):
|
||||
"company": self.company,
|
||||
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
||||
"do_not_submit": True,
|
||||
"do_not_submit": True if not via_landed_cost_voucher else False,
|
||||
}
|
||||
|
||||
if row.get("qty") or row.get("consumed_qty"):
|
||||
@@ -1119,7 +1119,7 @@ class StockController(AccountsController):
|
||||
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
||||
return message
|
||||
|
||||
def repost_future_sle_and_gle(self, force=False):
|
||||
def repost_future_sle_and_gle(self, force=False, via_landed_cost_voucher=False):
|
||||
args = frappe._dict(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
@@ -1127,6 +1127,7 @@ class StockController(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"company": self.company,
|
||||
"via_landed_cost_voucher": via_landed_cost_voucher,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1138,7 +1139,11 @@ class StockController(AccountsController):
|
||||
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
|
||||
)
|
||||
if item_based_reposting:
|
||||
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
create_item_wise_repost_entries(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.name,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
else:
|
||||
create_repost_item_valuation_entry(args)
|
||||
|
||||
@@ -1510,11 +1515,14 @@ def create_repost_item_valuation_entry(args):
|
||||
repost_entry.allow_zero_rate = args.allow_zero_rate
|
||||
repost_entry.flags.ignore_links = True
|
||||
repost_entry.flags.ignore_permissions = True
|
||||
repost_entry.via_landed_cost_voucher = args.via_landed_cost_voucher
|
||||
repost_entry.save()
|
||||
repost_entry.submit()
|
||||
|
||||
|
||||
def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False):
|
||||
def create_item_wise_repost_entries(
|
||||
voucher_type, voucher_no, allow_zero_rate=False, via_landed_cost_voucher=False
|
||||
):
|
||||
"""Using a voucher create repost item valuation records for all item-warehouse pairs."""
|
||||
|
||||
stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
|
||||
@@ -1538,6 +1546,7 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
|
||||
repost_entry.allow_zero_rate = allow_zero_rate
|
||||
repost_entry.flags.ignore_links = True
|
||||
repost_entry.flags.ignore_permissions = True
|
||||
repost_entry.via_landed_cost_voucher = via_landed_cost_voucher
|
||||
repost_entry.submit()
|
||||
repost_entries.append(repost_entry)
|
||||
|
||||
|
||||
@@ -467,7 +467,16 @@ class calculate_taxes_and_totals:
|
||||
if tax.charge_type == "Actual":
|
||||
# distribute the tax amount proportionally to each item row
|
||||
actual = flt(tax.tax_amount, tax.precision("tax_amount"))
|
||||
current_tax_amount = item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
|
||||
|
||||
if tax.get("is_tax_withholding_account") and item.meta.get_field("apply_tds"):
|
||||
if not item.get("apply_tds") or not self.doc.tax_withholding_net_total:
|
||||
current_tax_amount = 0.0
|
||||
else:
|
||||
current_tax_amount = item.net_amount * actual / self.doc.tax_withholding_net_total
|
||||
else:
|
||||
current_tax_amount = (
|
||||
item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
|
||||
)
|
||||
|
||||
elif tax.charge_type == "On Net Total":
|
||||
current_tax_amount = (tax_rate / 100.0) * item.net_amount
|
||||
@@ -1091,6 +1100,11 @@ def get_rounded_tax_amount(itemised_tax, precision):
|
||||
row["tax_amount"] = flt(row["tax_amount"], precision)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_rounding_tax_settings():
|
||||
return frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax")
|
||||
|
||||
|
||||
class init_landed_taxes_and_totals:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
@@ -53,7 +53,8 @@ class TestAccountsController(FrappeTestCase):
|
||||
20 series - Sales Invoice against Journals
|
||||
30 series - Sales Invoice against Credit Notes
|
||||
40 series - Company default Cost center is unset
|
||||
50 series = Journals against Journals
|
||||
50 series - Journals against Journals
|
||||
60 series - Journals against Payment Entries
|
||||
90 series - Dimension inheritence
|
||||
"""
|
||||
|
||||
@@ -1538,3 +1539,70 @@ class TestAccountsController(FrappeTestCase):
|
||||
exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name)
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_je, [])
|
||||
|
||||
def test_60_payment_entry_against_journal(self):
|
||||
# Invoices
|
||||
exc_rate1 = 75
|
||||
exc_rate2 = 77
|
||||
amount = 1
|
||||
je1 = self.create_journal_entry(
|
||||
acc1=self.debit_usd,
|
||||
acc1_exc_rate=exc_rate1,
|
||||
acc2=self.cash,
|
||||
acc1_amount=amount,
|
||||
acc2_amount=(amount * 75),
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
je1.accounts[0].party_type = "Customer"
|
||||
je1.accounts[0].party = self.customer
|
||||
je1 = je1.save().submit()
|
||||
|
||||
je2 = self.create_journal_entry(
|
||||
acc1=self.debit_usd,
|
||||
acc1_exc_rate=exc_rate2,
|
||||
acc2=self.cash,
|
||||
acc1_amount=amount,
|
||||
acc2_amount=(amount * exc_rate2),
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
je2.accounts[0].party_type = "Customer"
|
||||
je2.accounts[0].party = self.customer
|
||||
je2 = je2.save().submit()
|
||||
|
||||
# Payment
|
||||
pe = self.create_payment_entry(amount=2, source_exc_rate=exc_rate1).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.receivable_payable_account = self.debit_usd
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 2)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
# There should be no outstanding in both currencies
|
||||
self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0)
|
||||
self.assert_ledger_outstanding(je2.doctype, je2.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created only for JE2
|
||||
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
|
||||
self.assertEqual(exc_je_for_je1, [])
|
||||
self.assertEqual(len(exc_je_for_je2), 1)
|
||||
|
||||
# Cancel Payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount)
|
||||
self.assert_ledger_outstanding(je2.doctype, je2.name, (amount * exc_rate2), amount)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been cancelled
|
||||
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
|
||||
self.assertEqual(exc_je_for_je1, [])
|
||||
self.assertEqual(exc_je_for_je2, [])
|
||||
|
||||
@@ -349,7 +349,6 @@ doc_events = {
|
||||
"Payment Entry": {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
|
||||
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
||||
],
|
||||
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||
@@ -435,6 +434,7 @@ scheduler_events = {
|
||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
||||
"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",
|
||||
],
|
||||
"weekly": [
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||
@@ -534,6 +534,7 @@ accounting_dimension_doctypes = [
|
||||
"Supplier Quotation Item",
|
||||
"Payment Reconciliation",
|
||||
"Payment Reconciliation Allocation",
|
||||
"Payment Request",
|
||||
]
|
||||
|
||||
get_matching_queries = (
|
||||
@@ -635,3 +636,5 @@ default_log_clearing_doctypes = {
|
||||
}
|
||||
|
||||
export_python_type_annotations = True
|
||||
|
||||
fields_for_group_similar_items = ["qty", "amount"]
|
||||
|
||||
6664
erpnext/locale/ar.po
6664
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
83599
erpnext/locale/bs.po
Normal file
83599
erpnext/locale/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
6908
erpnext/locale/de.po
6908
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
6664
erpnext/locale/eo.po
6664
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
6812
erpnext/locale/es.po
6812
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
7095
erpnext/locale/fa.po
7095
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
6664
erpnext/locale/fr.po
6664
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6700
erpnext/locale/tr.po
6700
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
@@ -238,7 +238,7 @@
|
||||
"fieldname": "rm_cost_as_per",
|
||||
"fieldtype": "Select",
|
||||
"label": "Rate Of Materials Based On",
|
||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual"
|
||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -637,7 +637,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:40.214929",
|
||||
"modified": "2024-04-02 16:22:47.518411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -147,7 +147,7 @@ class BOM(WebsiteGenerator):
|
||||
quality_inspection_template: DF.Link | None
|
||||
quantity: DF.Float
|
||||
raw_material_cost: DF.Currency
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"]
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||
route: DF.SmallText | None
|
||||
routing: DF.Link | None
|
||||
scrap_items: DF.Table[BOMScrapItem]
|
||||
@@ -737,6 +737,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
def calculate_rm_cost(self, save=False):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
|
||||
total_rm_cost = 0
|
||||
base_total_rm_cost = 0
|
||||
|
||||
@@ -745,7 +746,7 @@ class BOM(WebsiteGenerator):
|
||||
continue
|
||||
|
||||
old_rate = d.rate
|
||||
if self.rm_cost_as_per != "Manual":
|
||||
if not self.bom_creator:
|
||||
d.rate = self.get_rm_rate(
|
||||
{
|
||||
"company": self.company,
|
||||
@@ -1017,8 +1018,6 @@ def get_bom_item_rate(args, bom_doc):
|
||||
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
|
||||
price_list_data = get_price_list_rate(bom_args, item_doc)
|
||||
rate = price_list_data.price_list_rate
|
||||
elif bom_doc.rm_cost_as_per == "Manual":
|
||||
return
|
||||
|
||||
return flt(rate)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"fieldname": "rm_cost_as_per",
|
||||
"fieldtype": "Select",
|
||||
"label": "Rate Of Materials Based On",
|
||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual",
|
||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -288,7 +288,7 @@
|
||||
"link_fieldname": "bom_creator"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-27 13:06:40.535884",
|
||||
"modified": "2024-04-02 16:30:59.779190",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Creator",
|
||||
|
||||
@@ -59,7 +59,7 @@ class BOMCreator(Document):
|
||||
qty: DF.Float
|
||||
raw_material_cost: DF.Currency
|
||||
remarks: DF.TextEditor | None
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"]
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||
set_rate_based_on_warehouse: DF.Check
|
||||
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
|
||||
uom: DF.Link | None
|
||||
@@ -141,9 +141,6 @@ class BOMCreator(Document):
|
||||
self.submit()
|
||||
|
||||
def set_rate_for_items(self):
|
||||
if self.rm_cost_as_per == "Manual":
|
||||
return
|
||||
|
||||
amount = self.get_raw_material_cost()
|
||||
self.raw_material_cost = amount
|
||||
|
||||
@@ -239,6 +236,9 @@ class BOMCreator(Document):
|
||||
frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}),
|
||||
)
|
||||
|
||||
if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)):
|
||||
frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item))
|
||||
|
||||
production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
|
||||
|
||||
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
|
||||
@@ -282,7 +282,6 @@ class BOMCreator(Document):
|
||||
"allow_alternative_item": 1,
|
||||
"bom_creator": self.name,
|
||||
"bom_creator_item": bom_creator_item,
|
||||
"rm_cost_as_per": "Manual",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@ class WorkstationDashboard {
|
||||
me.job_cards = [r.message];
|
||||
me.prepare_timer();
|
||||
me.update_job_card_details();
|
||||
me.frm.reload_doc();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -229,6 +230,7 @@ class WorkstationDashboard {
|
||||
me.job_cards = [r.message];
|
||||
me.prepare_timer();
|
||||
me.update_job_card_details();
|
||||
me.frm.reload_doc();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -357,6 +357,7 @@ erpnext.patches.v15_0.create_advance_payment_status
|
||||
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
||||
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
create_accounting_dimensions_for_doctype,
|
||||
)
|
||||
|
||||
|
||||
def execute():
|
||||
create_accounting_dimensions_for_doctype(doctype="Payment Request")
|
||||
@@ -210,10 +210,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
});
|
||||
}
|
||||
|
||||
frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax")
|
||||
.then((round_row_wise_tax) => {
|
||||
frappe.flags.round_row_wise_tax = round_row_wise_tax;
|
||||
})
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.taxes_and_totals.get_rounding_tax_settings",
|
||||
callback: function(r) {
|
||||
frappe.flags.round_off_settings = r.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
determine_exclusive_rate() {
|
||||
|
||||
@@ -252,9 +252,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
toggle_enable_for_stock_uom(field) {
|
||||
frappe.db.get_single_value('Stock Settings', field)
|
||||
.then(value => {
|
||||
this.frm.fields_dict["items"].grid.toggle_enable("stock_qty", value);
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.doctype.stock_settings.stock_settings.get_enable_stock_uom_editing',
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
var value = r.message[field];
|
||||
this.frm.fields_dict["items"].grid.toggle_enable("stock_qty", value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -452,6 +452,9 @@ $.extend(erpnext.utils, {
|
||||
},
|
||||
|
||||
get_fiscal_year: function (date, with_dates = false, boolean = false) {
|
||||
if (!frappe.boot.setup_complete) {
|
||||
return;
|
||||
}
|
||||
if (!date) {
|
||||
date = frappe.datetime.get_today();
|
||||
}
|
||||
@@ -934,7 +937,7 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
|
||||
if (opts.source_doctype) {
|
||||
let data_fields = [];
|
||||
if (opts.source_doctype == "Purchase Receipt") {
|
||||
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
|
||||
data_fields.push({
|
||||
fieldname: "merge_taxes",
|
||||
fieldtype: "Check",
|
||||
@@ -960,7 +963,10 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
return;
|
||||
}
|
||||
opts.source_name = values;
|
||||
if (opts.allow_child_item_selection || opts.source_doctype == "Purchase Receipt") {
|
||||
if (
|
||||
opts.allow_child_item_selection ||
|
||||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
|
||||
) {
|
||||
// args contains filtered child docnames
|
||||
opts.args = args;
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
)
|
||||
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
target.delivery_date = nowdate()
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.delivery_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
||||
def test_make_sales_order_with_terms(self):
|
||||
@@ -164,6 +165,7 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.delivery_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
||||
# Remove any unknown taxes if applied
|
||||
|
||||
@@ -111,16 +111,26 @@ frappe.ui.form.on("Sales Order", {
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
|
||||
if (!value) {
|
||||
// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden.
|
||||
frm.set_value("reserve_stock", 0);
|
||||
frm.set_df_property("reserve_stock", "read_only", 1);
|
||||
frm.set_df_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "default", 0);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "read_only", 1);
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.get_stock_reservation_status",
|
||||
callback: function (r) {
|
||||
if (!r.message) {
|
||||
frm.set_value("reserve_stock", 0);
|
||||
frm.set_df_property("reserve_stock", "read_only", 1);
|
||||
frm.set_df_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"reserve_stock",
|
||||
"default",
|
||||
0
|
||||
);
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"reserve_stock",
|
||||
"read_only",
|
||||
1
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -278,6 +288,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
label: __("Items to Reserve"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
@@ -346,7 +357,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
],
|
||||
primary_action_label: __("Reserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = { items: dialog.fields_dict.items.grid.data };
|
||||
var data = { items: dialog.fields_dict.items.grid.get_selected_children() };
|
||||
|
||||
if (data.items && data.items.length > 0) {
|
||||
frappe.call({
|
||||
@@ -363,9 +374,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
dialog.hide();
|
||||
} else {
|
||||
frappe.msgprint(__("Please select items to reserve."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -380,6 +393,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
|
||||
if (unreserved_qty > 0) {
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
__checked: 1,
|
||||
sales_order_item: item.name,
|
||||
item_code: item.item_code,
|
||||
warehouse: item.warehouse,
|
||||
@@ -404,6 +418,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
label: __("Reserved Stock"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
in_place_edit: true,
|
||||
data: [],
|
||||
fields: [
|
||||
@@ -447,7 +462,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
],
|
||||
primary_action_label: __("Unreserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.data };
|
||||
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() };
|
||||
|
||||
if (data.sr_entries && data.sr_entries.length > 0) {
|
||||
frappe.call({
|
||||
@@ -463,9 +478,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
dialog.hide();
|
||||
} else {
|
||||
frappe.msgprint(__("Please select items to unreserve."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1724,3 +1724,8 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
|
||||
)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_reservation_status():
|
||||
return frappe.db.get_single_value("Stock Settings", "enable_stock_reservation")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
import frappe.permissions
|
||||
@@ -1956,10 +1957,48 @@ class TestSalesOrder(FrappeTestCase):
|
||||
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
|
||||
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
|
||||
|
||||
def test_sales_order_advance_payment_status(self):
|
||||
@patch(
|
||||
# this also shadows one (1) call to _get_payment_gateway_controller
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
|
||||
return_value=None,
|
||||
)
|
||||
def test_sales_order_advance_payment_status(self, mocked_get_payment_url):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
|
||||
# Flow progressing to SI with payment entries "moved" from SO to SI
|
||||
so = make_sales_order(qty=1, rate=100, do_not_submit=True)
|
||||
# no-op; for optical consistency with how a webshop SO would look like
|
||||
so.order_type = "Shopping Cart"
|
||||
so.submit()
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
||||
|
||||
pr = make_payment_request(
|
||||
dt=so.doctype, dn=so.name, order_type="Shopping Cart", submit_doc=True, return_doc=True
|
||||
)
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
|
||||
|
||||
pe = pr.set_as_paid()
|
||||
pr.reload() # status updated
|
||||
pe.reload() # references moved to Sales Invoice
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pe.references[0].reference_doctype, "Sales Invoice")
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid")
|
||||
|
||||
pe.cancel()
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "Paid") # TODO: this might be a bug
|
||||
so.reload() # reload
|
||||
# regardless, since the references have already "handed-over" to SI,
|
||||
# the SO keeps its historical state at the time of hand over
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid")
|
||||
|
||||
pr.cancel()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||
) # TODO: this might be a bug; handover has happened
|
||||
|
||||
# Flow NOT progressing to SI with payment entries NOT "moved"
|
||||
so = make_sales_order(qty=1, rate=100)
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
||||
|
||||
@@ -1971,11 +2010,15 @@ class TestSalesOrder(FrappeTestCase):
|
||||
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested"
|
||||
) # here: reset
|
||||
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||
) # here: reset
|
||||
|
||||
def test_pick_list_without_rejected_materials(self):
|
||||
serial_and_batch_item = make_item(
|
||||
|
||||
@@ -547,6 +547,8 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
async on_cart_update(args) {
|
||||
frappe.dom.freeze();
|
||||
if (this.frm.doc.set_warehouse != this.settings.warehouse)
|
||||
this.frm.doc.set_warehouse = this.settings.warehouse;
|
||||
let item_row = undefined;
|
||||
try {
|
||||
let { field, value, item } = args;
|
||||
|
||||
@@ -142,6 +142,11 @@ def add_company_to_session_defaults():
|
||||
def add_standard_navbar_items():
|
||||
navbar_settings = frappe.get_single("Navbar Settings")
|
||||
|
||||
# Translatable strings for below navbar items
|
||||
__ = _("Documentation")
|
||||
__ = _("User Forum")
|
||||
__ = _("Report an Issue")
|
||||
|
||||
erpnext_navbar_items = [
|
||||
{
|
||||
"item_label": "Documentation",
|
||||
|
||||
@@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
|
||||
& (
|
||||
(table.from_date.between(self.from_date, self.to_date))
|
||||
| (table.to_date.between(self.from_date, self.to_date))
|
||||
| (table.from_date >= self.from_date and table.to_date >= self.to_date)
|
||||
| ((table.from_date >= self.from_date) & (table.to_date >= self.to_date))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
|
||||
|
||||
@@ -960,7 +960,7 @@ def get_returned_qty_map(delivery_note):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(source_name, target_doc=None):
|
||||
def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||
doc = frappe.get_doc("Delivery Note", source_name)
|
||||
|
||||
to_make_invoice_qty_map = {}
|
||||
@@ -974,6 +974,9 @@ def make_sales_invoice(source_name, target_doc=None):
|
||||
if len(target.get("items")) == 0:
|
||||
frappe.throw(_("All these items have already been Invoiced/Returned"))
|
||||
|
||||
if args and args.get("merge_taxes"):
|
||||
merge_taxes(source.get("taxes") or [], target)
|
||||
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
# set company address
|
||||
@@ -1038,7 +1041,11 @@ def make_sales_invoice(source_name, target_doc=None):
|
||||
if not doc.get("is_return")
|
||||
else get_pending_qty(d) > 0,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||
"Sales Taxes and Charges": {
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"add_if_empty": True,
|
||||
"ignore": args.get("merge_taxes") if args else 0,
|
||||
},
|
||||
"Sales Team": {
|
||||
"doctype": "Sales Team",
|
||||
"field_map": {"incentives": "incentives"},
|
||||
|
||||
@@ -32,7 +32,7 @@ test_ignore = ["BOM"]
|
||||
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
|
||||
|
||||
|
||||
def make_item(item_code=None, properties=None, uoms=None):
|
||||
def make_item(item_code=None, properties=None, uoms=None, barcode=None):
|
||||
if not item_code:
|
||||
item_code = frappe.generate_hash(length=16)
|
||||
|
||||
@@ -61,6 +61,14 @@ def make_item(item_code=None, properties=None, uoms=None):
|
||||
for uom in uoms:
|
||||
item.append("uoms", uom)
|
||||
|
||||
if barcode:
|
||||
item.append(
|
||||
"barcodes",
|
||||
{
|
||||
"barcode": barcode,
|
||||
},
|
||||
)
|
||||
|
||||
item.insert()
|
||||
|
||||
return item
|
||||
|
||||
@@ -250,9 +250,10 @@ class LandedCostVoucher(Document):
|
||||
|
||||
# update stock & gl entries for submit state of PR
|
||||
doc.docstatus = 1
|
||||
doc.make_bundle_using_old_serial_batch_fields(via_landed_cost_voucher=True)
|
||||
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||
doc.make_gl_entries()
|
||||
doc.repost_future_sle_and_gle()
|
||||
doc.repost_future_sle_and_gle(via_landed_cost_voucher=True)
|
||||
|
||||
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
|
||||
for item in self.get("items"):
|
||||
|
||||
@@ -596,6 +596,356 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
lcv.cancel()
|
||||
pr.cancel()
|
||||
|
||||
def test_landed_cost_voucher_with_serial_batch_for_legacy_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
sn_item = "Test Landed Cost Voucher Serial NO for Legacy PR"
|
||||
batch_item = "Test Landed Cost Voucher Batch NO for Legacy PR"
|
||||
sn_item_doc = make_item(
|
||||
sn_item,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-TLCVSNO-.####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
batch_item_doc = make_item(
|
||||
batch_item,
|
||||
{
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCH-TLCVSNO-.####",
|
||||
"create_new_batch": 1,
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
serial_nos = [
|
||||
"SN-TLCVSNO-0001",
|
||||
"SN-TLCVSNO-0002",
|
||||
"SN-TLCVSNO-0003",
|
||||
"SN-TLCVSNO-0004",
|
||||
"SN-TLCVSNO-0005",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
|
||||
if not frappe.db.exists("Batch", "BATCH-TLCVSNO-0001"):
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"item": batch_item,
|
||||
"batch_id": "BATCH-TLCVSNO-0001",
|
||||
}
|
||||
)
|
||||
batch_doc.insert()
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
item_code=sn_item,
|
||||
qty=5,
|
||||
rate=100,
|
||||
uom=sn_item_doc.stock_uom,
|
||||
stock_uom=sn_item_doc.stock_uom,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"item_name": batch_item,
|
||||
"description": "Test Batch Item",
|
||||
"uom": batch_item_doc.stock_uom,
|
||||
"stock_uom": batch_item_doc.stock_uom,
|
||||
"qty": 5,
|
||||
"rate": 100,
|
||||
"warehouse": warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 100)
|
||||
self.assertFalse(row.serial_no)
|
||||
self.assertFalse(row.batch_no)
|
||||
self.assertFalse(row.serial_and_batch_bundle)
|
||||
|
||||
if row.item_code == sn_item:
|
||||
row.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
row.db_set("batch_no", "BATCH-TLCVSNO-0001")
|
||||
|
||||
for sn in serial_nos:
|
||||
sn_doc = frappe.get_doc("Serial No", sn)
|
||||
sn_doc.db_set(
|
||||
{
|
||||
"warehouse": warehouse,
|
||||
"status": "Active",
|
||||
}
|
||||
)
|
||||
|
||||
batch_doc.db_set(
|
||||
{
|
||||
"batch_qty": 5,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
lcv = make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=20,
|
||||
distribute_charges_based_on="Qty",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
lcv.get_items_from_purchase_receipts()
|
||||
lcv.save()
|
||||
lcv.submit()
|
||||
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 102)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
lcv.cancel()
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 100)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
def test_do_not_validate_landed_cost_voucher_with_serial_batch_for_legacy_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_auto_batch_nos
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
sn_item = "Test Don't Validate Landed Cost Voucher Serial NO for Legacy PR"
|
||||
batch_item = "Test Don't Validate Landed Cost Voucher Batch NO for Legacy PR"
|
||||
sn_item_doc = make_item(
|
||||
sn_item,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-TDVLCVSNO-.####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
batch_item_doc = make_item(
|
||||
batch_item,
|
||||
{
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCH-TDVLCVSNO-.####",
|
||||
"create_new_batch": 1,
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
serial_nos = [
|
||||
"SN-TDVLCVSNO-0001",
|
||||
"SN-TDVLCVSNO-0002",
|
||||
"SN-TDVLCVSNO-0003",
|
||||
"SN-TDVLCVSNO-0004",
|
||||
"SN-TDVLCVSNO-0005",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
|
||||
if not frappe.db.exists("Batch", "BATCH-TDVLCVSNO-0001"):
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"item": batch_item,
|
||||
"batch_id": "BATCH-TDVLCVSNO-0001",
|
||||
}
|
||||
)
|
||||
batch_doc.insert()
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
item_code=sn_item,
|
||||
qty=5,
|
||||
rate=100,
|
||||
uom=sn_item_doc.stock_uom,
|
||||
stock_uom=sn_item_doc.stock_uom,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"item_name": batch_item,
|
||||
"description": "Test Batch Item",
|
||||
"uom": batch_item_doc.stock_uom,
|
||||
"stock_uom": batch_item_doc.stock_uom,
|
||||
"qty": 5,
|
||||
"rate": 100,
|
||||
"warehouse": warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
|
||||
for sn in serial_nos:
|
||||
sn_doc = frappe.get_doc("Serial No", sn)
|
||||
sn_doc.db_set(
|
||||
{
|
||||
"warehouse": warehouse,
|
||||
"status": "Active",
|
||||
}
|
||||
)
|
||||
|
||||
batch_doc.db_set(
|
||||
{
|
||||
"batch_qty": 5,
|
||||
}
|
||||
)
|
||||
|
||||
for row in pr.items:
|
||||
if row.item_code == sn_item:
|
||||
row.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
row.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name})
|
||||
for sle in stock_ledger_entries:
|
||||
doc = frappe.get_doc("Stock Ledger Entry", sle.name)
|
||||
if doc.item_code == sn_item:
|
||||
doc.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
|
||||
dn = create_delivery_note(
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
item_code=sn_item,
|
||||
qty=5,
|
||||
rate=100,
|
||||
uom=sn_item_doc.stock_uom,
|
||||
stock_uom=sn_item_doc.stock_uom,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"item_name": batch_item,
|
||||
"description": "Test Batch Item",
|
||||
"uom": batch_item_doc.stock_uom,
|
||||
"stock_uom": batch_item_doc.stock_uom,
|
||||
"qty": 5,
|
||||
"rate": 100,
|
||||
"warehouse": warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
dn.submit()
|
||||
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": dn.name})
|
||||
for sle in stock_ledger_entries:
|
||||
doc = frappe.get_doc("Stock Ledger Entry", sle.name)
|
||||
if doc.item_code == sn_item:
|
||||
doc.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
|
||||
available_batches = get_auto_batch_nos(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"warehouse": warehouse,
|
||||
"batch_no": ["BATCH-TDVLCVSNO-0001"],
|
||||
"consider_negative_batches": True,
|
||||
}
|
||||
)
|
||||
)[0]
|
||||
|
||||
self.assertFalse(available_batches.get("qty"))
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
lcv = make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=20,
|
||||
distribute_charges_based_on="Qty",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
lcv.get_items_from_purchase_receipts()
|
||||
lcv.save()
|
||||
lcv.submit()
|
||||
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 102)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
lcv.cancel()
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 100)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
|
||||
def make_landed_cost_voucher(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"parent_warehouse",
|
||||
"consider_rejected_warehouses",
|
||||
"get_item_locations",
|
||||
"pick_manually",
|
||||
"section_break_6",
|
||||
"scan_barcode",
|
||||
"column_break_13",
|
||||
@@ -192,11 +193,18 @@
|
||||
"fieldname": "consider_rejected_warehouses",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Rejected Warehouses"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled then system won't override the picked qty / batches / serial numbers.",
|
||||
"fieldname": "pick_manually",
|
||||
"fieldtype": "Check",
|
||||
"label": "Pick Manually"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:13.177072",
|
||||
"modified": "2024-03-27 22:49:16.954637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
@@ -264,7 +272,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
|
||||
@@ -12,7 +12,7 @@ from frappe.model.mapper import map_child_doc
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import GROUP_CONCAT
|
||||
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
|
||||
from frappe.utils import ceil, cint, floor, flt
|
||||
from frappe.utils import ceil, cint, floor, flt, get_link_to_form
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
@@ -23,7 +23,11 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
|
||||
get_picked_serial_nos,
|
||||
)
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
from erpnext.stock.serial_batch_bundle import (
|
||||
SerialBatchCreation,
|
||||
get_batches_from_bundle,
|
||||
get_serial_nos_from_bundle,
|
||||
)
|
||||
|
||||
# TODO: Prioritize SO or WO group warehouse
|
||||
|
||||
@@ -41,6 +45,7 @@ class PickList(Document):
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
consider_rejected_warehouses: DF.Check
|
||||
customer: DF.Link | None
|
||||
customer_name: DF.Data | None
|
||||
for_qty: DF.Float
|
||||
@@ -49,6 +54,7 @@ class PickList(Document):
|
||||
material_request: DF.Link | None
|
||||
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
|
||||
parent_warehouse: DF.Link | None
|
||||
pick_manually: DF.Check
|
||||
prompt_qty: DF.Check
|
||||
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
|
||||
scan_barcode: DF.Data | None
|
||||
@@ -70,7 +76,8 @@ class PickList(Document):
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
self.set_item_locations()
|
||||
if not self.pick_manually:
|
||||
self.set_item_locations()
|
||||
|
||||
if self.get("locations"):
|
||||
self.validate_sales_order_percentage()
|
||||
@@ -198,10 +205,11 @@ class PickList(Document):
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
|
||||
def on_update(self):
|
||||
self.linked_serial_and_batch_bundle()
|
||||
if self.get("locations"):
|
||||
self.linked_serial_and_batch_bundle()
|
||||
|
||||
def linked_serial_and_batch_bundle(self):
|
||||
for row in self.locations:
|
||||
for row in self.get("locations"):
|
||||
if row.serial_and_batch_bundle:
|
||||
frappe.get_doc(
|
||||
"Serial and Batch Bundle", row.serial_and_batch_bundle
|
||||
@@ -510,55 +518,82 @@ class PickList(Document):
|
||||
def get_picked_items_details(self, items):
|
||||
picked_items = frappe._dict()
|
||||
|
||||
if items:
|
||||
pi = frappe.qb.DocType("Pick List")
|
||||
pi_item = frappe.qb.DocType("Pick List Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(pi_item)
|
||||
.on(pi.name == pi_item.parent)
|
||||
.select(
|
||||
pi_item.item_code,
|
||||
pi_item.warehouse,
|
||||
pi_item.batch_no,
|
||||
pi_item.serial_and_batch_bundle,
|
||||
Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
|
||||
"picked_qty"
|
||||
),
|
||||
Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
|
||||
)
|
||||
.where(
|
||||
(pi_item.item_code.isin([x.item_code for x in items]))
|
||||
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
|
||||
& (pi.status != "Completed")
|
||||
& (pi.status != "Cancelled")
|
||||
& (pi_item.docstatus != 2)
|
||||
)
|
||||
.groupby(
|
||||
pi_item.item_code,
|
||||
pi_item.warehouse,
|
||||
pi_item.batch_no,
|
||||
)
|
||||
)
|
||||
if not items:
|
||||
return picked_items
|
||||
|
||||
if self.name:
|
||||
query = query.where(pi_item.parent != self.name)
|
||||
items_data = self._get_pick_list_items(items)
|
||||
|
||||
items_data = query.run(as_dict=True)
|
||||
for item_data in items_data:
|
||||
key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse
|
||||
serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None
|
||||
|
||||
for item_data in items_data:
|
||||
key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse
|
||||
serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None
|
||||
data = {"picked_qty": item_data.picked_qty}
|
||||
if serial_no:
|
||||
data["serial_no"] = serial_no
|
||||
if item_data.item_code not in picked_items:
|
||||
picked_items[item_data.item_code] = {key: data}
|
||||
else:
|
||||
picked_items[item_data.item_code][key] = data
|
||||
if item_data.serial_and_batch_bundle:
|
||||
if not serial_no:
|
||||
serial_no = get_serial_nos_from_bundle(item_data.serial_and_batch_bundle)
|
||||
|
||||
if not item_data.batch_no and not serial_no:
|
||||
bundle_batches = get_batches_from_bundle(item_data.serial_and_batch_bundle)
|
||||
for batch_no, batch_qty in bundle_batches.items():
|
||||
batch_qty = abs(batch_qty)
|
||||
|
||||
key = (item_data.warehouse, batch_no)
|
||||
if item_data.item_code not in picked_items:
|
||||
picked_items[item_data.item_code] = {key: {"picked_qty": batch_qty}}
|
||||
else:
|
||||
picked_items[item_data.item_code][key]["picked_qty"] += batch_qty
|
||||
|
||||
continue
|
||||
|
||||
if item_data.item_code not in picked_items:
|
||||
picked_items[item_data.item_code] = {}
|
||||
|
||||
if key not in picked_items[item_data.item_code]:
|
||||
picked_items[item_data.item_code][key] = frappe._dict(
|
||||
{
|
||||
"picked_qty": 0,
|
||||
"serial_no": [],
|
||||
"batch_no": item_data.batch_no or "",
|
||||
"warehouse": item_data.warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
picked_items[item_data.item_code][key]["picked_qty"] += item_data.picked_qty
|
||||
if serial_no:
|
||||
picked_items[item_data.item_code][key]["serial_no"].extend(serial_no)
|
||||
|
||||
return picked_items
|
||||
|
||||
def _get_pick_list_items(self, items):
|
||||
pi = frappe.qb.DocType("Pick List")
|
||||
pi_item = frappe.qb.DocType("Pick List Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(pi_item)
|
||||
.on(pi.name == pi_item.parent)
|
||||
.select(
|
||||
pi_item.item_code,
|
||||
pi_item.warehouse,
|
||||
pi_item.batch_no,
|
||||
pi_item.serial_and_batch_bundle,
|
||||
pi_item.serial_no,
|
||||
(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
|
||||
"picked_qty"
|
||||
),
|
||||
)
|
||||
.where(
|
||||
(pi_item.item_code.isin([x.item_code for x in items]))
|
||||
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
|
||||
& (pi.status != "Completed")
|
||||
& (pi.status != "Cancelled")
|
||||
& (pi_item.docstatus != 2)
|
||||
)
|
||||
)
|
||||
|
||||
if self.name:
|
||||
query = query.where(pi_item.parent != self.name)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def _get_product_bundles(self) -> dict[str, str]:
|
||||
# Dict[so_item_row: item_code]
|
||||
product_bundles = {}
|
||||
@@ -715,9 +750,7 @@ def get_available_item_locations(
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
locations = []
|
||||
total_picked_qty = (
|
||||
sum([v.get("picked_qty") for k, v in picked_item_details.items()]) if picked_item_details else 0
|
||||
)
|
||||
|
||||
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
|
||||
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
|
||||
|
||||
@@ -727,63 +760,90 @@ def get_available_item_locations(
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
elif has_serial_no:
|
||||
locations = get_available_item_locations_for_serialized_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
elif has_batch_no:
|
||||
locations = get_available_item_locations_for_batched_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
else:
|
||||
locations = get_available_item_locations_for_other_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
|
||||
if picked_item_details:
|
||||
locations = filter_locations_by_picked_materials(locations, picked_item_details)
|
||||
|
||||
if locations:
|
||||
locations = get_locations_based_on_required_qty(locations, required_qty)
|
||||
|
||||
if not ignore_validation:
|
||||
validate_picked_materials(item_code, required_qty, locations)
|
||||
|
||||
return locations
|
||||
|
||||
|
||||
def get_locations_based_on_required_qty(locations, required_qty):
|
||||
filtered_locations = []
|
||||
|
||||
for location in locations:
|
||||
if location.qty >= required_qty:
|
||||
location.qty = required_qty
|
||||
filtered_locations.append(location)
|
||||
break
|
||||
|
||||
required_qty -= location.qty
|
||||
filtered_locations.append(location)
|
||||
|
||||
return filtered_locations
|
||||
|
||||
|
||||
def validate_picked_materials(item_code, required_qty, locations):
|
||||
for location in list(locations):
|
||||
if location["qty"] < 0:
|
||||
locations.remove(location)
|
||||
|
||||
total_qty_available = sum(location.get("qty") for location in locations)
|
||||
remaining_qty = required_qty - total_qty_available
|
||||
|
||||
if remaining_qty > 0 and not ignore_validation:
|
||||
if remaining_qty > 0:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is not available.").format(
|
||||
remaining_qty, frappe.get_desk_link("Item", item_code)
|
||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
),
|
||||
title=_("Insufficient Stock"),
|
||||
title=_("Already Picked"),
|
||||
)
|
||||
|
||||
if picked_item_details:
|
||||
for location in list(locations):
|
||||
if location["qty"] < 0:
|
||||
locations.remove(location)
|
||||
|
||||
total_qty_available = sum(location.get("qty") for location in locations)
|
||||
remaining_qty = required_qty - total_qty_available
|
||||
def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
|
||||
for row in locations:
|
||||
key = row.warehouse
|
||||
if row.batch_no:
|
||||
key = (row.warehouse, row.batch_no)
|
||||
|
||||
if remaining_qty > 0 and not ignore_validation:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
||||
remaining_qty, frappe.get_desk_link("Item", item_code)
|
||||
),
|
||||
title=_("Already Picked"),
|
||||
)
|
||||
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
|
||||
if not picked_qty:
|
||||
continue
|
||||
if picked_qty > row.qty:
|
||||
row.qty = 0
|
||||
picked_item_details[key]["picked_qty"] -= row.qty
|
||||
else:
|
||||
row.qty -= picked_qty
|
||||
picked_item_details[key]["picked_qty"] = 0.0
|
||||
if row.serial_nos:
|
||||
row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no")))
|
||||
|
||||
return locations
|
||||
|
||||
@@ -793,15 +853,12 @@ def get_available_item_locations_for_serial_and_batched_item(
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
# Get batch nos by FIFO
|
||||
locations = get_available_item_locations_for_batched_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
|
||||
@@ -821,7 +878,6 @@ def get_available_item_locations_for_serial_and_batched_item(
|
||||
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
|
||||
)
|
||||
.orderby(sn.creation)
|
||||
.limit(ceil(location.qty + total_picked_qty))
|
||||
).run(as_dict=True)
|
||||
|
||||
serial_nos = [sn.name for sn in serial_nos]
|
||||
@@ -834,18 +890,14 @@ def get_available_item_locations_for_serial_and_batched_item(
|
||||
def get_available_item_locations_for_serialized_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
|
||||
|
||||
sn = frappe.qb.DocType("Serial No")
|
||||
query = (
|
||||
frappe.qb.from_(sn)
|
||||
.select(sn.name, sn.warehouse)
|
||||
.where((sn.item_code == item_code) & (sn.company == company))
|
||||
.where(sn.item_code == item_code)
|
||||
.orderby(sn.creation)
|
||||
)
|
||||
|
||||
@@ -853,6 +905,7 @@ def get_available_item_locations_for_serialized_item(
|
||||
query = query.where(sn.warehouse.isin(from_warehouses))
|
||||
else:
|
||||
query = query.where(Coalesce(sn.warehouse, "") != "")
|
||||
query = query.where(sn.company == company)
|
||||
|
||||
if not consider_rejected_warehouses:
|
||||
if rejected_warehouses := get_rejected_warehouses():
|
||||
@@ -861,16 +914,8 @@ def get_available_item_locations_for_serialized_item(
|
||||
serial_nos = query.run(as_list=True)
|
||||
|
||||
warehouse_serial_nos_map = frappe._dict()
|
||||
picked_qty = required_qty
|
||||
for serial_no, warehouse in serial_nos:
|
||||
if serial_no in picked_serial_nos:
|
||||
continue
|
||||
|
||||
if picked_qty <= 0:
|
||||
break
|
||||
|
||||
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
|
||||
picked_qty -= 1
|
||||
|
||||
locations = []
|
||||
|
||||
@@ -878,12 +923,14 @@ def get_available_item_locations_for_serialized_item(
|
||||
qty = len(serial_nos)
|
||||
|
||||
locations.append(
|
||||
{
|
||||
"qty": qty,
|
||||
"warehouse": warehouse,
|
||||
"item_code": item_code,
|
||||
"serial_nos": serial_nos,
|
||||
}
|
||||
frappe._dict(
|
||||
{
|
||||
"qty": qty,
|
||||
"warehouse": warehouse,
|
||||
"item_code": item_code,
|
||||
"serial_nos": serial_nos,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return locations
|
||||
@@ -892,9 +939,6 @@ def get_available_item_locations_for_serialized_item(
|
||||
def get_available_item_locations_for_batched_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
locations = []
|
||||
@@ -903,8 +947,6 @@ def get_available_item_locations_for_batched_item(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": from_warehouses,
|
||||
"qty": required_qty,
|
||||
"is_pick_list": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -940,9 +982,7 @@ def get_available_item_locations_for_batched_item(
|
||||
def get_available_item_locations_for_other_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
@@ -951,7 +991,6 @@ def get_available_item_locations_for_other_item(
|
||||
.select(bin.warehouse, bin.actual_qty.as_("qty"))
|
||||
.where((bin.item_code == item_code) & (bin.actual_qty > 0))
|
||||
.orderby(bin.creation)
|
||||
.limit(cint(required_qty + total_picked_qty))
|
||||
)
|
||||
|
||||
if from_warehouses:
|
||||
|
||||
@@ -815,7 +815,7 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
def test_pick_list_status(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(properties={"maintain_stock": 1}).name
|
||||
item = make_item(properties={"is_stock_item": 1}).name
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
@@ -845,3 +845,135 @@ class TestPickList(FrappeTestCase):
|
||||
pl.cancel()
|
||||
pl.reload()
|
||||
self.assertEqual(pl.status, "Cancelled")
|
||||
|
||||
def test_pick_list_validation(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item("Test Non Serialized Pick List Item", properties={"is_stock_item": 1}).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
self.assertFalse(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_validation_for_serial_no(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Serialized Pick List Item",
|
||||
properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-SPLI-.####"},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.locations[0].qty = 5
|
||||
pl.save()
|
||||
pl.submit()
|
||||
self.assertTrue(pl.locations[0].serial_no)
|
||||
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertTrue(pl.locations[0].serial_no)
|
||||
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
self.assertFalse(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_validation_for_batch_no(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Batch Pick List Item",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCH-SPLI-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.locations[0].qty = 5
|
||||
pl.save()
|
||||
pl.submit()
|
||||
self.assertTrue(pl.locations[0].batch_no)
|
||||
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertTrue(pl.locations[0].batch_no)
|
||||
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
self.assertFalse(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_validation_for_batch_no_and_serial_item(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Serialized Batch Pick List Item",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "SN-BT-BATCH-SPLI-.####",
|
||||
"create_new_batch": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-BT-SPLI-.####",
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.locations[0].qty = 5
|
||||
pl.save()
|
||||
pl.submit()
|
||||
self.assertTrue(pl.locations[0].batch_no)
|
||||
self.assertTrue(pl.locations[0].serial_no)
|
||||
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertTrue(pl.locations[0].batch_no)
|
||||
self.assertTrue(pl.locations[0].serial_no)
|
||||
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
self.assertFalse(hasattr(pl, "locations"))
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
"column_break_27",
|
||||
"total",
|
||||
"net_total",
|
||||
"tax_withholding_net_total",
|
||||
"base_tax_withholding_net_total",
|
||||
"taxes_charges_section",
|
||||
"tax_category",
|
||||
"taxes_and_charges",
|
||||
@@ -1246,13 +1248,31 @@
|
||||
"label": "Subcontracting Receipt",
|
||||
"options": "Subcontracting Receipt",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_net_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Tax Withholding Net Total",
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_tax_withholding_net_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Base Tax Withholding Net Total",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:25.441066",
|
||||
"modified": "2024-04-08 20:23:03.699201",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
||||
@@ -15,6 +15,7 @@ from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.accounts_controller import merge_taxes
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
|
||||
|
||||
@@ -51,6 +52,7 @@ class PurchaseReceipt(BuyingController):
|
||||
base_net_total: DF.Currency
|
||||
base_rounded_total: DF.Currency
|
||||
base_rounding_adjustment: DF.Currency
|
||||
base_tax_withholding_net_total: DF.Currency
|
||||
base_taxes_and_charges_added: DF.Currency
|
||||
base_taxes_and_charges_deducted: DF.Currency
|
||||
base_total: DF.Currency
|
||||
@@ -120,6 +122,7 @@ class PurchaseReceipt(BuyingController):
|
||||
supplier_name: DF.Data | None
|
||||
supplier_warehouse: DF.Link | None
|
||||
tax_category: DF.Link | None
|
||||
tax_withholding_net_total: DF.Currency
|
||||
taxes: DF.Table[PurchaseTaxesandCharges]
|
||||
taxes_and_charges: DF.Link | None
|
||||
taxes_and_charges_added: DF.Currency
|
||||
@@ -1123,37 +1126,6 @@ def get_item_wise_returned_qty(pr_doc):
|
||||
)
|
||||
|
||||
|
||||
def merge_taxes(source_taxes, target_doc):
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
update_item_wise_tax_detail,
|
||||
)
|
||||
|
||||
existing_taxes = target_doc.get("taxes") or []
|
||||
idx = 1
|
||||
for tax in source_taxes:
|
||||
found = False
|
||||
for t in existing_taxes:
|
||||
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
||||
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
||||
update_item_wise_tax_detail(t, tax)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
tax.charge_type = "Actual"
|
||||
tax.idx = idx
|
||||
idx += 1
|
||||
tax.included_in_print_rate = 0
|
||||
tax.dont_recompute_tax = 1
|
||||
tax.row_id = ""
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
||||
tax.item_wise_tax_detail = tax.item_wise_tax_detail
|
||||
existing_taxes.append(tax)
|
||||
|
||||
target_doc.set("taxes", existing_taxes)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
from erpnext.accounts.party import get_payment_terms_template
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"apply_tds",
|
||||
"section_break_29",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -1107,12 +1108,20 @@
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Apply TDS",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:25.896543",
|
||||
"modified": "2024-04-08 20:00:16.277292",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
||||
@@ -16,6 +16,7 @@ class PurchaseReceiptItem(Document):
|
||||
|
||||
allow_zero_valuation_rate: DF.Check
|
||||
amount: DF.Currency
|
||||
apply_tds: DF.Check
|
||||
asset_category: DF.Link | None
|
||||
asset_location: DF.Link | None
|
||||
barcode: DF.Data | None
|
||||
|
||||
@@ -860,6 +860,12 @@ class SerialandBatchBundle(Document):
|
||||
self.validate_batch_inventory()
|
||||
|
||||
def validate_batch_inventory(self):
|
||||
if (
|
||||
self.voucher_type in ["Purchase Invoice", "Purchase Receipt"]
|
||||
and frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1
|
||||
):
|
||||
return
|
||||
|
||||
if not self.has_batch_no:
|
||||
return
|
||||
|
||||
|
||||
@@ -9,7 +9,17 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
comma_or,
|
||||
cstr,
|
||||
flt,
|
||||
format_time,
|
||||
formatdate,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
@@ -30,6 +40,7 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||
OpeningEntryAccountError,
|
||||
)
|
||||
from erpnext.stock.get_item_details import (
|
||||
get_barcode_data,
|
||||
get_bin_details,
|
||||
get_conversion_factor,
|
||||
get_default_cost_center,
|
||||
@@ -428,7 +439,14 @@ class StockEntry(StockController):
|
||||
for field in reset_fields:
|
||||
item.set(field, item_details.get(field))
|
||||
|
||||
update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor")
|
||||
update_fields = (
|
||||
"uom",
|
||||
"description",
|
||||
"expense_account",
|
||||
"cost_center",
|
||||
"conversion_factor",
|
||||
"barcode",
|
||||
)
|
||||
|
||||
for field in update_fields:
|
||||
if not item.get(field):
|
||||
@@ -637,8 +655,8 @@ class StockEntry(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order)
|
||||
job_card_link = frappe.utils.get_link_to_form("Job Card", job_card)
|
||||
work_order_link = get_link_to_form("Work Order", self.work_order)
|
||||
job_card_link = get_link_to_form("Job Card", job_card)
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}."
|
||||
@@ -1347,9 +1365,24 @@ class StockEntry(StockController):
|
||||
|
||||
return finished_item_row
|
||||
|
||||
def validate_serial_batch_bundle_type(self, serial_and_batch_bundle):
|
||||
if (
|
||||
frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "type_of_transaction")
|
||||
!= "Outward"
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}"
|
||||
).format(get_link_to_form("Serial and Batch Bundle", serial_and_batch_bundle)),
|
||||
title=_("Invalid Serial and Batch Bundle"),
|
||||
)
|
||||
|
||||
def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
|
||||
for d in self.get("items"):
|
||||
if cstr(d.s_warehouse):
|
||||
if d.serial_and_batch_bundle and self.docstatus == 1:
|
||||
self.validate_serial_batch_bundle_type(d.serial_and_batch_bundle)
|
||||
|
||||
sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
@@ -1366,6 +1399,21 @@ class StockEntry(StockController):
|
||||
):
|
||||
sle.dependant_sle_voucher_detail_no = finished_item_row.name
|
||||
|
||||
if sle.serial_and_batch_bundle and self.docstatus == 2:
|
||||
bundle_id = frappe.get_cached_value(
|
||||
"Serial and Batch Bundle",
|
||||
{
|
||||
"voucher_detail_no": d.name,
|
||||
"voucher_no": self.name,
|
||||
"is_cancelled": 0,
|
||||
"type_of_transaction": "Outward",
|
||||
},
|
||||
"name",
|
||||
)
|
||||
|
||||
if bundle_id:
|
||||
sle.serial_and_batch_bundle = bundle_id
|
||||
|
||||
sl_entries.append(sle)
|
||||
|
||||
def make_serial_and_batch_bundle_for_transfer(self):
|
||||
@@ -1634,6 +1682,10 @@ class StockEntry(StockController):
|
||||
if subcontract_items and len(subcontract_items) == 1:
|
||||
ret["subcontracted_item"] = subcontract_items[0].main_item_code
|
||||
|
||||
barcode_data = get_barcode_data(item_code=item.name)
|
||||
if barcode_data and len(barcode_data.get(item.name)) == 1:
|
||||
ret["barcode"] = barcode_data.get(item.name)[0]
|
||||
|
||||
return ret
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -110,6 +110,12 @@ class TestStockEntry(FrappeTestCase):
|
||||
self._test_auto_material_request("_Test Item")
|
||||
self._test_auto_material_request("_Test Item", material_request_type="Transfer")
|
||||
|
||||
def test_barcode_item_stock_entry(self):
|
||||
item_code = make_item("_Test Item Stock Entry For Barcode", barcode="BDD-1234567890")
|
||||
|
||||
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
|
||||
self.assertEqual(se.items[0].barcode, "BDD-1234567890")
|
||||
|
||||
def test_auto_material_request_for_variant(self):
|
||||
fields = [{"field_name": "reorder_levels"}]
|
||||
set_item_variant_settings(fields)
|
||||
@@ -1748,6 +1754,41 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertTrue(frappe.db.exists("Serial No", serial_no))
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||
|
||||
def test_serial_batch_bundle_type_of_transaction(self):
|
||||
item = make_item(
|
||||
"Test Use Serial and Batch Item SN Item",
|
||||
{
|
||||
"has_batch_no": 1,
|
||||
"is_stock_item": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_naming_series": "Test-SBBTYT-NNS.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=2,
|
||||
target="_Test Warehouse - _TC",
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=2,
|
||||
source="_Test Warehouse - _TC",
|
||||
target="Stores - _TC",
|
||||
use_serial_batch_fields=0,
|
||||
batch_no=batch_no,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
se.reload()
|
||||
sbb = se.items[0].serial_and_batch_bundle
|
||||
frappe.db.set_value("Serial and Batch Bundle", sbb, "type_of_transaction", "Inward")
|
||||
self.assertRaises(frappe.ValidationError, se.submit)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -81,6 +81,18 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
if (frm.doc.company) {
|
||||
frm.trigger("toggle_display_account_head");
|
||||
}
|
||||
|
||||
frm.events.set_fields_onload_for_line_item(frm);
|
||||
},
|
||||
|
||||
set_fields_onload_for_line_item(frm) {
|
||||
if (frm.is_new() && frm.doc?.items && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) {
|
||||
frm.doc.items.forEach((item) => {
|
||||
if (!item.serial_and_batch_bundle) {
|
||||
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
scan_barcode: function (frm) {
|
||||
@@ -155,6 +167,9 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
|
||||
item.qty = item.qty || 0;
|
||||
item.valuation_rate = item.valuation_rate || 0;
|
||||
item.use_serial_batch_fields = cint(
|
||||
frappe.user_defaults?.use_serial_batch_fields
|
||||
);
|
||||
});
|
||||
frm.refresh_field("items");
|
||||
},
|
||||
@@ -298,6 +313,10 @@ frappe.ui.form.on("Stock Reconciliation Item", {
|
||||
if (!item.warehouse && frm.doc.set_warehouse) {
|
||||
frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.set_warehouse);
|
||||
}
|
||||
|
||||
if (item.docstatus === 0 && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) {
|
||||
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
|
||||
}
|
||||
},
|
||||
|
||||
add_serial_batch_bundle(frm, cdt, cdn) {
|
||||
|
||||
@@ -1021,7 +1021,9 @@ def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no, posting_date, p
|
||||
@frappe.whitelist()
|
||||
def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False):
|
||||
ignore_empty_stock = cint(ignore_empty_stock)
|
||||
items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
|
||||
items = []
|
||||
if item_code and warehouse:
|
||||
items = get_item_and_warehouses(item_code, warehouse)
|
||||
|
||||
if not item_code:
|
||||
items = get_items_for_stock_reco(warehouse, company)
|
||||
@@ -1066,6 +1068,20 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
|
||||
return res
|
||||
|
||||
|
||||
def get_item_and_warehouses(item_code, warehouse):
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
items = []
|
||||
if frappe.get_cached_value("Warehouse", warehouse, "is_group"):
|
||||
childrens = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
|
||||
for ch_warehouse in childrens:
|
||||
items.append(frappe._dict({"item_code": item_code, "warehouse": ch_warehouse}))
|
||||
else:
|
||||
items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def get_items_for_stock_reco(warehouse, company):
|
||||
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||
items = frappe.db.sql(
|
||||
@@ -1080,7 +1096,7 @@ def get_items_for_stock_reco(warehouse, company):
|
||||
and i.is_stock_item = 1
|
||||
and i.has_variants = 0
|
||||
and exists(
|
||||
select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse
|
||||
select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse and is_group = 0
|
||||
)
|
||||
""",
|
||||
as_dict=1,
|
||||
@@ -1095,7 +1111,7 @@ def get_items_for_stock_reco(warehouse, company):
|
||||
where
|
||||
i.name = id.parent
|
||||
and exists(
|
||||
select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse
|
||||
select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse and is_group = 0
|
||||
)
|
||||
and i.is_stock_item = 1
|
||||
and i.has_variants = 0
|
||||
@@ -1157,7 +1173,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": row[0],
|
||||
"warehouse": warehouse,
|
||||
"warehouse": row[3],
|
||||
"qty": row[8],
|
||||
"item_name": row[1],
|
||||
"batch_no": row[4],
|
||||
|
||||
@@ -308,3 +308,13 @@ def clean_all_descriptions():
|
||||
clean_description = clean_html(item.description)
|
||||
if item.description != clean_description:
|
||||
frappe.db.set_value("Item", item.name, "description", clean_description)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_enable_stock_uom_editing():
|
||||
return frappe.get_cached_value(
|
||||
"Stock Settings",
|
||||
None,
|
||||
["allow_to_edit_stock_uom_qty_for_sales", "allow_to_edit_stock_uom_qty_for_purchase"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -490,12 +490,21 @@ def update_barcode_value(out):
|
||||
out["barcode"] = barcode_data.get(out.item_code)[0]
|
||||
|
||||
|
||||
def get_barcode_data(items_list):
|
||||
def get_barcode_data(items_list=None, item_code=None):
|
||||
# get item-wise batch no data
|
||||
# example: {'LED-GRE': [Batch001, Batch002]}
|
||||
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
|
||||
|
||||
itemwise_barcode = {}
|
||||
if not items_list and item_code:
|
||||
_dict_item_code = frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
}
|
||||
)
|
||||
|
||||
items_list = [frappe._dict(_dict_item_code)]
|
||||
|
||||
for item in items_list:
|
||||
barcodes = frappe.db.get_all("Item Barcode", filters={"parent": item.item_code}, fields="barcode")
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@ frappe.query_reports["Item Prices"] = {
|
||||
fieldtype: "Select",
|
||||
options: "Enabled Items only\nDisabled Items only\nAll Items",
|
||||
default: "Enabled Items only",
|
||||
on_change: function (query_report) {
|
||||
query_report.trigger_refresh();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -498,8 +498,6 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
return process_gl_map(gl_entries)
|
||||
|
||||
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
|
||||
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
||||
|
||||
warehouse_with_no_account = []
|
||||
|
||||
for item in self.items:
|
||||
@@ -517,31 +515,41 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
warehouse_account_name = warehouse_account[item.warehouse]["account"]
|
||||
warehouse_account_currency = warehouse_account[item.warehouse]["account_currency"]
|
||||
accepted_warehouse_account = warehouse_account[item.warehouse]["account"]
|
||||
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
|
||||
"account"
|
||||
)
|
||||
supplier_warehouse_account_currency = warehouse_account.get(
|
||||
self.supplier_warehouse, {}
|
||||
).get("account_currency")
|
||||
remarks = self.get("remarks") or _("Accounting Entry for Stock")
|
||||
|
||||
# FG Warehouse Account (Debit)
|
||||
# Accepted Warehouse Account (Debit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=warehouse_account_name,
|
||||
account=accepted_warehouse_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=stock_value_diff,
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=stock_rbnb,
|
||||
account_currency=warehouse_account_currency,
|
||||
against_account=item.expense_account,
|
||||
account_currency=get_account_currency(accepted_warehouse_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
# Expense Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=stock_value_diff,
|
||||
remarks=remarks,
|
||||
against_account=accepted_warehouse_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
# Supplier Warehouse Account (Credit)
|
||||
if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
|
||||
if flt(item.rm_supp_cost) and supplier_warehouse_account:
|
||||
# Supplier Warehouse Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=supplier_warehouse_account,
|
||||
@@ -549,40 +557,66 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
debit=0.0,
|
||||
credit=flt(item.rm_supp_cost),
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
account_currency=supplier_warehouse_account_currency,
|
||||
against_account=item.expense_account,
|
||||
account_currency=get_account_currency(supplier_warehouse_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
# Expense Account (Credit)
|
||||
if flt(item.service_cost_per_qty):
|
||||
# Expense Account (Debit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=flt(item.service_cost_per_qty) * flt(item.qty),
|
||||
debit=flt(item.rm_supp_cost),
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
against_account=supplier_warehouse_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
# Loss Account (Credit)
|
||||
divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount"))
|
||||
# Expense Account (Debit)
|
||||
if item.additional_cost_per_qty:
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=self.cost_center or self.get_company_default("cost_center"),
|
||||
debit=item.qty * item.additional_cost_per_qty,
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=None,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
)
|
||||
|
||||
if divisional_loss:
|
||||
loss_account = item.expense_account
|
||||
if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")):
|
||||
loss_account = self.get_company_default(
|
||||
"stock_adjustment_account", ignore_validation=True
|
||||
)
|
||||
|
||||
# Loss Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=loss_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=divisional_loss,
|
||||
remarks=remarks,
|
||||
against_account=item.expense_account,
|
||||
account_currency=get_account_currency(loss_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
# Expense Account (Debit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=divisional_loss,
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
account_currency=get_account_currency(loss_account),
|
||||
against_account=loss_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
@@ -592,7 +626,6 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
):
|
||||
warehouse_with_no_account.append(item.warehouse)
|
||||
|
||||
# Additional Costs Expense Accounts (Credit)
|
||||
for row in self.additional_costs:
|
||||
credit_amount = (
|
||||
flt(row.base_amount)
|
||||
@@ -600,6 +633,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
else flt(row.amount)
|
||||
)
|
||||
|
||||
# Additional Cost Expense Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=row.expense_account,
|
||||
@@ -608,6 +642,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
credit=credit_amount,
|
||||
remarks=remarks,
|
||||
against_account=None,
|
||||
account_currency=get_account_currency(row.expense_account),
|
||||
)
|
||||
|
||||
if warehouse_with_no_account:
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.utils import add_days, cint, flt, nowtime, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_rm_items,
|
||||
@@ -351,26 +352,15 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1)
|
||||
|
||||
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
|
||||
supplier_warehouse_ac = get_inventory_account(scr.company, scr.supplier_warehouse)
|
||||
expense_account = scr.items[0].expense_account
|
||||
|
||||
if fg_warehouse_ac == supplier_warehouse_ac:
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C)
|
||||
expense_account: [0.0, 1000.0], # Service Cost (C)
|
||||
additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C)
|
||||
}
|
||||
else:
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [2100.0, 0.0], # FG Amount (D)
|
||||
supplier_warehouse_ac: [0.0, 1000.0], # RM Cost (C)
|
||||
expense_account: [0.0, 1000.0], # Service Cost (C)
|
||||
additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C)
|
||||
}
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [2100.0, 1000],
|
||||
expense_account: [1100, 2100],
|
||||
additional_costs_expense_account: [0.0, 100.0],
|
||||
}
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||
@@ -381,6 +371,53 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name))
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
@change_settings("Stock Settings", {"use_serial_batch_fields": 0})
|
||||
def test_subcontracting_receipt_with_zero_service_cost(self):
|
||||
warehouse = "Stores - TCP1"
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": warehouse,
|
||||
"item_code": "Subcontracted Service Item 7",
|
||||
"qty": 10,
|
||||
"rate": 0,
|
||||
"fg_item": "Subcontracted Item SA7",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(
|
||||
company="_Test Company with perpetual inventory",
|
||||
warehouse=warehouse,
|
||||
supplier_warehouse="Work In Progress - TCP1",
|
||||
service_items=service_items,
|
||||
)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
|
||||
expense_account = scr.items[0].expense_account
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [1000, 1000],
|
||||
expense_account: [1000, 1000],
|
||||
}
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
||||
|
||||
scr.reload()
|
||||
scr.cancel()
|
||||
|
||||
def test_supplied_items_consumed_qty(self):
|
||||
# Set Backflush Based On as "Material Transferred for Subcontracting" to transfer RM's more than the required qty
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
@@ -1 +1 @@
|
||||
<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext</a>
|
||||
{{ _("Powered by {0}").format('<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
|
||||
|
||||
Reference in New Issue
Block a user