Merge branch 'frappe:develop' into patch-1

This commit is contained in:
Markus Lobedann
2024-04-18 14:51:05 +02:00
committed by GitHub
145 changed files with 211390 additions and 34294 deletions

View File

@@ -1,4 +1,8 @@
files:
- source: /erpnext/locale/main.pot
translation: /erpnext/locale/%two_letters_code%.po
pull_request_title: "chore: sync translations from crowdin"
pull_request_title: "fix: sync translations from crowdin"
pull_request_labels:
- translation
commit_message: "fix: %language% translations"
append_commit_message: false

View File

@@ -57,9 +57,12 @@ frappe.ui.form.on("Accounting Dimension", {
}
},
label: function (frm) {
frm.set_value("fieldname", frappe.model.scrub(frm.doc.label));
},
document_type: function (frm) {
frm.set_value("label", frm.doc.document_type);
frm.set_value("fieldname", frappe.model.scrub(frm.doc.document_type));
frappe.db.get_value(
"Accounting Dimension",

View File

@@ -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,19 +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, 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()

View File

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

View File

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

View File

@@ -453,7 +453,10 @@ frappe.ui.form.on("Journal Entry Account", {
}
},
cost_center: function (frm, dt, dn) {
erpnext.journal_entry.set_account_details(frm, dt, dn);
// Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
erpnext.journal_entry.set_account_details(frm, dt, dn);
}
},
account: function (frm, dt, dn) {

View 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) {
// },
// });

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

View 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

View 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])

