mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 16:34:46 +00:00
Merge pull request #45704 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
"section_break_jpd0",
|
"section_break_jpd0",
|
||||||
"auto_reconcile_payments",
|
"auto_reconcile_payments",
|
||||||
"stale_days",
|
"stale_days",
|
||||||
|
"exchange_gain_loss_posting_date",
|
||||||
"invoicing_settings_tab",
|
"invoicing_settings_tab",
|
||||||
"accounts_transactions_settings_section",
|
"accounts_transactions_settings_section",
|
||||||
"over_billing_allowance",
|
"over_billing_allowance",
|
||||||
@@ -383,7 +384,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_jpd0",
|
"fieldname": "section_break_jpd0",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Payment Reconciliations"
|
"label": "Payment Reconciliation Settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -462,6 +463,14 @@
|
|||||||
"fieldname": "remarks_section",
|
"fieldname": "remarks_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Remarks Column Length"
|
"label": "Remarks Column Length"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Payment",
|
||||||
|
"description": "Only applies for Normal Payments",
|
||||||
|
"fieldname": "exchange_gain_loss_posting_date",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||||
|
"options": "Invoice\nPayment\nReconciliation Date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -469,7 +478,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-22 12:10:10.151819",
|
"modified": "2025-01-23 13:15:44.077853",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
@@ -498,4 +507,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,15 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
|
if (!frm.doc.company) {
|
||||||
|
frm.set_value("company", frappe.defaults.get_default("company"));
|
||||||
|
}
|
||||||
|
|
||||||
// Set default filter dates
|
// Set default filter dates
|
||||||
let today = frappe.datetime.get_today();
|
let today = frappe.datetime.get_today();
|
||||||
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
||||||
frm.doc.bank_statement_to_date = today;
|
frm.doc.bank_statement_to_date = today;
|
||||||
|
|
||||||
frm.trigger("bank_account");
|
frm.trigger("bank_account");
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -94,7 +99,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
|
|
||||||
make_reconciliation_tool(frm) {
|
make_reconciliation_tool(frm) {
|
||||||
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
|
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
|
||||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||||
frm.trigger("get_cleared_balance").then(() => {
|
frm.trigger("get_cleared_balance").then(() => {
|
||||||
if (
|
if (
|
||||||
frm.doc.bank_account &&
|
frm.doc.bank_account &&
|
||||||
@@ -110,7 +115,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_account_opening_balance(frm) {
|
get_account_opening_balance(frm) {
|
||||||
if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||||
args: {
|
args: {
|
||||||
@@ -125,7 +130,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_cleared_balance(frm) {
|
get_cleared_balance(frm) {
|
||||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ class PaymentEntry(AccountsController):
|
|||||||
if d.reference_doctype not in valid_reference_doctypes:
|
if d.reference_doctype not in valid_reference_doctypes:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Reference Doctype must be one of {0}").format(
|
_("Reference Doctype must be one of {0}").format(
|
||||||
comma_or(_(d) for d in valid_reference_doctypes)
|
comma_or([_(d) for d in valid_reference_doctypes])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2481,6 +2481,7 @@ def get_payment_entry(
|
|||||||
pe.paid_amount = paid_amount
|
pe.paid_amount = paid_amount
|
||||||
pe.received_amount = received_amount
|
pe.received_amount = received_amount
|
||||||
pe.letter_head = doc.get("letter_head")
|
pe.letter_head = doc.get("letter_head")
|
||||||
|
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
|
||||||
|
|
||||||
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
||||||
pe.project = doc.get("project") or reduce(
|
pe.project = doc.get("project") or reduce(
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ class PaymentReconciliation(Document):
|
|||||||
for payment in non_reconciled_payments:
|
for payment in non_reconciled_payments:
|
||||||
row = self.append("payments", {})
|
row = self.append("payments", {})
|
||||||
row.update(payment)
|
row.update(payment)
|
||||||
|
row.is_advance = payment.book_advance_payments_in_separate_party_account
|
||||||
|
|
||||||
def get_invoice_entries(self):
|
def get_invoice_entries(self):
|
||||||
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
|
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
|
||||||
@@ -354,6 +355,9 @@ class PaymentReconciliation(Document):
|
|||||||
def allocate_entries(self, args):
|
def allocate_entries(self, args):
|
||||||
self.validate_entries()
|
self.validate_entries()
|
||||||
|
|
||||||
|
exc_gain_loss_posting_date = frappe.db.get_single_value(
|
||||||
|
"Accounts Settings", "exchange_gain_loss_posting_date", cache=True
|
||||||
|
)
|
||||||
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
|
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
|
||||||
default_exchange_gain_loss_account = frappe.get_cached_value(
|
default_exchange_gain_loss_account = frappe.get_cached_value(
|
||||||
"Company", self.company, "exchange_gain_loss_account"
|
"Company", self.company, "exchange_gain_loss_account"
|
||||||
@@ -380,6 +384,11 @@ class PaymentReconciliation(Document):
|
|||||||
res.difference_account = default_exchange_gain_loss_account
|
res.difference_account = default_exchange_gain_loss_account
|
||||||
res.exchange_rate = inv.get("exchange_rate")
|
res.exchange_rate = inv.get("exchange_rate")
|
||||||
res.update({"gain_loss_posting_date": pay.get("posting_date")})
|
res.update({"gain_loss_posting_date": pay.get("posting_date")})
|
||||||
|
if not pay.get("is_advance"):
|
||||||
|
if exc_gain_loss_posting_date == "Invoice":
|
||||||
|
res.update({"gain_loss_posting_date": inv.get("invoice_date")})
|
||||||
|
elif exc_gain_loss_posting_date == "Reconciliation Date":
|
||||||
|
res.update({"gain_loss_posting_date": nowdate()})
|
||||||
|
|
||||||
if pay.get("amount") == 0:
|
if pay.get("amount") == 0:
|
||||||
entries.append(res)
|
entries.append(res)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"is_advance",
|
"is_advance",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"difference_amount",
|
"difference_amount",
|
||||||
|
"gain_loss_posting_date",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
"difference_account",
|
"difference_account",
|
||||||
"exchange_rate",
|
"exchange_rate",
|
||||||
@@ -153,11 +154,16 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Reconciled"
|
"label": "Reconciled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "gain_loss_posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Difference Posting Date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-03-20 21:05:43.121945",
|
"modified": "2025-01-23 16:09:01.058574",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Payment Reconciliation Log Allocations",
|
"name": "Process Payment Reconciliation Log Allocations",
|
||||||
|
|||||||
@@ -262,9 +262,12 @@ def get_recipients_and_cc(customer, doc):
|
|||||||
recipients = []
|
recipients = []
|
||||||
for clist in doc.customers:
|
for clist in doc.customers:
|
||||||
if clist.customer == customer:
|
if clist.customer == customer:
|
||||||
recipients.append(clist.billing_email)
|
if clist.billing_email:
|
||||||
|
for email in clist.billing_email.split(","):
|
||||||
|
recipients.append(email.strip())
|
||||||
if doc.primary_mandatory and clist.primary_email:
|
if doc.primary_mandatory and clist.primary_email:
|
||||||
recipients.append(clist.primary_email)
|
for email in clist.primary_email.split(","):
|
||||||
|
recipients.append(email.strip())
|
||||||
cc = []
|
cc = []
|
||||||
if doc.cc_to != "":
|
if doc.cc_to != "":
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
|
|
||||||
if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
|
if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
|
||||||
|
|
||||||
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
|
let payment_terms_template = this.frm.doc.payment_terms_template;
|
||||||
|
|
||||||
|
erpnext.utils.get_party_details(
|
||||||
|
this.frm,
|
||||||
|
"erpnext.accounts.party.get_party_details",
|
||||||
{
|
{
|
||||||
posting_date: this.frm.doc.posting_date,
|
posting_date: this.frm.doc.posting_date,
|
||||||
bill_date: this.frm.doc.bill_date,
|
bill_date: this.frm.doc.bill_date,
|
||||||
@@ -320,7 +324,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
||||||
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
||||||
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
||||||
})
|
|
||||||
|
// while duplicating, don't change payment terms
|
||||||
|
if (me.frm.doc.__run_link_triggers === false) {
|
||||||
|
me.frm.set_value("payment_terms_template", payment_terms_template);
|
||||||
|
me.frm.refresh_field("payment_terms_template");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_tds(frm) {
|
apply_tds(frm) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"add_total_row": 1,
|
"add_total_row": 0,
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"creation": "2013-02-25 17:03:34",
|
"creation": "2013-02-25 17:03:34",
|
||||||
"disable_prepared_report": 0,
|
"disable_prepared_report": 0,
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"filters": [],
|
"filters": [],
|
||||||
"idx": 3,
|
"idx": 3,
|
||||||
"is_standard": "Yes",
|
"is_standard": "Yes",
|
||||||
"modified": "2022-02-11 10:18:36.956558",
|
"modified": "2025-01-27 18:40:24.493829",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Gross Profit",
|
"name": "Gross Profit",
|
||||||
|
|||||||
@@ -166,7 +166,14 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
|||||||
# removing Item Code and Item Name columns
|
# removing Item Code and Item Name columns
|
||||||
del columns[4:6]
|
del columns[4:6]
|
||||||
|
|
||||||
|
total_base_amount = 0
|
||||||
|
total_buying_amount = 0
|
||||||
|
|
||||||
for src in gross_profit_data.si_list:
|
for src in gross_profit_data.si_list:
|
||||||
|
if src.indent == 1:
|
||||||
|
total_base_amount += src.base_amount or 0.0
|
||||||
|
total_buying_amount += src.buying_amount or 0.0
|
||||||
|
|
||||||
row = frappe._dict()
|
row = frappe._dict()
|
||||||
row.indent = src.indent
|
row.indent = src.indent
|
||||||
row.parent_invoice = src.parent_invoice
|
row.parent_invoice = src.parent_invoice
|
||||||
@@ -177,6 +184,27 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
|||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
|
total_gross_profit = total_base_amount - total_buying_amount
|
||||||
|
data.append(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"sales_invoice": "Total",
|
||||||
|
"qty": None,
|
||||||
|
"avg._selling_rate": None,
|
||||||
|
"valuation_rate": None,
|
||||||
|
"selling_amount": total_base_amount,
|
||||||
|
"buying_amount": total_buying_amount,
|
||||||
|
"gross_profit": total_gross_profit,
|
||||||
|
"gross_profit_%": flt(
|
||||||
|
(total_gross_profit / total_base_amount) * 100.0,
|
||||||
|
cint(frappe.db.get_default("currency_precision")) or 3,
|
||||||
|
)
|
||||||
|
if total_base_amount
|
||||||
|
else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
||||||
for src in gross_profit_data.grouped_data:
|
for src in gross_profit_data.grouped_data:
|
||||||
|
|||||||
@@ -558,3 +558,33 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||||
|
|
||||||
|
def test_gross_profit_groupby_invoices(self):
|
||||||
|
create_sales_invoice(
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
item_code=self.item,
|
||||||
|
item_name=self.item,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
update_stock=0,
|
||||||
|
currency="INR",
|
||||||
|
income_account=self.income_account,
|
||||||
|
expense_account=self.expense_account,
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
_, data = execute(filters=filters)
|
||||||
|
total = data[-1]
|
||||||
|
|
||||||
|
self.assertEqual(total.selling_amount, 100.0)
|
||||||
|
self.assertEqual(total.buying_amount, 0.0)
|
||||||
|
self.assertEqual(total.gross_profit, 100.0)
|
||||||
|
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||||
|
|||||||
@@ -205,9 +205,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(is_drop_ship && doc.status!="Delivered") {
|
if (is_drop_ship && doc.status != "Delivered") {
|
||||||
this.frm.add_custom_button(__('Delivered'),
|
this.frm.add_custom_button(
|
||||||
this.delivered_by_supplier, __("Status"));
|
__("Delivered"),
|
||||||
|
this.delivered_by_supplier.bind(this),
|
||||||
|
__("Status")
|
||||||
|
);
|
||||||
|
|
||||||
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
|
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
|
||||||
}
|
}
|
||||||
@@ -582,4 +585,4 @@ frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) {
|
|||||||
if (frm.doc.is_old_subcontracting_flow) {
|
if (frm.doc.is_old_subcontracting_flow) {
|
||||||
erpnext.buying.get_default_bom(frm);
|
erpnext.buying.get_default_bom(frm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ from datetime import datetime
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, getdate, nowdate
|
from frappe.utils import add_days, getdate, nowdate
|
||||||
|
from frappe.utils.data import getdate as convert_to_date
|
||||||
|
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||||
@@ -707,6 +708,67 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
self.assertEqual(exc_je_for_si, [])
|
self.assertEqual(exc_je_for_si, [])
|
||||||
self.assertEqual(exc_je_for_pe, [])
|
self.assertEqual(exc_je_for_pe, [])
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"exchange_gain_loss_posting_date": "Reconciliation Date"})
|
||||||
|
def test_17_gain_loss_posting_date_for_normal_payment(self):
|
||||||
|
# Sales Invoice in Foreign Currency
|
||||||
|
rate = 80
|
||||||
|
rate_in_account_currency = 1
|
||||||
|
|
||||||
|
adv_date = convert_to_date(add_days(nowdate(), -2))
|
||||||
|
inv_date = convert_to_date(add_days(nowdate(), -1))
|
||||||
|
|
||||||
|
si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=rate_in_account_currency)
|
||||||
|
|
||||||
|
# Test payments with different exchange rates
|
||||||
|
pe = self.create_payment_entry(posting_date=adv_date, amount=1, source_exc_rate=75.1).save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.from_invoice_date = add_days(nowdate(), -1)
|
||||||
|
pr.to_invoice_date = nowdate()
|
||||||
|
pr.from_payment_date = add_days(nowdate(), -2)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Outstanding in both currencies should be '0'
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.outstanding_amount, 0)
|
||||||
|
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||||
|
|
||||||
|
# Exchange Gain/Loss Journal should've been created.
|
||||||
|
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||||
|
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||||
|
self.assertNotEqual(exc_je_for_si, [])
|
||||||
|
self.assertEqual(len(exc_je_for_si), 1)
|
||||||
|
self.assertEqual(len(exc_je_for_pe), 1)
|
||||||
|
self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
getdate(nowdate()), frappe.db.get_value("Journal Entry", exc_je_for_pe[0].parent, "posting_date")
|
||||||
|
)
|
||||||
|
# Cancel Payment
|
||||||
|
pe.reload()
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
# outstanding should be same as grand total
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.outstanding_amount, rate_in_account_currency)
|
||||||
|
self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency)
|
||||||
|
|
||||||
|
# Exchange Gain/Loss Journal should've been cancelled
|
||||||
|
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||||
|
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||||
|
self.assertEqual(exc_je_for_si, [])
|
||||||
|
self.assertEqual(exc_je_for_pe, [])
|
||||||
|
|
||||||
def test_20_journal_against_sales_invoice(self):
|
def test_20_journal_against_sales_invoice(self):
|
||||||
# Invoice in Foreign Currency
|
# Invoice in Foreign Currency
|
||||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||||
|
|||||||
@@ -375,7 +375,7 @@
|
|||||||
"depends_on": "eval:!doc.__islocal",
|
"depends_on": "eval:!doc.__islocal",
|
||||||
"fieldname": "notes_tab",
|
"fieldname": "notes_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Comments"
|
"label": "Notes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
@@ -514,7 +514,7 @@
|
|||||||
"idx": 5,
|
"idx": 5,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-01 18:46:49.468526",
|
"modified": "2025-01-31 13:40:08.094759",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Lead",
|
"name": "Lead",
|
||||||
|
|||||||
@@ -368,3 +368,5 @@ erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
|
|||||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||||
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
|
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
|
||||||
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||||
|
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||||
|
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||||
|
|||||||
5
erpnext/patches/v14_0/disable_add_row_in_gross_profit.py
Normal file
5
erpnext/patches/v14_0/disable_add_row_in_gross_profit.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.db.set_value("Report", "Gross Profit", "add_total_row", 0)
|
||||||
@@ -635,7 +635,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
i.item_code === item_code &&
|
i.item_code === item_code &&
|
||||||
(!has_batch_no || (has_batch_no && i.batch_no === batch_no)) &&
|
(!has_batch_no || (has_batch_no && i.batch_no === batch_no)) &&
|
||||||
i.uom === uom &&
|
i.uom === uom &&
|
||||||
i.rate == rate
|
i.price_list_rate === flt(rate)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -871,5 +871,6 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "employee_name"
|
"title_field": "employee_name",
|
||||||
}
|
"track_changes": 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ class ClosingStockBalance(Document):
|
|||||||
"item_group": self.item_group,
|
"item_group": self.item_group,
|
||||||
"warehouse_type": self.warehouse_type,
|
"warehouse_type": self.warehouse_type,
|
||||||
"include_uom": self.include_uom,
|
"include_uom": self.include_uom,
|
||||||
"ignore_closing_balance": 1,
|
|
||||||
"show_variant_attributes": 1,
|
"show_variant_attributes": 1,
|
||||||
"show_stock_ageing_data": 1,
|
"show_stock_ageing_data": 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, flt, get_table_name, getdate
|
from frappe.utils import cint, flt, get_datetime, get_table_name, getdate
|
||||||
from pypika import functions as fn
|
from pypika import functions as fn
|
||||||
|
|
||||||
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
||||||
@@ -99,6 +99,8 @@ def get_stock_ledger_entries(filters):
|
|||||||
if not filters.get("to_date"):
|
if not filters.get("to_date"):
|
||||||
frappe.throw(_("'To Date' is required"))
|
frappe.throw(_("'To Date' is required"))
|
||||||
|
|
||||||
|
to_date = get_datetime(filters.get("to_date") + " 23:59:59")
|
||||||
|
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
@@ -113,7 +115,7 @@ def get_stock_ledger_entries(filters):
|
|||||||
(sle.docstatus < 2)
|
(sle.docstatus < 2)
|
||||||
& (sle.is_cancelled == 0)
|
& (sle.is_cancelled == 0)
|
||||||
& (fn.IfNull(sle.batch_no, "") != "")
|
& (fn.IfNull(sle.batch_no, "") != "")
|
||||||
& (sle.posting_date <= filters["to_date"])
|
& (sle.posting_datetime <= to_date)
|
||||||
)
|
)
|
||||||
.groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
|
.groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
|
||||||
.orderby(sle.item_code, sle.warehouse)
|
.orderby(sle.item_code, sle.warehouse)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from operator import itemgetter
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, date_diff, flt
|
from frappe.utils import cint, date_diff, flt, get_datetime
|
||||||
|
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
@@ -387,6 +387,7 @@ class FIFOSlots:
|
|||||||
def __get_stock_ledger_entries(self) -> list[dict]:
|
def __get_stock_ledger_entries(self) -> list[dict]:
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
item = self.__get_item_query() # used as derived table in sle query
|
item = self.__get_item_query() # used as derived table in sle query
|
||||||
|
to_date = get_datetime(self.filters.get("to_date") + " 23:59:59")
|
||||||
|
|
||||||
sle_query = (
|
sle_query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
@@ -411,7 +412,7 @@ class FIFOSlots:
|
|||||||
.where(
|
.where(
|
||||||
(sle.item_code == item.name)
|
(sle.item_code == item.name)
|
||||||
& (sle.company == self.filters.get("company"))
|
& (sle.company == self.filters.get("company"))
|
||||||
& (sle.posting_date <= self.filters.get("to_date"))
|
& (sle.posting_datetime <= to_date)
|
||||||
& (sle.is_cancelled != 1)
|
& (sle.is_cancelled != 1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -428,7 +429,7 @@ class FIFOSlots:
|
|||||||
if warehouses:
|
if warehouses:
|
||||||
sle_query = sle_query.where(sle.warehouse.isin(warehouses))
|
sle_query = sle_query.where(sle.warehouse.isin(warehouses))
|
||||||
|
|
||||||
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)
|
sle_query = sle_query.orderby(sle.posting_datetime, sle.creation)
|
||||||
|
|
||||||
return sle_query.run(as_dict=True, as_iterator=True)
|
return sle_query.run(as_dict=True, as_iterator=True)
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,6 @@ class StockBalanceReport:
|
|||||||
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
|
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
|
||||||
.orderby(sle.posting_datetime)
|
.orderby(sle.posting_datetime)
|
||||||
.orderby(sle.creation)
|
.orderby(sle.creation)
|
||||||
.orderby(sle.actual_qty)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
query = self.apply_inventory_dimensions_filters(query, sle)
|
query = self.apply_inventory_dimensions_filters(query, sle)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import CombineDatetime
|
from frappe.query_builder.functions import CombineDatetime
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt, get_datetime
|
||||||
|
|
||||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
@@ -264,6 +264,9 @@ def get_columns(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_stock_ledger_entries(filters, items):
|
def get_stock_ledger_entries(filters, items):
|
||||||
|
from_date = get_datetime(filters.from_date + " 00:00:00")
|
||||||
|
to_date = get_datetime(filters.to_date + " 23:59:59")
|
||||||
|
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
@@ -286,12 +289,8 @@ def get_stock_ledger_entries(filters, items):
|
|||||||
sle.serial_no,
|
sle.serial_no,
|
||||||
sle.project,
|
sle.project,
|
||||||
)
|
)
|
||||||
.where(
|
.where((sle.docstatus < 2) & (sle.is_cancelled == 0) & (sle.posting_datetime[from_date:to_date]))
|
||||||
(sle.docstatus < 2)
|
.orderby(sle.posting_datetime)
|
||||||
& (sle.is_cancelled == 0)
|
|
||||||
& (sle.posting_date[filters.from_date : filters.to_date])
|
|
||||||
)
|
|
||||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
|
|
||||||
.orderby(sle.creation)
|
.orderby(sle.creation)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user