mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-15 21:05:10 +00:00
Merge branch 'frappe:develop' into patch-1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -146,6 +146,10 @@ class Dunning(AccountsController):
|
||||
)
|
||||
row.dunning_level = len(past_dunnings) + 1
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
|
||||
@@ -182,6 +182,7 @@ class GLEntry(Document):
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
@@ -195,6 +196,7 @@ class GLEntry(Document):
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Ledger Health", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2024-03-26 17:01:47.443986",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"checked_on",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "debit_credit_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Debit-Credit mismatch"
|
||||
},
|
||||
{
|
||||
"fieldname": "checked_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Checked On"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "general_and_payment_ledger_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "General and Payment Ledger mismatch"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-09 11:16:07.044484",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealth(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
checked_on: DF.Datetime | None
|
||||
debit_credit_mismatch: DF.Check
|
||||
general_and_payment_ledger_mismatch: DF.Check
|
||||
name: DF.Int | None
|
||||
voucher_no: DF.Data | None
|
||||
voucher_type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import run_ledger_health_checks
|
||||
|
||||
|
||||
class TestLedgerHealth(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.configure_monitoring_tool()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def configure_monitoring_tool(self):
|
||||
monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
monitor_settings.enable_health_monitor = True
|
||||
monitor_settings.enable_for_last_x_days = 60
|
||||
monitor_settings.debit_credit_mismatch = True
|
||||
monitor_settings.general_and_payment_ledger_mismatch = True
|
||||
exists = [x for x in monitor_settings.companies if x.company == self.company]
|
||||
if not exists:
|
||||
monitor_settings.append("companies", {"company": self.company})
|
||||
monitor_settings.save()
|
||||
|
||||
def clear_old_entries(self):
|
||||
super().clear_old_entries()
|
||||
lh = qb.DocType("Ledger Health")
|
||||
qb.from_(lh).delete().run()
|
||||
|
||||
def create_journal(self):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = self.company
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.posting_date = nowdate()
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 10000,
|
||||
},
|
||||
)
|
||||
je.append("accounts", {"account": self.income_account, "credit_in_account_currency": 10000})
|
||||
je.save().submit()
|
||||
self.je = je
|
||||
|
||||
def test_debit_credit_mismatch(self):
|
||||
self.create_journal()
|
||||
|
||||
# manually cause debit-credit mismatch
|
||||
gle = frappe.db.get_all(
|
||||
"GL Entry", filters={"voucher_no": self.je.name, "account": self.income_account}
|
||||
)[0]
|
||||
frappe.db.set_value("GL Entry", gle.name, "credit", 8000)
|
||||
|
||||
run_ledger_health_checks()
|
||||
expected = {
|
||||
"voucher_type": self.je.doctype,
|
||||
"voucher_no": self.je.name,
|
||||
"debit_credit_mismatch": True,
|
||||
"general_and_payment_ledger_mismatch": False,
|
||||
}
|
||||
actual = frappe.db.get_all(
|
||||
"Ledger Health",
|
||||
fields=[
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual[0])
|
||||
|
||||
def test_gl_and_pl_mismatch(self):
|
||||
self.create_journal()
|
||||
|
||||
# manually cause GL and PL discrepancy
|
||||
ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": self.je.name})[0]
|
||||
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", 11000)
|
||||
|
||||
run_ledger_health_checks()
|
||||
expected = {
|
||||
"voucher_type": self.je.doctype,
|
||||
"voucher_no": self.je.name,
|
||||
"debit_credit_mismatch": False,
|
||||
"general_and_payment_ledger_mismatch": True,
|
||||
}
|
||||
actual = frappe.db.get_all(
|
||||
"Ledger Health",
|
||||
fields=[
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual[0])
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Ledger Health Monitor", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-03-27 09:38:07.427997",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable_health_monitor",
|
||||
"monitor_section",
|
||||
"monitor_for_last_x_days",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
"section_break_xdsp",
|
||||
"companies"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_health_monitor",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Health Monitor"
|
||||
},
|
||||
{
|
||||
"fieldname": "monitor_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Configuration"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "debit_credit_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Debit-Credit Mismatch"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "general_and_payment_ledger_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Discrepancy between General and Payment Ledger"
|
||||
},
|
||||
{
|
||||
"default": "60",
|
||||
"fieldname": "monitor_for_last_x_days",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Monitor for Last 'X' days",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xdsp",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Companies"
|
||||
},
|
||||
{
|
||||
"fieldname": "companies",
|
||||
"fieldtype": "Table",
|
||||
"options": "Ledger Health Monitor Company"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 10:14:16.511681",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health Monitor",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealthMonitor(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.ledger_health_monitor_company.ledger_health_monitor_company import (
|
||||
LedgerHealthMonitorCompany,
|
||||
)
|
||||
|
||||
companies: DF.Table[LedgerHealthMonitorCompany]
|
||||
debit_credit_mismatch: DF.Check
|
||||
enable_health_monitor: DF.Check
|
||||
general_and_payment_ledger_mismatch: DF.Check
|
||||
monitor_for_last_x_days: DF.Int
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLedgerHealthMonitor(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-03-27 10:04:45.727054",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 10:06:22.806155",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health Monitor Company",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealthMonitorCompany(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1074,9 +1074,13 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.source_exchange_rate = 50
|
||||
pe.save()
|
||||
|
||||
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
||||
ref_details = get_reference_details(
|
||||
so.doctype, so.name, pe.paid_from_account_currency, "Customer", so.customer
|
||||
)
|
||||
expected_response = {
|
||||
"account": get_party_account("Customer", so.customer, so.company),
|
||||
"account_type": None, # only applies for Reverse Payment Entry
|
||||
"payment_type": None, # only applies for Reverse Payment Entry
|
||||
"total_amount": 5000.0,
|
||||
"outstanding_amount": 5000.0,
|
||||
"exchange_rate": 1.0,
|
||||
@@ -1543,7 +1547,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
company = "_Test Company"
|
||||
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
||||
advance_account = create_account(
|
||||
parent_account="Current Assets - _TC",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Received",
|
||||
company=company,
|
||||
account_type="Receivable",
|
||||
@@ -1599,9 +1603,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
# assert General and Payment Ledger entries post partial reconciliation
|
||||
self.expected_gle = [
|
||||
{"account": "Debtors - _TC", "debit": 0.0, "credit": 400.0},
|
||||
{"account": advance_account, "debit": 400.0, "credit": 0.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 400.0},
|
||||
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
||||
]
|
||||
self.expected_ple = [
|
||||
@@ -1612,7 +1616,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
"amount": -1000.0,
|
||||
},
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": reverse_pe.name,
|
||||
"amount": -400.0,
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"due_date",
|
||||
"bill_no",
|
||||
"payment_term",
|
||||
"account_type",
|
||||
"payment_type",
|
||||
"column_break_4",
|
||||
"total_amount",
|
||||
"outstanding_amount",
|
||||
@@ -108,12 +110,22 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment Type"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:09.578312",
|
||||
"modified": "2024-04-05 09:44:08.310593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -15,6 +15,7 @@ class PaymentEntryReference(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
account: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
allocated_amount: DF.Float
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
@@ -25,6 +26,7 @@ class PaymentEntryReference(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
payment_term: DF.Link | None
|
||||
payment_type: DF.Data | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Float
|
||||
|
||||
@@ -5,6 +5,8 @@ import unittest
|
||||
|
||||
# test_records = frappe.get_test_records('Payment Gateway Account')
|
||||
|
||||
test_ignore = ["Payment Gateway"]
|
||||
|
||||
|
||||
class TestPaymentGatewayAccount(unittest.TestCase):
|
||||
pass
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -35,10 +35,11 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:11.676098",
|
||||
"modified": "2024-04-07 11:26:42.021585",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Terms Template",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -76,6 +77,15 @@
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
|
||||
@@ -441,6 +441,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
if self.is_return:
|
||||
invoice_total = self.rounded_total or self.grand_total
|
||||
total_amount_in_payments = flt(total_amount_in_payments, self.precision("grand_total"))
|
||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
|
||||
@@ -136,7 +136,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
if (!doc.is_return && doc.docstatus == 1) {
|
||||
if (doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
||||
this.frm.add_custom_button(__("Return / Debit Note"), this.make_debit_note, __("Create"));
|
||||
this.frm.add_custom_button(
|
||||
__("Return / Debit Note"),
|
||||
this.make_debit_note.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"col_break1",
|
||||
"account_head",
|
||||
"description",
|
||||
"is_tax_withholding_account",
|
||||
"section_break_10",
|
||||
"rate",
|
||||
"accounting_dimensions_section",
|
||||
@@ -225,15 +226,23 @@
|
||||
"label": "Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:26.775139",
|
||||
"modified": "2024-04-08 19:51:36.678551",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
|
||||
@@ -33,6 +33,7 @@ class PurchaseTaxesandCharges(Document):
|
||||
description: DF.SmallText
|
||||
included_in_paid_amount: DF.Check
|
||||
included_in_print_rate: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
item_wise_tax_detail: DF.Code | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -118,7 +118,11 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
});
|
||||
|
||||
if (doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
||||
this.frm.add_custom_button(__("Return / Credit Note"), this.make_sales_return, __("Create"));
|
||||
this.frm.add_custom_button(
|
||||
__("Return / Credit Note"),
|
||||
this.make_sales_return.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -3632,6 +3632,105 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(1, len(advances))
|
||||
self.assertEqual(advances[0].reference_name, pe.name)
|
||||
|
||||
def test_taxes_merging_from_delivery_note(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
dn1 = create_delivery_note(do_not_submit=1)
|
||||
dn1.items[0].qty = 10
|
||||
dn1.items[0].rate = 100
|
||||
dn1.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Freight and Forwarding Charges - _TC",
|
||||
"description": "movement charges",
|
||||
"tax_amount": 100,
|
||||
},
|
||||
)
|
||||
dn1.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Marketing Expenses - _TC",
|
||||
"description": "marketing",
|
||||
"tax_amount": 150,
|
||||
},
|
||||
)
|
||||
dn1.save().submit()
|
||||
|
||||
dn2 = create_delivery_note(do_not_submit=1)
|
||||
dn2.items[0].qty = 5
|
||||
dn2.items[0].rate = 100
|
||||
dn2.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Freight and Forwarding Charges - _TC",
|
||||
"description": "movement charges",
|
||||
"tax_amount": 20,
|
||||
},
|
||||
)
|
||||
dn2.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Miscellaneous Expenses - _TC",
|
||||
"description": "marketing",
|
||||
"tax_amount": 60,
|
||||
},
|
||||
)
|
||||
dn2.save().submit()
|
||||
|
||||
# si = make_sales_invoice(dn1.name)
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
si.customer = dn1.customer
|
||||
si.items.clear()
|
||||
|
||||
from frappe.model.mapper import map_docs
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
expected = [
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Freight and Forwarding Charges - _TC",
|
||||
"tax_amount": 120.0,
|
||||
"total": 1520.0,
|
||||
"base_total": 1520.0,
|
||||
},
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Marketing Expenses - _TC",
|
||||
"tax_amount": 150.0,
|
||||
"total": 1670.0,
|
||||
"base_total": 1670.0,
|
||||
},
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Miscellaneous Expenses - _TC",
|
||||
"tax_amount": 60.0,
|
||||
"total": 1610.0,
|
||||
"base_total": 1610.0,
|
||||
},
|
||||
]
|
||||
actual = [
|
||||
dict(
|
||||
charge_type=x.charge_type,
|
||||
account_head=x.account_head,
|
||||
tax_amount=x.tax_amount,
|
||||
total=x.total,
|
||||
base_total=x.base_total,
|
||||
)
|
||||
for x in si.taxes
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -146,7 +146,12 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||
|
||||
cost_center = get_cost_center(inv)
|
||||
tax_row.update({"cost_center": cost_center})
|
||||
tax_row.update(
|
||||
{
|
||||
"cost_center": cost_center,
|
||||
"is_tax_withholding_account": 1,
|
||||
}
|
||||
)
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -55,10 +55,10 @@
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
{% } else { %}
|
||||
<td></td>
|
||||
|
||||
@@ -347,7 +347,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({})
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
@@ -359,7 +359,8 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# closing
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
data.append(acc_dict.totals.closing)
|
||||
data.append({})
|
||||
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
else:
|
||||
data += entries
|
||||
|
||||
@@ -380,6 +381,8 @@ def get_totals_dict():
|
||||
credit=0.0,
|
||||
debit_in_account_currency=0.0,
|
||||
credit_in_account_currency=0.0,
|
||||
debit_in_transaction_currency=None,
|
||||
credit_in_transaction_currency=None,
|
||||
)
|
||||
|
||||
return _dict(
|
||||
@@ -424,6 +427,10 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
data[key].debit_in_account_currency += gle.debit_in_account_currency
|
||||
data[key].credit_in_account_currency += gle.credit_in_account_currency
|
||||
|
||||
if filters.get("add_values_in_transaction_currency") and key not in ["opening", "closing", "total"]:
|
||||
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
|
||||
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
|
||||
|
||||
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
|
||||
"Receivable",
|
||||
"Payable",
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,11 +13,13 @@ from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||
from frappe.query_builder.functions import Round, Sum
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
create_batch,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_datetime,
|
||||
get_number_format_info,
|
||||
getdate,
|
||||
now,
|
||||
@@ -720,7 +722,19 @@ def update_reference_in_payment_entry(
|
||||
payment_entry.setup_party_account_field()
|
||||
payment_entry.set_missing_values()
|
||||
if not skip_ref_details_update_for_pe:
|
||||
payment_entry.set_missing_ref_details(ref_exchange_rate=d.exchange_rate or None)
|
||||
reference_exchange_details = frappe._dict()
|
||||
if d.against_voucher_type == "Journal Entry" and d.exchange_rate:
|
||||
reference_exchange_details.update(
|
||||
{
|
||||
"reference_doctype": d.against_voucher_type,
|
||||
"reference_name": d.against_voucher,
|
||||
"exchange_rate": d.exchange_rate,
|
||||
}
|
||||
)
|
||||
payment_entry.set_missing_ref_details(
|
||||
update_ref_details_only_for=[(d.against_voucher_type, d.against_voucher)],
|
||||
reference_exchange_details=reference_exchange_details,
|
||||
)
|
||||
payment_entry.set_amounts()
|
||||
|
||||
payment_entry.make_exchange_gain_loss_journal(
|
||||
@@ -1709,6 +1723,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
)
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
ref_doc.notify_update()
|
||||
|
||||
|
||||
def delink_original_entry(pl_entry, partial_cancel=False):
|
||||
@@ -2059,3 +2074,44 @@ def create_gain_loss_journal(
|
||||
|
||||
def get_party_types_from_account_type(account_type):
|
||||
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
|
||||
|
||||
|
||||
def run_ledger_health_checks():
|
||||
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
if health_monitor_settings.enable_health_monitor:
|
||||
period_end = getdate()
|
||||
period_start = add_days(period_end, -abs(health_monitor_settings.monitor_for_last_x_days))
|
||||
|
||||
run_date = get_datetime()
|
||||
|
||||
# Debit-Credit mismatch report
|
||||
if health_monitor_settings.debit_credit_mismatch:
|
||||
for x in health_monitor_settings.companies:
|
||||
filters = {"company": x.company, "from_date": period_start, "to_date": period_end}
|
||||
voucher_wise = frappe.get_doc("Report", "Voucher-wise Balance")
|
||||
res = voucher_wise.execute_script_report(filters=filters)
|
||||
for x in res[1]:
|
||||
doc = frappe.new_doc("Ledger Health")
|
||||
doc.voucher_type = x.voucher_type
|
||||
doc.voucher_no = x.voucher_no
|
||||
doc.debit_credit_mismatch = True
|
||||
doc.checked_on = run_date
|
||||
doc.save()
|
||||
|
||||
# General Ledger and Payment Ledger discrepancy
|
||||
if health_monitor_settings.general_and_payment_ledger_mismatch:
|
||||
for x in health_monitor_settings.companies:
|
||||
filters = {
|
||||
"company": x.company,
|
||||
"period_start_date": period_start,
|
||||
"period_end_date": period_end,
|
||||
}
|
||||
gl_pl_comparison = frappe.get_doc("Report", "General and Payment Ledger Comparison")
|
||||
res = gl_pl_comparison.execute_script_report(filters=filters)
|
||||
for x in res[1]:
|
||||
doc = frappe.new_doc("Ledger Health")
|
||||
doc.voucher_type = x.voucher_type
|
||||
doc.voucher_no = x.voucher_no
|
||||
doc.general_and_payment_ledger_mismatch = True
|
||||
doc.checked_on = run_date
|
||||
doc.save()
|
||||
|
||||
@@ -232,7 +232,11 @@ class TestAsset(AssetSetup):
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
asset.finance_books[0],
|
||||
9000,
|
||||
get_last_day(add_months(purchase_date, 1)),
|
||||
date,
|
||||
original_schedule_date=get_last_day(nowdate()),
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
self.assertEqual(
|
||||
@@ -314,7 +318,11 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||
|
||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
asset.finance_books[0],
|
||||
9000,
|
||||
get_last_day(add_months(purchase_date, 1)),
|
||||
date,
|
||||
original_schedule_date=get_last_day(nowdate()),
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
|
||||
@@ -332,7 +340,6 @@ class TestAsset(AssetSetup):
|
||||
),
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Sales Invoice", si.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
@@ -378,7 +385,7 @@ class TestAsset(AssetSetup):
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
|
||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1737.7, 37737.7]]
|
||||
|
||||
second_asset_depr_schedule = get_depr_schedule(asset.name, "Active")
|
||||
|
||||
@@ -391,7 +398,7 @@ class TestAsset(AssetSetup):
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
37742.47,
|
||||
37737.7,
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
@@ -402,7 +409,7 @@ class TestAsset(AssetSetup):
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
0.0,
|
||||
17742.47,
|
||||
17737.7,
|
||||
),
|
||||
("Debtors - _TC", 40000.0, 0.0),
|
||||
)
|
||||
@@ -707,25 +714,24 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2023-01-31", 1021.98, 1021.98],
|
||||
["2023-02-28", 923.08, 1945.06],
|
||||
["2023-03-31", 1021.98, 2967.04],
|
||||
["2023-04-30", 989.01, 3956.05],
|
||||
["2023-05-31", 1021.98, 4978.03],
|
||||
["2023-06-30", 989.01, 5967.04],
|
||||
["2023-07-31", 1021.98, 6989.02],
|
||||
["2023-08-31", 1021.98, 8011.0],
|
||||
["2023-09-30", 989.01, 9000.01],
|
||||
["2023-10-31", 1021.98, 10021.99],
|
||||
["2023-11-30", 989.01, 11011.0],
|
||||
["2023-12-31", 989.0, 12000.0],
|
||||
["2023-01-31", 1019.18, 1019.18],
|
||||
["2023-02-28", 920.55, 1939.73],
|
||||
["2023-03-31", 1019.18, 2958.91],
|
||||
["2023-04-30", 986.3, 3945.21],
|
||||
["2023-05-31", 1019.18, 4964.39],
|
||||
["2023-06-30", 986.3, 5950.69],
|
||||
["2023-07-31", 1019.18, 6969.87],
|
||||
["2023-08-31", 1019.18, 7989.05],
|
||||
["2023-09-30", 986.3, 8975.35],
|
||||
["2023-10-31", 1019.18, 9994.53],
|
||||
["2023-11-30", 986.3, 10980.83],
|
||||
["2023-12-31", 1019.17, 12000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_for_straight_line_method_for_existing_asset(self):
|
||||
|
||||
@@ -313,7 +313,6 @@ class AssetDepreciationSchedule(Document):
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
number_of_pending_depreciations,
|
||||
)
|
||||
|
||||
if not has_pro_rata or (
|
||||
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
||||
):
|
||||
@@ -338,6 +337,7 @@ class AssetDepreciationSchedule(Document):
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
date_of_disposal,
|
||||
original_schedule_date=schedule_date,
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
@@ -565,23 +565,27 @@ def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=Fa
|
||||
)
|
||||
|
||||
|
||||
def _get_pro_rata_amt(row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False):
|
||||
def _get_pro_rata_amt(
|
||||
row,
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
to_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
original_schedule_date=None,
|
||||
):
|
||||
days = date_diff(to_date, from_date)
|
||||
months = month_diff(to_date, from_date)
|
||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||
total_days = get_total_days(to_date, 12)
|
||||
total_days = get_total_days(original_schedule_date or to_date, 12)
|
||||
else:
|
||||
total_days = get_total_days(to_date, row.frequency_of_depreciation)
|
||||
|
||||
total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation)
|
||||
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
||||
|
||||
|
||||
def get_total_days(date, frequency):
|
||||
period_start_date = add_months(date, cint(frequency) * -1)
|
||||
|
||||
if is_last_day_of_the_month(date):
|
||||
period_start_date = get_last_day(period_start_date)
|
||||
|
||||
return date_diff(date, period_start_date)
|
||||
|
||||
|
||||
@@ -632,33 +636,37 @@ def get_straight_line_or_manual_depr_amount(
|
||||
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
|
||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
if row.daily_prorata_based:
|
||||
daily_depr_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(
|
||||
row.total_number_of_depreciations
|
||||
- asset.number_of_depreciations_booked
|
||||
- number_of_pending_depreciations
|
||||
- 1
|
||||
)
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
1,
|
||||
),
|
||||
add_days(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(
|
||||
row.total_number_of_depreciations
|
||||
- asset.number_of_depreciations_booked
|
||||
- number_of_pending_depreciations
|
||||
- 1
|
||||
)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
|
||||
daily_depr_amount = amount / total_days
|
||||
|
||||
to_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
)
|
||||
@@ -679,24 +687,33 @@ def get_straight_line_or_manual_depr_amount(
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
if row.daily_prorata_based:
|
||||
daily_depr_amount = (
|
||||
amount = (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)),
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(
|
||||
add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
|
||||
daily_depr_amount = amount / total_days
|
||||
|
||||
to_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
)
|
||||
@@ -708,7 +725,6 @@ def get_straight_line_or_manual_depr_amount(
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
||||
else:
|
||||
return (
|
||||
@@ -980,32 +996,35 @@ def get_depr_schedule(asset_name, status, finance_book=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
|
||||
asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
||||
asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
||||
|
||||
if not asset_depr_schedule_name:
|
||||
if not asset_depr_schedule:
|
||||
return
|
||||
|
||||
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
|
||||
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule[0].name)
|
||||
|
||||
return asset_depr_schedule_doc
|
||||
|
||||
|
||||
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
|
||||
if finance_book is None:
|
||||
finance_book_filter = ["finance_book", "is", "not set"]
|
||||
else:
|
||||
finance_book_filter = ["finance_book", "=", finance_book]
|
||||
|
||||
if isinstance(status, str):
|
||||
status = [status]
|
||||
|
||||
return frappe.db.get_value(
|
||||
filters = [
|
||||
["asset", "=", asset_name],
|
||||
["status", "in", status],
|
||||
["docstatus", "<", 2],
|
||||
]
|
||||
|
||||
if finance_book:
|
||||
filters.append(["finance_book", "=", finance_book])
|
||||
else:
|
||||
filters.append(["finance_book", "is", "not set"])
|
||||
|
||||
return frappe.get_all(
|
||||
doctype="Asset Depreciation Schedule",
|
||||
filters=[
|
||||
["asset", "=", asset_name],
|
||||
finance_book_filter,
|
||||
["status", "in", status],
|
||||
],
|
||||
filters=filters,
|
||||
limit=1,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ def get_data(filters):
|
||||
"asset_category": asset.asset_category,
|
||||
"purchase_date": asset.purchase_date,
|
||||
"asset_value": asset_value,
|
||||
"company": asset.company,
|
||||
}
|
||||
data.append(row)
|
||||
|
||||
@@ -369,30 +370,37 @@ def get_columns(filters):
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Opening Accumulated Depreciation"),
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciated Amount"),
|
||||
"fieldname": "depreciated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Value"),
|
||||
"fieldname": "asset_value",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
return [
|
||||
@@ -423,28 +431,28 @@ def get_columns(filters):
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Value"),
|
||||
"fieldname": "asset_value",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Opening Accumulated Depreciation"),
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciated Amount"),
|
||||
"fieldname": "depreciated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
@@ -469,4 +477,11 @@ def get_columns(filters):
|
||||
"options": "Location",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1127,10 +1127,17 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
po = create_purchase_order()
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated")
|
||||
|
||||
pr = make_payment_request(dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True)
|
||||
pr = make_payment_request(
|
||||
dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True, payment_request_type="Outward"
|
||||
)
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
|
||||
|
||||
pe = get_payment_entry(po.doctype, po.name).save().submit()
|
||||
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid")
|
||||
|
||||
pe.reload()
|
||||
|
||||
@@ -22,9 +22,13 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
this.frm.set_value("valid_till", frappe.datetime.add_months(this.frm.doc.transaction_date, 1));
|
||||
}
|
||||
if (this.frm.doc.docstatus === 1) {
|
||||
this.frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
||||
this.frm.add_custom_button(
|
||||
__("Purchase Order"),
|
||||
this.make_purchase_order.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create"));
|
||||
} else if (this.frm.doc.docstatus === 0) {
|
||||
this.frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -467,7 +467,16 @@ class calculate_taxes_and_totals:
|
||||
if tax.charge_type == "Actual":
|
||||
# distribute the tax amount proportionally to each item row
|
||||
actual = flt(tax.tax_amount, tax.precision("tax_amount"))
|
||||
current_tax_amount = item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
|
||||
|
||||
if tax.get("is_tax_withholding_account") and item.meta.get_field("apply_tds"):
|
||||
if not item.get("apply_tds") or not self.doc.tax_withholding_net_total:
|
||||
current_tax_amount = 0.0
|
||||
else:
|
||||
current_tax_amount = item.net_amount * actual / self.doc.tax_withholding_net_total
|
||||
else:
|
||||
current_tax_amount = (
|
||||
item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
|
||||
)
|
||||
|
||||
elif tax.charge_type == "On Net Total":
|
||||
current_tax_amount = (tax_rate / 100.0) * item.net_amount
|
||||
@@ -1091,6 +1100,11 @@ def get_rounded_tax_amount(itemised_tax, precision):
|
||||
row["tax_amount"] = flt(row["tax_amount"], precision)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_rounding_tax_settings():
|
||||
return frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax")
|
||||
|
||||
|
||||
class init_landed_taxes_and_totals:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
@@ -53,7 +53,8 @@ class TestAccountsController(FrappeTestCase):
|
||||
20 series - Sales Invoice against Journals
|
||||
30 series - Sales Invoice against Credit Notes
|
||||
40 series - Company default Cost center is unset
|
||||
50 series = Journals against Journals
|
||||
50 series - Journals against Journals
|
||||
60 series - Journals against Payment Entries
|
||||
90 series - Dimension inheritence
|
||||
"""
|
||||
|
||||
@@ -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, [])
|
||||
|
||||
@@ -349,7 +349,6 @@ doc_events = {
|
||||
"Payment Entry": {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
|
||||
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
||||
],
|
||||
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||
@@ -435,6 +434,7 @@ scheduler_events = {
|
||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
||||
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
|
||||
"erpnext.accounts.utils.run_ledger_health_checks",
|
||||
],
|
||||
"weekly": [
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||
@@ -534,6 +534,7 @@ accounting_dimension_doctypes = [
|
||||
"Supplier Quotation Item",
|
||||
"Payment Reconciliation",
|
||||
"Payment Reconciliation Allocation",
|
||||
"Payment Request",
|
||||
]
|
||||
|
||||
get_matching_queries = (
|
||||
@@ -635,3 +636,5 @@ default_log_clearing_doctypes = {
|
||||
}
|
||||
|
||||
export_python_type_annotations = True
|
||||
|
||||
fields_for_group_similar_items = ["qty", "amount"]
|
||||
|
||||
6664
erpnext/locale/ar.po
6664
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
83599
erpnext/locale/bs.po
Normal file
83599
erpnext/locale/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
6970
erpnext/locale/de.po
6970
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
83599
erpnext/locale/eo.po
Normal file
83599
erpnext/locale/eo.po
Normal file
File diff suppressed because it is too large
Load Diff
7128
erpnext/locale/es.po
7128
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
7095
erpnext/locale/fa.po
7095
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
6664
erpnext/locale/fr.po
6664
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
33258
erpnext/locale/tr.po
33258
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
@@ -238,7 +238,7 @@
|
||||
"fieldname": "rm_cost_as_per",
|
||||
"fieldtype": "Select",
|
||||
"label": "Rate Of Materials Based On",
|
||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual"
|
||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -637,7 +637,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:40.214929",
|
||||
"modified": "2024-04-02 16:22:47.518411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -147,7 +147,7 @@ class BOM(WebsiteGenerator):
|
||||
quality_inspection_template: DF.Link | None
|
||||
quantity: DF.Float
|
||||
raw_material_cost: DF.Currency
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"]
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||
route: DF.SmallText | None
|
||||
routing: DF.Link | None
|
||||
scrap_items: DF.Table[BOMScrapItem]
|
||||
@@ -737,6 +737,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
def calculate_rm_cost(self, save=False):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
|
||||
total_rm_cost = 0
|
||||
base_total_rm_cost = 0
|
||||
|
||||
@@ -745,7 +746,7 @@ class BOM(WebsiteGenerator):
|
||||
continue
|
||||
|
||||
old_rate = d.rate
|
||||
if self.rm_cost_as_per != "Manual":
|
||||
if not self.bom_creator:
|
||||
d.rate = self.get_rm_rate(
|
||||
{
|
||||
"company": self.company,
|
||||
@@ -1017,8 +1018,6 @@ def get_bom_item_rate(args, bom_doc):
|
||||
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
|
||||
price_list_data = get_price_list_rate(bom_args, item_doc)
|
||||
rate = price_list_data.price_list_rate
|
||||
elif bom_doc.rm_cost_as_per == "Manual":
|
||||
return
|
||||
|
||||
return flt(rate)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"fieldname": "rm_cost_as_per",
|
||||
"fieldtype": "Select",
|
||||
"label": "Rate Of Materials Based On",
|
||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual",
|
||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -288,7 +288,7 @@
|
||||
"link_fieldname": "bom_creator"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-27 13:06:40.535884",
|
||||
"modified": "2024-04-02 16:30:59.779190",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Creator",
|
||||
|
||||
@@ -59,7 +59,7 @@ class BOMCreator(Document):
|
||||
qty: DF.Float
|
||||
raw_material_cost: DF.Currency
|
||||
remarks: DF.TextEditor | None
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"]
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||
set_rate_based_on_warehouse: DF.Check
|
||||
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
|
||||
uom: DF.Link | None
|
||||
@@ -141,9 +141,6 @@ class BOMCreator(Document):
|
||||
self.submit()
|
||||
|
||||
def set_rate_for_items(self):
|
||||
if self.rm_cost_as_per == "Manual":
|
||||
return
|
||||
|
||||
amount = self.get_raw_material_cost()
|
||||
self.raw_material_cost = amount
|
||||
|
||||
@@ -239,6 +236,9 @@ class BOMCreator(Document):
|
||||
frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}),
|
||||
)
|
||||
|
||||
if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)):
|
||||
frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item))
|
||||
|
||||
production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
|
||||
|
||||
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
|
||||
@@ -282,7 +282,6 @@ class BOMCreator(Document):
|
||||
"allow_alternative_item": 1,
|
||||
"bom_creator": self.name,
|
||||
"bom_creator_item": bom_creator_item,
|
||||
"rm_cost_as_per": "Manual",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -182,6 +182,7 @@ class WorkstationDashboard {
|
||||
me.job_cards = [r.message];
|
||||
me.prepare_timer();
|
||||
me.update_job_card_details();
|
||||
me.frm.reload_doc();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -229,6 +230,7 @@ class WorkstationDashboard {
|
||||
me.job_cards = [r.message];
|
||||
me.prepare_timer();
|
||||
me.update_job_card_details();
|
||||
me.frm.reload_doc();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -357,6 +357,7 @@ erpnext.patches.v15_0.create_advance_payment_status
|
||||
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
||||
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,10 +210,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
});
|
||||
}
|
||||
|
||||
frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax")
|
||||
.then((round_row_wise_tax) => {
|
||||
frappe.flags.round_row_wise_tax = round_row_wise_tax;
|
||||
})
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.taxes_and_totals.get_rounding_tax_settings",
|
||||
callback: function(r) {
|
||||
frappe.flags.round_off_settings = r.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
determine_exclusive_rate() {
|
||||
|
||||
@@ -252,9 +252,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
toggle_enable_for_stock_uom(field) {
|
||||
frappe.db.get_single_value('Stock Settings', field)
|
||||
.then(value => {
|
||||
this.frm.fields_dict["items"].grid.toggle_enable("stock_qty", value);
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.doctype.stock_settings.stock_settings.get_enable_stock_uom_editing',
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
var value = r.message[field];
|
||||
this.frm.fields_dict["items"].grid.toggle_enable("stock_qty", value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
];
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
];
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
)
|
||||
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
target.delivery_date = nowdate()
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.delivery_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
||||
def test_make_sales_order_with_terms(self):
|
||||
@@ -164,6 +165,7 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.delivery_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
||||
# Remove any unknown taxes if applied
|
||||
|
||||
@@ -111,16 +111,26 @@ frappe.ui.form.on("Sales Order", {
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
|
||||
if (!value) {
|
||||
// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden.
|
||||
frm.set_value("reserve_stock", 0);
|
||||
frm.set_df_property("reserve_stock", "read_only", 1);
|
||||
frm.set_df_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "default", 0);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "read_only", 1);
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.get_stock_reservation_status",
|
||||
callback: function (r) {
|
||||
if (!r.message) {
|
||||
frm.set_value("reserve_stock", 0);
|
||||
frm.set_df_property("reserve_stock", "read_only", 1);
|
||||
frm.set_df_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "hidden", 1);
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"reserve_stock",
|
||||
"default",
|
||||
0
|
||||
);
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"reserve_stock",
|
||||
"read_only",
|
||||
1
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
import frappe.permissions
|
||||
@@ -1956,10 +1957,48 @@ class TestSalesOrder(FrappeTestCase):
|
||||
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
|
||||
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
|
||||
|
||||
def test_sales_order_advance_payment_status(self):
|
||||
@patch(
|
||||
# this also shadows one (1) call to _get_payment_gateway_controller
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
|
||||
return_value=None,
|
||||
)
|
||||
def test_sales_order_advance_payment_status(self, mocked_get_payment_url):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
|
||||
# Flow progressing to SI with payment entries "moved" from SO to SI
|
||||
so = make_sales_order(qty=1, rate=100, do_not_submit=True)
|
||||
# no-op; for optical consistency with how a webshop SO would look like
|
||||
so.order_type = "Shopping Cart"
|
||||
so.submit()
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
||||
|
||||
pr = make_payment_request(
|
||||
dt=so.doctype, dn=so.name, order_type="Shopping Cart", submit_doc=True, return_doc=True
|
||||
)
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
|
||||
|
||||
pe = pr.set_as_paid()
|
||||
pr.reload() # status updated
|
||||
pe.reload() # references moved to Sales Invoice
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pe.references[0].reference_doctype, "Sales Invoice")
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid")
|
||||
|
||||
pe.cancel()
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "Paid") # TODO: this might be a bug
|
||||
so.reload() # reload
|
||||
# regardless, since the references have already "handed-over" to SI,
|
||||
# the SO keeps its historical state at the time of hand over
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid")
|
||||
|
||||
pr.cancel()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||
) # TODO: this might be a bug; handover has happened
|
||||
|
||||
# Flow NOT progressing to SI with payment entries NOT "moved"
|
||||
so = make_sales_order(qty=1, rate=100)
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
||||
|
||||
@@ -1971,11 +2010,15 @@ class TestSalesOrder(FrappeTestCase):
|
||||
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested"
|
||||
) # here: reset
|
||||
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||
) # here: reset
|
||||
|
||||
def test_pick_list_without_rejected_materials(self):
|
||||
serial_and_batch_item = make_item(
|
||||
|
||||
@@ -547,6 +547,8 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
async on_cart_update(args) {
|
||||
frappe.dom.freeze();
|
||||
if (this.frm.doc.set_warehouse != this.settings.warehouse)
|
||||
this.frm.doc.set_warehouse = this.settings.warehouse;
|
||||
let item_row = undefined;
|
||||
try {
|
||||
let { field, value, item } = args;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user