View 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 Monitor", {
// refresh(frm) {
// },
// });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -470,6 +470,9 @@ frappe.ui.form.on("Payment Entry", {
() => frm.events.set_dynamic_labels(frm),
() => {
frm.set_party_account_based_on_party = false;
if (r.message.party_bank_account) {
frm.set_value("party_bank_account", r.message.party_bank_account);
}
if (r.message.bank_account) {
frm.set_value("bank_account", r.message.bank_account);
}
@@ -1662,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) {

View File

@@ -150,6 +150,7 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
@@ -477,6 +478,7 @@
"label": "More Information"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -580,6 +582,7 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Payment Order Status",
"no_copy": 1,
"options": "Initiated\nPayment Ordered",
"read_only": 1
},
@@ -776,7 +779,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2024-03-27 13:10:09.131139",
"modified": "2024-04-11 11:25:07.366347",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -16,6 +16,7 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.bank_account.bank_account import (
get_bank_account_details,
get_default_company_bank_account,
get_party_bank_account,
)
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
@@ -187,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):
@@ -194,6 +196,9 @@ class PaymentEntry(AccountsController):
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
return
if self.party_type not in ("Customer", "Supplier"):
return
if not frappe.db.get_value(
"Company", self.company, "book_advance_payments_in_separate_party_account"
):
@@ -480,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:
@@ -491,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:
@@ -1116,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,
)
]
@@ -1307,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,
@@ -1326,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(
@@ -2109,14 +2136,16 @@ def get_party_details(company, party_type, party, date, cost_center=None):
party_name = frappe.db.get_value(party_type, party, _party_name)
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
if party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(party_type, party)
party_bank_account = get_party_bank_account(party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party)
return {
"party_account": party_account,
"party_name": party_name,
"party_account_currency": account_currency,
"party_balance": party_balance,
"account_balance": account_balance,
"party_bank_account": party_bank_account,
"bank_account": bank_account,
}
@@ -2150,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:
@@ -2238,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:

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,8 @@ import unittest
# test_records = frappe.get_test_records('Payment Gateway Account')
test_ignore = ["Payment Gateway"]
class TestPaymentGatewayAccount(unittest.TestCase):
pass

View File

@@ -71,6 +71,7 @@ frappe.ui.form.on("Payment Order", {
target: frm,
date_field: "posting_date",
setters: {
party_type: "Supplier",
party: frm.doc.supplier || "",
},
get_query_filters: {
@@ -91,6 +92,7 @@ frappe.ui.form.on("Payment Order", {
source_doctype: "Payment Request",
target: frm,
setters: {
party_type: "Supplier",
party: frm.doc.supplier || "",
},
get_query_filters: {

View File

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

View File

@@ -92,7 +92,7 @@ class PaymentRequest(Document):
self.status = "Draft"
self.validate_reference_document()
self.validate_payment_request_amount()
self.validate_currency()
# self.validate_currency()
self.validate_subscription_details()
def validate_reference_document(self):
@@ -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":
@@ -341,21 +335,17 @@ class PaymentRequest(Document):
}
)
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
amount = payment_entry.base_paid_amount
else:
amount = self.grand_total
payment_entry.received_amount = amount
payment_entry.get("references")[0].allocated_amount = amount
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
if payment_entry.difference_amount:
company_details = get_company_defaults(ref_doc.company)
payment_entry.append(
"deductions",
{
"account": company_details.exchange_gain_loss_account,
"cost_center": company_details.cost_center,
"amount": payment_entry.difference_amount,
},
)
if submit:
payment_entry.insert(ignore_permissions=True)
payment_entry.submit()
@@ -485,6 +475,12 @@ def make_payment_request(**args):
pr = frappe.get_doc("Payment Request", draft_payment_request)
else:
pr = frappe.new_doc("Payment Request")
if not args.get("payment_request_type"):
args["payment_request_type"] = (
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
)
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -544,9 +540,9 @@ def get_amount(ref_doc, payment_account=None):
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
grand_total = flt(ref_doc.grand_total)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
@@ -644,7 +640,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

View File

@@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase):
pr = make_payment_request(
dt="Purchase Invoice",
dn=si_usd.name,
party_type="Supplier",
party="_Test Supplier USD",
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
@@ -98,6 +100,51 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.status, "Paid")
def test_multiple_payment_entry_against_purchase_invoice(self):
purchase_invoice = make_purchase_invoice(
customer="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
)
pr = make_payment_request(
dt="Purchase Invoice",
party_type="Supplier",
party="_Test Supplier USD",
dn=purchase_invoice.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
return_doc=1,
)
pr.grand_total = pr.grand_total / 2
pr.submit()
pr.create_payment_entry()
purchase_invoice.load_from_db()
self.assertEqual(purchase_invoice.status, "Partly Paid")
pr = make_payment_request(
dt="Purchase Invoice",
party_type="Supplier",
party="_Test Supplier USD",
dn=purchase_invoice.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
return_doc=1,
)
pr.save()
pr.submit()
pr.create_payment_entry()
purchase_invoice.load_from_db()
self.assertEqual(purchase_invoice.status, "Paid")
def test_payment_entry(self):
frappe.db.set_value(
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"

View File

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

View File

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

View File

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

View File

@@ -299,6 +299,7 @@
"remember_last_selected_value": 1
},
{
"allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
@@ -1368,6 +1369,7 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -1638,7 +1640,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:23.476658",
"modified": "2024-04-11 11:28:42.802211",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -68,15 +68,11 @@ class PurchaseInvoice(BuyingController):
from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import (
PurchaseInvoiceAdvance,
)
from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import (
PurchaseInvoiceItem,
)
from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import PurchaseInvoiceItem
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
PurchaseTaxesandCharges,
)
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import (
TaxWithheldVouchers,
)
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
PurchaseReceiptItemSupplied,
)

View File

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

View File

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

View File

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

View File

@@ -291,6 +291,7 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"hide_days": 1,
@@ -356,6 +357,7 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"hide_days": 1,
@@ -2201,7 +2203,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-03-27 13:10:35.407256",
"modified": "2024-04-11 11:30:26.272441",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -58,13 +58,9 @@ class SalesInvoice(SellingController):
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
SalesInvoiceAdvance,
)
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
SalesInvoicePayment,
)
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
SalesInvoiceTimesheet,
)

View File

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

View File

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

View File

@@ -665,7 +665,7 @@ class ReceivablePayableReport:
else:
future_amount_field = "future_amount_in_base_currency"
if row.remaining_balance > 0 and future.get(future_amount_field):
if row.remaining_balance != 0 and future.get(future_amount_field):
if future.get(future_amount_field) > row.outstanding:
row.future_amount = row.outstanding
future[future_amount_field] = future.get(future_amount_field) - row.outstanding

View File

@@ -463,11 +463,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
)
def test_future_payments(self):
sr = self.create_sales_invoice(do_not_submit=True)
sr.is_return = 1
sr.items[0].qty = -1
sr.items[0].rate = 10
sr.calculate_taxes_and_totals()
sr.submit()
si = self.create_sales_invoice()
pe = get_payment_entry(si.doctype, si.name)
pe.append(
"references",
{
"reference_doctype": sr.doctype,
"reference_name": sr.name,
"due_date": sr.due_date,
"total_amount": sr.grand_total,
"outstanding_amount": sr.outstanding_amount,
"allocated_amount": sr.outstanding_amount,
},
)
pe.posting_date = add_days(today(), 1)
pe.paid_amount = 90.0
pe.references[0].allocated_amount = 90.0
pe.paid_amount = 80
pe.references[0].allocated_amount = 90.0 # pe.paid_amount + sr.grand_total
pe.save().submit()
filters = {
"company": self.company,
@@ -479,16 +498,21 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"show_future_payments": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
self.assertEqual(len(report), 2)
expected_data = [100.0, 100.0, 10.0, 90.0]
expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
rows = report[:2]
for row in rows:
self.assertEqual(
expected_data[row.voucher_no],
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
)
pe.cancel()
sr.load_from_db() # Outstanding amount is updated so a updated timestamp is needed.
sr.cancel()
# full payment in future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)

View File

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

View File

@@ -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",
@@ -453,7 +460,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
for gle in gl_entries:
group_by_value = gle.get(group_by)
gle.voucher_type = _(gle.voucher_type)
gle.voucher_type = gle.voucher_type
gle.voucher_subtype = _(gle.voucher_subtype)
gle.against_voucher_type = _(gle.against_voucher_type)
gle.remarks = _(gle.remarks)

View File

@@ -655,13 +655,13 @@ class GrossProfitGenerator:
elif self.delivery_notes.get((row.parent, row.item_code), None):
# check if Invoice has delivery notes
dn = self.delivery_notes.get((row.parent, row.item_code))
parenttype, parent, item_row, _warehouse = (
parenttype, parent, item_row, dn_warehouse = (
"Delivery Note",
dn["delivery_note"],
dn["item_row"],
dn["warehouse"],
)
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)

View File

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

View File

@@ -1,8 +1,4 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () {
frappe.query_reports["Purchase Invoice Trends"] = {
filters: erpnext.get_purchase_trends_filters(),
};
});
frappe.query_reports["Purchase Invoice Trends"] = $.extend({}, erpnext.purchase_trends_filters);

View File

@@ -1,8 +1,4 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.require("assets/erpnext/js/sales_trends_filters.js", function () {
frappe.query_reports["Sales Invoice Trends"] = {
filters: erpnext.get_sales_trends_filters(),
};
});
frappe.query_reports["Sales Invoice Trends"] = $.extend({}, erpnext.sales_trends_filters);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -462,7 +462,7 @@
},
{
"fieldname": "other_charges_calculation",
"fieldtype": "Markdown Editor",
"fieldtype": "Text Editor",
"label": "Taxes and Charges Calculation",
"no_copy": 1,
"oldfieldtype": "HTML",
@@ -928,7 +928,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:49.116641",
"modified": "2024-03-28 10:20:30.231915",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@@ -71,7 +71,7 @@ class SupplierQuotation(BuyingController):
naming_series: DF.Literal["PUR-SQTN-.YYYY.-"]
net_total: DF.Currency
opportunity: DF.Link | None
other_charges_calculation: DF.MarkdownEditor | None
other_charges_calculation: DF.TextEditor | None
plc_conversion_rate: DF.Float
price_list_currency: DF.Link | None
pricing_rules: DF.Table[PricingRuleDetail]

View File

@@ -1,8 +1,4 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () {
frappe.query_reports["Purchase Order Trends"] = {
filters: erpnext.get_purchase_trends_filters(),
};
});
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);

View File

@@ -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 = []
@@ -1438,7 +1439,8 @@ class AccountsController(TransactionBase):
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
if d.reference_doctype == "Purchase Invoice":
# Inverse debit/credit for payable accounts
if self.is_payable_account(d.reference_doctype, party_account):
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
@@ -1472,6 +1474,14 @@ class AccountsController(TransactionBase):
)
)
def is_payable_account(self, reference_doctype, account):
if reference_doctype == "Purchase Invoice" or (
reference_doctype == "Journal Entry"
and frappe.get_cached_value("Account", account, "account_type") == "Payable"
):
return True
return False
def update_against_document_in_jv(self):
"""
Links invoice and advance voucher:
@@ -1924,32 +1934,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 +2099,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 +2126,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 +3536,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

View File

@@ -139,7 +139,7 @@ class SellingController(StockController):
self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self):
if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted():
if not self.meta.get_field("commission_rate"):
return
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
@@ -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
@@ -748,12 +748,12 @@ def get_serial_and_batch_bundle(child, parent):
"item_code": child.item_code,
"warehouse": child.warehouse,
"voucher_type": parent.doctype,
"voucher_no": parent.name,
"voucher_no": parent.name if parent.docstatus < 2 else None,
"voucher_detail_no": child.name,
"posting_date": parent.posting_date,
"posting_time": parent.posting_time,
"qty": child.qty,
"type_of_transaction": "Outward" if child.qty > 0 else "Inward",
"type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward",
"company": parent.company,
"do_not_submit": "True",
}

View File

@@ -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"):
@@ -907,7 +907,7 @@ class StockController(AccountsController):
self.validate_multi_currency()
self.validate_packed_items()
if self.get("is_internal_supplier"):
if self.get("is_internal_supplier") and self.docstatus == 1:
self.validate_internal_transfer_qty()
else:
self.validate_internal_transfer_warehouse()
@@ -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)

View File

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

View File

@@ -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
"""
@@ -134,6 +135,27 @@ class TestAccountsController(FrappeTestCase):
acc = frappe.get_doc("Account", name)
self.debtors_usd = acc.name
account_name = "Creditors USD"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Payable - " + self.company_abbr
acc.company = self.company
acc.account_currency = "USD"
acc.account_type = "Payable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.creditors_usd = acc.name
def create_sales_invoice(
self,
qty=1,
@@ -173,7 +195,9 @@ class TestAccountsController(FrappeTestCase):
)
return sinv
def create_payment_entry(self, amount=1, source_exc_rate=75, posting_date=None, customer=None):
def create_payment_entry(
self, amount=1, source_exc_rate=75, posting_date=None, customer=None, submit=True
):
"""
Helper function to populate default values in payment entry
"""
@@ -1538,3 +1562,139 @@ 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, [])
def test_61_payment_entry_against_journal_for_payable_accounts(self):
# Invoices
exc_rate1 = 75
exc_rate2 = 77
amount = 1
je1 = self.create_journal_entry(
acc1=self.creditors_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 = "Supplier"
je1.accounts[0].party = self.supplier
je1 = je1.save().submit()
# Payment
pe = create_payment_entry(
company=self.company,
payment_type="Pay",
party_type="Supplier",
party=self.supplier,
paid_from=self.cash,
paid_to=self.creditors_usd,
paid_amount=amount,
)
pe.target_exchange_rate = exc_rate2
pe.received_amount = amount
pe.paid_amount = amount * exc_rate2
pe.save().submit()
pr = frappe.get_doc(
{
"doctype": "Payment Reconciliation",
"company": self.company,
"party_type": "Supplier",
"party": self.supplier,
"receivable_payable_account": get_party_account("Supplier", self.supplier, self.company),
}
)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
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)
# Exchange Gain/Loss Journal should've been created
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
self.assertEqual(len(exc_je_for_je1), 1)
# Cancel Payment
pe.reload()
pe.cancel()
self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount)
# Exchange Gain/Loss Journal should've been cancelled
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
self.assertEqual(exc_je_for_je1, [])

View File

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

File diff suppressed because it is too large Load Diff

83599
erpnext/locale/bs.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

83599
erpnext/locale/eo.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -948,6 +948,21 @@ class WorkOrder(Document):
if self.qty <= 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
if (
self.stock_uom
and frappe.get_cached_value("UOM", self.stock_uom, "must_be_whole_number")
and abs(cint(self.qty) - flt(self.qty, self.precision("qty"))) > 0.0000001
):
frappe.throw(
_(
"Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}."
).format(
flt(self.qty, self.precision("qty")),
frappe.bold(_("Must be Whole Number")),
frappe.bold(self.stock_uom),
),
)
if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item:
qty_dict = frappe.db.get_value(
"Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ def execute():
.where(
(si.creation.gte(creation_date))
& (si.docstatus == 1)
& (si.is_return is True)
& (si.is_return.eq(True))
& (si.return_against.notnull())
)
.run()
@@ -51,7 +51,7 @@ def execute():
.where(
(pi.creation.gte(creation_date))
& (pi.docstatus == 1)
& (pi.is_return is True)
& (pi.is_return.eq(True))
& (pi.return_against.notnull())
)
.run()

View File

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

View File

@@ -342,7 +342,6 @@ erpnext.buying = {
add_serial_batch_bundle(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
@@ -352,30 +351,28 @@ erpnext.buying = {
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false;
frappe.require(path, function() {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
);
});
}
);
}
});
}
@@ -383,40 +380,37 @@ erpnext.buying = {
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward";
item.is_rejected = true;
frappe.require(path, function() {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["rejected_warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["rejected_warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
);
});
}
);
}
});
}

View File

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

View File

@@ -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);
}
}
});
}
@@ -415,7 +420,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let row = locals[cdt][cdn];
if (row.barcode) {
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
debugger
frappe.model.set_value(cdt, cdn, {
"item_code": r.message.item_code,
"qty": 1,
@@ -2509,27 +2513,25 @@ erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close
}
}
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) {
item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward";
} else {
item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward";
}
if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) {
item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward";
} else {
item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward";
}
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) {
let update_values = {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
}
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) {
let update_values = {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
}
});
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
}
});
}

View File

@@ -4,6 +4,7 @@ import "./queries";
import "./sms_manager";
import "./utils/party";
import "./controllers/stock_controller";
import "./utils/serial_no_batch_selector";
import "./payment/payments";
import "./templates/visual_plant_floor_template.html";
import "./plant_floor_visual/visual_plant";
@@ -33,5 +34,7 @@ import "./utils/sales_common.js";
import "./controllers/buying.js";
import "./utils/demo.js";
import "./financial_statements.js";
import "./sales_trends_filters.js";
import "./purchase_trends_filters.js";
// import { sum } from 'frappe/public/utils/util.js'

View File

@@ -1,8 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
erpnext.get_purchase_trends_filters = function () {
return [
erpnext.purchase_trends_filters = {
filters: [
{
fieldname: "company",
label: __("Company"),
@@ -63,5 +63,5 @@ erpnext.get_purchase_trends_filters = function () {
options: ["", { value: "Item", label: __("Item") }, { value: "Supplier", label: __("Supplier") }],
default: "",
},
];
],
};

View File

@@ -1,8 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
erpnext.get_sales_trends_filters = function () {
return [
erpnext.sales_trends_filters = {
filters: [
{
fieldname: "period",
label: __("Period"),
@@ -53,5 +53,5 @@ erpnext.get_sales_trends_filters = function () {
options: "Company",
default: frappe.defaults.get_user_default("Company"),
},
];
],
};

View File

@@ -51,38 +51,7 @@ $.extend(erpnext, {
},
setup_serial_or_batch_no: function () {
let grid_row = cur_frm.open_grid_row();
if (
!grid_row ||
!grid_row.grid_form.fields_dict.serial_no ||
grid_row.grid_form.fields_dict.serial_no.get_status() !== "Write"
)
return;
frappe.model.get_value(
"Item",
{ name: grid_row.doc.item_code },
["has_serial_no", "has_batch_no"],
({ has_serial_no, has_batch_no }) => {
Object.assign(grid_row.doc, { has_serial_no, has_batch_no });
if (has_serial_no) {
attach_selector_button(
__("Add Serial No"),
grid_row.grid_form.fields_dict.serial_no.$wrapper,
this,
grid_row
);
} else if (has_batch_no) {
attach_selector_button(
__("Pick Batch No"),
grid_row.grid_form.fields_dict.batch_no.$wrapper,
this,
grid_row
);
}
}
);
// Deprecated in v15
},
route_to_adjustment_jv: (args) => {
@@ -430,30 +399,31 @@ $.extend(erpnext.utils, {
item_row.has_batch_no = r.message.has_batch_no;
item_row.has_serial_no = r.message.has_serial_no;
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function () {
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) {
let update_values = {
serial_and_batch_bundle: r.name,
qty: Math.abs(r.total_qty),
};
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) {
let update_values = {
serial_and_batch_bundle: r.name,
qty: Math.abs(r.total_qty),
};
if (!warehouse_field) {
warehouse_field = "warehouse";
}
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
if (!warehouse_field) {
warehouse_field = "warehouse";
}
});
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
}
});
});
},
get_fiscal_year: function (date, with_dates = false, boolean = false) {
if (!frappe.boot.setup_complete) {
return;
}
if (!date) {
date = frappe.datetime.get_today();
}
@@ -936,7 +906,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",
@@ -962,7 +932,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;
}

View File

@@ -350,7 +350,6 @@ erpnext.sales_common = {
pick_serial_and_batch(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
@@ -364,26 +363,24 @@ erpnext.sales_common = {
item.title = __("Select Serial and Batch");
}
frappe.require(path, function () {
new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
frappe.model.set_value(item.doctype, item.name, {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
qty:
qty /
flt(
item.conversion_factor || 1,
precision("conversion_factor", item)
),
});
new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
});
frappe.model.set_value(item.doctype, item.name, {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
qty:
qty /
flt(
item.conversion_factor || 1,
precision("conversion_factor", item)
),
});
}
});
}
});

View File

@@ -421,7 +421,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
let { qty, based_on } = this.dialog.get_values();
if (this.item.serial_and_batch_bundle || this.item.rejected_serial_and_batch_bundle) {
if (qty === this.qty) {
if (this.qty && qty === Math.abs(this.qty)) {
return;
}
}

View File

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

View File

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

View File

@@ -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
);
}
},
});
}
}
@@ -159,6 +169,27 @@ frappe.ui.form.on("Sales Order", {
);
},
// When multiple companies are set up. in case company name is changed set default company address
company: function (frm) {
if (frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: {
name: frm.doc.company,
existing_address: frm.doc.company_address || "",
},
debounce: 2000,
callback: function (r) {
if (r.message) {
frm.set_value("company_address", r.message);
} else {
frm.set_value("company_address", "");
}
},
});
}
},
onload: function (frm) {
if (!frm.doc.transaction_date) {
frm.set_value("transaction_date", frappe.datetime.get_today());
@@ -278,6 +309,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 +378,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 +395,11 @@ frappe.ui.form.on("Sales Order", {
frm.reload_doc();
},
});
}
dialog.hide();
dialog.hide();
} else {
frappe.msgprint(__("Please select items to reserve."));
}
},
});
@@ -380,6 +414,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 +439,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 +483,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 +499,11 @@ frappe.ui.form.on("Sales Order", {
frm.reload_doc();
},
});
}
dialog.hide();
dialog.hide();
} else {
frappe.msgprint(__("Please select items to unreserve."));
}
},
});

View File

@@ -1530,6 +1530,7 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
@@ -1657,7 +1658,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:36.714671",
"modified": "2024-03-29 16:27:41.539613",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -525,6 +525,8 @@ class SalesOrder(SellingController):
pass
def on_update_after_submit(self):
self.calculate_commission()
self.calculate_contribution()
self.check_credit_limit()
def before_update_after_submit(self):
@@ -1722,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")

View File

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

View File

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

View File

@@ -394,19 +394,17 @@ erpnext.PointOfSale.ItemDetails = class {
bind_auto_serial_fetch_event() {
this.$form_container.on("click", ".auto-fetch-btn", () => {
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => {
let frm = this.events.get_frm();
let item_row = this.item_row;
item_row.type_of_transaction = "Outward";
let frm = this.events.get_frm();
let item_row = this.item_row;
item_row.type_of_transaction = "Outward";
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) {
frappe.model.set_value(item_row.doctype, item_row.name, {
serial_and_batch_bundle: r.name,
qty: Math.abs(r.total_qty),
});
}
});
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) {
frappe.model.set_value(item_row.doctype, item_row.name, {
serial_and_batch_bundle: r.name,
qty: Math.abs(r.total_qty),
});
}
});
});
}

Some files were not shown because too many files have changed in this diff Show More