mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-27 08:54:45 +00:00
Merge pull request #42744 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not amount:
|
if not amount:
|
||||||
return
|
|
||||||
|
|
||||||
gl_posting_date = end_date
|
|
||||||
prev_posting_date = None
|
|
||||||
# check if books nor frozen till endate:
|
|
||||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
|
||||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
|
||||||
prev_posting_date = end_date
|
prev_posting_date = end_date
|
||||||
|
|
||||||
if via_journal_entry:
|
|
||||||
book_revenue_via_journal_entry(
|
|
||||||
doc,
|
|
||||||
credit_account,
|
|
||||||
debit_account,
|
|
||||||
amount,
|
|
||||||
base_amount,
|
|
||||||
gl_posting_date,
|
|
||||||
project,
|
|
||||||
account_currency,
|
|
||||||
item.cost_center,
|
|
||||||
item,
|
|
||||||
deferred_process,
|
|
||||||
submit_journal_entry,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
make_gl_entries(
|
gl_posting_date = end_date
|
||||||
doc,
|
prev_posting_date = None
|
||||||
credit_account,
|
# check if books nor frozen till endate:
|
||||||
debit_account,
|
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||||
against,
|
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||||
amount,
|
prev_posting_date = end_date
|
||||||
base_amount,
|
|
||||||
gl_posting_date,
|
if via_journal_entry:
|
||||||
project,
|
book_revenue_via_journal_entry(
|
||||||
account_currency,
|
doc,
|
||||||
item.cost_center,
|
credit_account,
|
||||||
item,
|
debit_account,
|
||||||
deferred_process,
|
amount,
|
||||||
)
|
base_amount,
|
||||||
|
gl_posting_date,
|
||||||
|
project,
|
||||||
|
account_currency,
|
||||||
|
item.cost_center,
|
||||||
|
item,
|
||||||
|
deferred_process,
|
||||||
|
submit_journal_entry,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
make_gl_entries(
|
||||||
|
doc,
|
||||||
|
credit_account,
|
||||||
|
debit_account,
|
||||||
|
against,
|
||||||
|
amount,
|
||||||
|
base_amount,
|
||||||
|
gl_posting_date,
|
||||||
|
project,
|
||||||
|
account_currency,
|
||||||
|
item.cost_center,
|
||||||
|
item,
|
||||||
|
deferred_process,
|
||||||
|
)
|
||||||
|
|
||||||
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
||||||
if frappe.flags.deferred_accounting_error:
|
if frappe.flags.deferred_accounting_error:
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
hide_unhide_fields: function(frm) {
|
hide_unhide_fields: function(frm) {
|
||||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
|
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company)?.default_currency: "";
|
||||||
|
|
||||||
frm.toggle_display("source_exchange_rate",
|
frm.toggle_display("source_exchange_rate",
|
||||||
(frm.doc.paid_amount && frm.doc.paid_from_account_currency != company_currency));
|
(frm.doc.paid_amount && frm.doc.paid_from_account_currency != company_currency));
|
||||||
|
|||||||
@@ -284,6 +284,17 @@ class PaymentRequest(Document):
|
|||||||
payment_entry.received_amount = amount
|
payment_entry.received_amount = amount
|
||||||
payment_entry.get("references")[0].allocated_amount = amount
|
payment_entry.get("references")[0].allocated_amount = amount
|
||||||
|
|
||||||
|
# Update 'Paid Amount' on Forex transactions
|
||||||
|
if self.currency != ref_doc.company_currency:
|
||||||
|
if (
|
||||||
|
self.payment_request_type == "Outward"
|
||||||
|
and payment_entry.paid_from_account_currency == ref_doc.company_currency
|
||||||
|
and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
|
||||||
|
):
|
||||||
|
payment_entry.paid_amount = payment_entry.base_paid_amount = (
|
||||||
|
payment_entry.target_exchange_rate * payment_entry.received_amount
|
||||||
|
)
|
||||||
|
|
||||||
for dimension in get_accounting_dimensions():
|
for dimension in get_accounting_dimensions():
|
||||||
payment_entry.update({dimension: self.get(dimension)})
|
payment_entry.update({dimension: self.get(dimension)})
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ payment_method = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestPaymentRequest(unittest.TestCase):
|
class TestPaymentRequest(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||||
@@ -260,3 +262,19 @@ class TestPaymentRequest(unittest.TestCase):
|
|||||||
# Try to make Payment Request more than SO amount, should give validation
|
# Try to make Payment Request more than SO amount, should give validation
|
||||||
pr2.grand_total = 900
|
pr2.grand_total = 900
|
||||||
self.assertRaises(frappe.ValidationError, pr2.save)
|
self.assertRaises(frappe.ValidationError, pr2.save)
|
||||||
|
|
||||||
|
def test_conversion_on_foreign_currency_accounts(self):
|
||||||
|
po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1)
|
||||||
|
po_doc.conversion_rate = 80
|
||||||
|
po_doc.items[0].qty = 1
|
||||||
|
po_doc.items[0].rate = 10
|
||||||
|
po_doc.save().submit()
|
||||||
|
|
||||||
|
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
|
||||||
|
pr = frappe.get_doc(pr).save().submit()
|
||||||
|
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
self.assertEqual(pe.base_paid_amount, 800)
|
||||||
|
self.assertEqual(pe.paid_amount, 800)
|
||||||
|
self.assertEqual(pe.base_received_amount, 800)
|
||||||
|
self.assertEqual(pe.received_amount, 10)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"cost_center",
|
"cost_center",
|
||||||
"territory",
|
"territory",
|
||||||
"ignore_exchange_rate_revaluation_journals",
|
"ignore_exchange_rate_revaluation_journals",
|
||||||
|
"ignore_cr_dr_notes",
|
||||||
"column_break_14",
|
"column_break_14",
|
||||||
"to_date",
|
"to_date",
|
||||||
"finance_book",
|
"finance_book",
|
||||||
@@ -381,10 +382,16 @@
|
|||||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "ignore_cr_dr_notes",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Ignore System Generated Credit / Debit Notes"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-18 12:20:08.965120",
|
"modified": "2024-08-13 10:41:18.381165",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts",
|
"name": "Process Statement Of Accounts",
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ def get_statement_dict(doc, get_statement_dict=False):
|
|||||||
if doc.ignore_exchange_rate_revaluation_journals:
|
if doc.ignore_exchange_rate_revaluation_journals:
|
||||||
filters.update({"ignore_err": True})
|
filters.update({"ignore_err": True})
|
||||||
|
|
||||||
|
if doc.ignore_cr_dr_notes:
|
||||||
|
filters.update({"ignore_cr_dr_notes": True})
|
||||||
|
|
||||||
if doc.report == "General Ledger":
|
if doc.report == "General Ledger":
|
||||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||||
col, res = get_soa(filters)
|
col, res = get_soa(filters)
|
||||||
|
|||||||
@@ -1294,6 +1294,10 @@ class SalesInvoice(SellingController):
|
|||||||
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
||||||
payment_mode.base_amount -= flt(self.change_amount)
|
payment_mode.base_amount -= flt(self.change_amount)
|
||||||
|
|
||||||
|
against_voucher = self.name
|
||||||
|
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||||
|
against_voucher = self.return_against
|
||||||
|
|
||||||
if payment_mode.base_amount:
|
if payment_mode.base_amount:
|
||||||
# POS, make payment entries
|
# POS, make payment entries
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
@@ -1307,7 +1311,7 @@ class SalesInvoice(SellingController):
|
|||||||
"credit_in_account_currency": payment_mode.base_amount
|
"credit_in_account_currency": payment_mode.base_amount
|
||||||
if self.party_account_currency == self.company_currency
|
if self.party_account_currency == self.company_currency
|
||||||
else payment_mode.amount,
|
else payment_mode.amount,
|
||||||
"against_voucher": self.name,
|
"against_voucher": against_voucher,
|
||||||
"against_voucher_type": self.doctype,
|
"against_voucher_type": self.doctype,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import copy
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import qb
|
||||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||||
from frappe.model.naming import make_autoname
|
from frappe.model.naming import make_autoname
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
@@ -3025,6 +3026,84 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
party_link.delete()
|
party_link.delete()
|
||||||
frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 0)
|
frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 0)
|
||||||
|
|
||||||
|
def test_sales_invoice_against_supplier_usd_with_dimensions(self):
|
||||||
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||||
|
make_customer,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||||
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
|
||||||
|
# create a customer
|
||||||
|
customer = make_customer(customer="_Test Common Supplier USD")
|
||||||
|
cust_doc = frappe.get_doc("Customer", customer)
|
||||||
|
cust_doc.default_currency = "USD"
|
||||||
|
cust_doc.save()
|
||||||
|
# create a supplier
|
||||||
|
supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
|
||||||
|
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||||
|
supp_doc.default_currency = "USD"
|
||||||
|
supp_doc.save()
|
||||||
|
|
||||||
|
# create a party link between customer & supplier
|
||||||
|
party_link = create_party_link("Supplier", supplier, customer)
|
||||||
|
|
||||||
|
# enable common party accounting
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||||
|
|
||||||
|
# create a dimension and make it mandatory
|
||||||
|
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
|
||||||
|
dim = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Accounting Dimension",
|
||||||
|
"document_type": "Department",
|
||||||
|
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
dim.save()
|
||||||
|
else:
|
||||||
|
dim = frappe.get_doc(
|
||||||
|
"Accounting Dimension",
|
||||||
|
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
|
||||||
|
)
|
||||||
|
dim.disabled = False
|
||||||
|
dim.dimension_defaults = []
|
||||||
|
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
|
||||||
|
dim.save()
|
||||||
|
|
||||||
|
# create a sales invoice
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
|
||||||
|
)
|
||||||
|
si.department = "All Departments"
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
# check outstanding of sales invoice
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Paid")
|
||||||
|
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||||
|
|
||||||
|
# check creation of journal entry
|
||||||
|
jv = frappe.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
{
|
||||||
|
"account": si.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": si.customer,
|
||||||
|
"reference_type": si.doctype,
|
||||||
|
"reference_name": si.name,
|
||||||
|
"department": "All Departments",
|
||||||
|
},
|
||||||
|
pluck="credit_in_account_currency",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(jv)
|
||||||
|
self.assertEqual(jv[0], si.grand_total)
|
||||||
|
|
||||||
|
dim.disabled = True
|
||||||
|
dim.save()
|
||||||
|
party_link.delete()
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||||
|
|
||||||
def test_payment_statuses(self):
|
def test_payment_statuses(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
@@ -3512,6 +3591,40 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
def test_pos_returns_without_update_outstanding_for_self(self):
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||||
|
|
||||||
|
pos_profile = make_pos_profile()
|
||||||
|
pos_profile.payments = []
|
||||||
|
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
|
||||||
|
pos_profile.save()
|
||||||
|
|
||||||
|
pos = create_sales_invoice(qty=10, do_not_save=True)
|
||||||
|
pos.is_pos = 1
|
||||||
|
pos.pos_profile = pos_profile.name
|
||||||
|
pos.append(
|
||||||
|
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
||||||
|
)
|
||||||
|
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
||||||
|
pos.save().submit()
|
||||||
|
|
||||||
|
pos_return = make_sales_return(pos.name)
|
||||||
|
pos_return.update_outstanding_for_self = False
|
||||||
|
pos_return.save().submit()
|
||||||
|
|
||||||
|
gle = qb.DocType("GL Entry")
|
||||||
|
res = (
|
||||||
|
qb.from_(gle)
|
||||||
|
.select(gle.against_voucher)
|
||||||
|
.distinct()
|
||||||
|
.where(
|
||||||
|
gle.is_cancelled.eq(0) & gle.voucher_no.eq(pos_return.name) & gle.against_voucher.notnull()
|
||||||
|
)
|
||||||
|
.run(as_list=1)
|
||||||
|
)
|
||||||
|
self.assertEqual(len(res), 1)
|
||||||
|
self.assertEqual(res[0][0], pos_return.return_against)
|
||||||
|
|
||||||
|
|
||||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||||
gl_entries = frappe.db.sql(
|
gl_entries = frappe.db.sql(
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ frappe.ui.form.on("Subscription", {
|
|||||||
frm.add_custom_button(__("Fetch Subscription Updates"), () =>
|
frm.add_custom_button(__("Fetch Subscription Updates"), () =>
|
||||||
frm.events.get_subscription_updates(frm)
|
frm.events.get_subscription_updates(frm)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Force-Fetch Subscription Updates"),
|
||||||
|
() => frm.trigger("force_fetch_subscription_updates"),
|
||||||
|
__("Actions")
|
||||||
|
);
|
||||||
} else if (frm.doc.status === "Cancelled") {
|
} else if (frm.doc.status === "Cancelled") {
|
||||||
frm.add_custom_button(__("Restart Subscription"), () =>
|
frm.add_custom_button(__("Restart Subscription"), () =>
|
||||||
frm.events.renew_this_subscription(frm)
|
frm.events.renew_this_subscription(frm)
|
||||||
@@ -96,4 +102,11 @@ frappe.ui.form.on("Subscription", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
force_fetch_subscription_updates: function (frm) {
|
||||||
|
frm.call("force_fetch_subscription_updates").then((r) => {
|
||||||
|
if (!r.exec) {
|
||||||
|
frm.reload_doc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -674,6 +674,31 @@ class Subscription(Document):
|
|||||||
if invoice:
|
if invoice:
|
||||||
return invoice.precision("grand_total")
|
return invoice.precision("grand_total")
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def force_fetch_subscription_updates(self):
|
||||||
|
"""
|
||||||
|
Process Subscription and create Invoices even if current date doesn't lie between current_invoice_start and currenct_invoice_end
|
||||||
|
It makes use of 'Proces Subscription' to force processing in a specific 'posting_date'
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Don't process future subscriptions
|
||||||
|
if nowdate() < self.current_invoice_start:
|
||||||
|
frappe.msgprint(_("Subscription for Future dates cannot be processed."))
|
||||||
|
return
|
||||||
|
|
||||||
|
processing_date = None
|
||||||
|
if self.generate_invoice_at == "Beginning of the current subscription period":
|
||||||
|
processing_date = self.current_invoice_start
|
||||||
|
elif self.generate_invoice_at == "End of the current subscription period":
|
||||||
|
processing_date = self.current_invoice_end
|
||||||
|
elif self.generate_invoice_at == "Days before the current subscription period":
|
||||||
|
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
|
||||||
|
|
||||||
|
process_subscription = frappe.new_doc("Process Subscription")
|
||||||
|
process_subscription.posting_date = processing_date
|
||||||
|
process_subscription.subscription = self.name
|
||||||
|
process_subscription.save().submit()
|
||||||
|
|
||||||
|
|
||||||
def get_calendar_months(billing_interval):
|
def get_calendar_months(billing_interval):
|
||||||
calendar_months = []
|
calendar_months = []
|
||||||
|
|||||||
@@ -712,3 +712,18 @@ class TestSubscription(FrappeTestCase):
|
|||||||
self.assertEqual(pi.total, 55333.33)
|
self.assertEqual(pi.total, 55333.33)
|
||||||
|
|
||||||
subscription.delete()
|
subscription.delete()
|
||||||
|
|
||||||
|
def test_future_subscription(self):
|
||||||
|
"""Force-Fetch should not process future subscriptions"""
|
||||||
|
subscription = frappe.new_doc("Subscription")
|
||||||
|
subscription.party_type = "Customer"
|
||||||
|
subscription.party = "_Test Customer"
|
||||||
|
subscription.generate_invoice_at_period_start = 1
|
||||||
|
subscription.generate_new_invoices_past_due_date = 1
|
||||||
|
subscription.start_date = add_months(nowdate(), 1)
|
||||||
|
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||||
|
subscription.save()
|
||||||
|
|
||||||
|
subscription.force_fetch_subscription_updates()
|
||||||
|
subscription.reload()
|
||||||
|
self.assertEqual(len(subscription.invoices), 0)
|
||||||
|
|||||||
@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
|||||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||||
]
|
]
|
||||||
self.assertEqual(report.period_total, expected)
|
self.assertEqual(report.period_total, expected)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||||
|
)
|
||||||
|
def test_zero_amount(self):
|
||||||
|
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||||
|
item = frappe.get_doc("Item", self.item)
|
||||||
|
item.enable_deferred_expense = 1
|
||||||
|
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||||
|
item.no_of_months_exp = 12
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
supplier=self.supplier,
|
||||||
|
is_return=False,
|
||||||
|
update_stock=False,
|
||||||
|
posting_date=frappe.utils.datetime.date(2021, 12, 30),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
do_not_save=True,
|
||||||
|
rate=3910,
|
||||||
|
price_list_rate=3910,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
qty=1,
|
||||||
|
)
|
||||||
|
pi.set_posting_time = True
|
||||||
|
pi.items[0].enable_deferred_expense = 1
|
||||||
|
pi.items[0].service_start_date = "2021-12-30"
|
||||||
|
pi.items[0].service_end_date = "2022-12-30"
|
||||||
|
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||||
|
pi.items[0].expense_account = self.expense_account
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pda = frappe.get_doc(
|
||||||
|
doctype="Process Deferred Accounting",
|
||||||
|
posting_date=nowdate(),
|
||||||
|
start_date="2022-01-01",
|
||||||
|
end_date="2022-01-31",
|
||||||
|
type="Expense",
|
||||||
|
company=self.company,
|
||||||
|
)
|
||||||
|
pda.insert()
|
||||||
|
pda.submit()
|
||||||
|
|
||||||
|
# execute report
|
||||||
|
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
|
||||||
|
self.filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"period_start_date": "2022-01-01",
|
||||||
|
"period_end_date": "2022-01-31",
|
||||||
|
"from_fiscal_year": fiscal_year.year,
|
||||||
|
"to_fiscal_year": fiscal_year.year,
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"type": "Expense",
|
||||||
|
"with_upcoming_postings": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||||
|
report.run()
|
||||||
|
|
||||||
|
# fetch the invoice from deferred invoices list
|
||||||
|
inv = [d for d in report.deferred_invoices if d.name == pi.name]
|
||||||
|
# make sure the list isn't empty
|
||||||
|
self.assertTrue(inv)
|
||||||
|
# calculate the total deferred expense for the period
|
||||||
|
inv = inv[0].calculate_invoice_revenue_expense_for_period()
|
||||||
|
deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
|
||||||
|
# make sure the total deferred expense is greater than 0
|
||||||
|
self.assertLess(deferred_exp, 0)
|
||||||
|
|||||||
@@ -246,7 +246,10 @@ def get_conditions(filters):
|
|||||||
as_list=True,
|
as_list=True,
|
||||||
)
|
)
|
||||||
if system_generated_cr_dr_journals:
|
if system_generated_cr_dr_journals:
|
||||||
filters.update({"voucher_no_not_in": [x[0] for x in system_generated_cr_dr_journals]})
|
vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + [
|
||||||
|
x[0] for x in system_generated_cr_dr_journals
|
||||||
|
]
|
||||||
|
filters.update({"voucher_no_not_in": vouchers_to_ignore})
|
||||||
|
|
||||||
if filters.get("voucher_no_not_in"):
|
if filters.get("voucher_no_not_in"):
|
||||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||||
|
|||||||
@@ -2312,6 +2312,15 @@ class AccountsController(TransactionBase):
|
|||||||
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||||
advance_entry.is_advance = "Yes"
|
advance_entry.is_advance = "Yes"
|
||||||
|
|
||||||
|
# update dimesions
|
||||||
|
dimensions_dict = frappe._dict()
|
||||||
|
active_dimensions = get_dimensions()[0]
|
||||||
|
for dim in active_dimensions:
|
||||||
|
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
||||||
|
|
||||||
|
reconcilation_entry.update(dimensions_dict)
|
||||||
|
advance_entry.update(dimensions_dict)
|
||||||
|
|
||||||
if self.doctype == "Sales Invoice":
|
if self.doctype == "Sales Invoice":
|
||||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||||
|
|||||||
@@ -27,7 +27,26 @@ from erpnext.utilities.transaction_base import TransactionBase
|
|||||||
class Opportunity(TransactionBase, CRMNote):
|
class Opportunity(TransactionBase, CRMNote):
|
||||||
def onload(self):
|
def onload(self):
|
||||||
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
|
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
|
||||||
|
|
||||||
load_address_and_contact(ref_doc)
|
load_address_and_contact(ref_doc)
|
||||||
|
load_address_and_contact(self)
|
||||||
|
|
||||||
|
ref_doc_contact_list = ref_doc.get("__onload").get("contact_list")
|
||||||
|
opportunity_doc_contact_list = [
|
||||||
|
contact
|
||||||
|
for contact in self.get("__onload").get("contact_list")
|
||||||
|
if contact not in ref_doc_contact_list
|
||||||
|
]
|
||||||
|
ref_doc_contact_list.extend(opportunity_doc_contact_list)
|
||||||
|
ref_doc.set_onload("contact_list", ref_doc_contact_list)
|
||||||
|
|
||||||
|
ref_doc_addr_list = ref_doc.get("__onload").get("addr_list")
|
||||||
|
opportunity_doc_addr_list = [
|
||||||
|
addr for addr in self.get("__onload").get("addr_list") if addr not in ref_doc_addr_list
|
||||||
|
]
|
||||||
|
ref_doc_addr_list.extend(opportunity_doc_addr_list)
|
||||||
|
ref_doc.set_onload("addr_list", ref_doc_addr_list)
|
||||||
|
|
||||||
self.set("__onload", ref_doc.get("__onload"))
|
self.set("__onload", ref_doc.get("__onload"))
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ class OpportunitySummaryBySalesStage:
|
|||||||
self.grouped_data = []
|
self.grouped_data = []
|
||||||
|
|
||||||
grouping_key = lambda o: (o["sales_stage"], o[based_on]) # noqa
|
grouping_key = lambda o: (o["sales_stage"], o[based_on]) # noqa
|
||||||
for (sales_stage, _based_on), rows in groupby(self.query_result, grouping_key):
|
for (sales_stage, _based_on), rows in groupby(
|
||||||
|
sorted(self.query_result, key=grouping_key), key=grouping_key
|
||||||
|
):
|
||||||
self.grouped_data.append(
|
self.grouped_data.append(
|
||||||
{
|
{
|
||||||
"sales_stage": sales_stage,
|
"sales_stage": sales_stage,
|
||||||
|
|||||||
@@ -122,7 +122,9 @@ class SalesPipelineAnalytics:
|
|||||||
self.grouped_data = []
|
self.grouped_data = []
|
||||||
|
|
||||||
grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by]) # noqa
|
grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by]) # noqa
|
||||||
for (pipeline_by, period_by), rows in groupby(self.query_result, grouping_key):
|
for (pipeline_by, period_by), rows in groupby(
|
||||||
|
sorted(self.query_result, key=grouping_key), grouping_key
|
||||||
|
):
|
||||||
self.grouped_data.append(
|
self.grouped_data.append(
|
||||||
{
|
{
|
||||||
self.pipeline_by: pipeline_by,
|
self.pipeline_by: pipeline_by,
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
|||||||
erpnext.patches.v14_0.update_total_asset_cost_field
|
erpnext.patches.v14_0.update_total_asset_cost_field
|
||||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||||
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
||||||
|
erpnext.patches.v14_0.update_pos_return_ledger_entries
|
||||||
# below migration patch should always run last
|
# below migration patch should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||||
|
|||||||
61
erpnext/patches/v14_0/update_pos_return_ledger_entries.py
Normal file
61
erpnext/patches/v14_0/update_pos_return_ledger_entries.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import qb
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
sinv = qb.DocType("Sales Invoice")
|
||||||
|
pos_returns_without_self = (
|
||||||
|
qb.from_(sinv)
|
||||||
|
.select(sinv.name)
|
||||||
|
.where(
|
||||||
|
sinv.docstatus.eq(1)
|
||||||
|
& sinv.is_pos.eq(1)
|
||||||
|
& sinv.is_return.eq(1)
|
||||||
|
& sinv.return_against.notnull()
|
||||||
|
& sinv.update_outstanding_for_self.eq(0)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
if pos_returns_without_self:
|
||||||
|
pos_returns_without_self = [x[0] for x in pos_returns_without_self]
|
||||||
|
|
||||||
|
gle = qb.DocType("GL Entry")
|
||||||
|
gl_against_references = (
|
||||||
|
qb.from_(gle)
|
||||||
|
.select(gle.voucher_no, gle.against_voucher)
|
||||||
|
.where(
|
||||||
|
gle.voucher_no.isin(pos_returns_without_self)
|
||||||
|
& gle.against_voucher.notnull()
|
||||||
|
& gle.against_voucher.eq(gle.voucher_no)
|
||||||
|
& gle.is_cancelled.eq(0)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
|
||||||
|
_vouchers = list(set([x[0] for x in gl_against_references]))
|
||||||
|
invoice_return_against = (
|
||||||
|
qb.from_(sinv)
|
||||||
|
.select(sinv.name, sinv.return_against)
|
||||||
|
.where(sinv.name.isin(_vouchers) & sinv.return_against.notnull())
|
||||||
|
.orderby(sinv.name)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_references = set(invoice_return_against)
|
||||||
|
actual_references = set(gl_against_references)
|
||||||
|
|
||||||
|
invalid_references = actual_references.difference(valid_references)
|
||||||
|
|
||||||
|
if invalid_references:
|
||||||
|
# Repost Accounting Ledger
|
||||||
|
pos_for_reposting = (
|
||||||
|
qb.from_(sinv)
|
||||||
|
.select(sinv.company, sinv.name)
|
||||||
|
.where(sinv.name.isin([x[0] for x in invalid_references]))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
for x in pos_for_reposting:
|
||||||
|
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||||
|
ral.company = x.company
|
||||||
|
ral.append("vouchers", {"voucher_type": "Sales Invoice", "voucher_no": x.name})
|
||||||
|
ral.save().submit()
|
||||||
@@ -1139,6 +1139,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
"Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"],
|
"Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"],
|
||||||
"Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
|
"Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
|
||||||
"Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"],
|
"Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"],
|
||||||
|
"Sales Order Item": ["prevdoc_docname", "quotation_item"],
|
||||||
};
|
};
|
||||||
const mappped_fields = mapped_item_field_map[item.doctype] || [];
|
const mappped_fields = mapped_item_field_map[item.doctype] || [];
|
||||||
|
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ erpnext.SalesFunnel = class SalesFunnel {
|
|||||||
context.fill();
|
context.fill();
|
||||||
|
|
||||||
// draw text
|
// draw text
|
||||||
context.fillStyle = "";
|
context.fillStyle = getComputedStyle(document.body).getPropertyValue("--text-color");
|
||||||
context.textBaseline = "middle";
|
context.textBaseline = "middle";
|
||||||
context.font = "1.1em sans-serif";
|
context.font = "1.1em sans-serif";
|
||||||
context.fillText(__(title), width + 20, y_mid);
|
context.fillText(__(title), width + 20, y_mid);
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ def get_opp_by_lead_source(from_date, to_date, company):
|
|||||||
summary = {}
|
summary = {}
|
||||||
sales_stages = set()
|
sales_stages = set()
|
||||||
group_key = lambda o: (o["source"], o["sales_stage"]) # noqa
|
group_key = lambda o: (o["source"], o["sales_stage"]) # noqa
|
||||||
for (source, sales_stage), rows in groupby(cp_opportunities, group_key):
|
for (source, sales_stage), rows in groupby(sorted(cp_opportunities, key=group_key), group_key):
|
||||||
summary.setdefault(source, {})[sales_stage] = sum(r["compound_amount"] for r in rows)
|
summary.setdefault(source, {})[sales_stage] = sum(r["compound_amount"] for r in rows)
|
||||||
sales_stages.add(sales_stage)
|
sales_stages.add(sales_stage)
|
||||||
|
|
||||||
|
|||||||
@@ -837,7 +837,8 @@ def create_delivery_note(source_name, target_doc=None):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]):
|
group_key = lambda so: so["customer"] # noqa
|
||||||
|
for customer, rows in groupby(sorted(sales_orders, key=group_key), key=group_key):
|
||||||
sales_dict[customer] = {row.sales_order for row in rows}
|
sales_dict[customer] = {row.sales_order for row in rows}
|
||||||
|
|
||||||
if sales_dict:
|
if sales_dict:
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, bold
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
|
from frappe.utils import cint, cstr, flt, format_date, get_link_to_form, getdate, now, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||||
@@ -542,11 +542,31 @@ class update_entries_after:
|
|||||||
|
|
||||||
return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
|
return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
|
||||||
|
|
||||||
|
def validate_previous_sle_qty(self, sle):
|
||||||
|
previous_sle = self.data[sle.warehouse].previous_sle
|
||||||
|
if previous_sle and previous_sle.get("qty_after_transaction") < 0 and sle.get("actual_qty") > 0:
|
||||||
|
frappe.msgprint(
|
||||||
|
_(
|
||||||
|
"The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the <a href='https://docs.erpnext.com/docs/user/manual/en/stock-adjustment-cogs-with-negative-stock'>documentation<a>."
|
||||||
|
).format(
|
||||||
|
bold(sle.item_code),
|
||||||
|
bold(sle.warehouse),
|
||||||
|
bold(format_date(previous_sle.posting_date)),
|
||||||
|
sle.voucher_no,
|
||||||
|
bold(format_date(previous_sle.posting_date)),
|
||||||
|
bold(previous_sle.posting_time),
|
||||||
|
),
|
||||||
|
title=_("Warning on Negative Stock"),
|
||||||
|
indicator="blue",
|
||||||
|
)
|
||||||
|
|
||||||
def process_sle(self, sle):
|
def process_sle(self, sle):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
# previous sle data for this warehouse
|
# previous sle data for this warehouse
|
||||||
self.wh_data = self.data[sle.warehouse]
|
self.wh_data = self.data[sle.warehouse]
|
||||||
|
|
||||||
|
self.validate_previous_sle_qty(sle)
|
||||||
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
|
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
|
||||||
|
|
||||||
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
|
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
|
||||||
|
|||||||
@@ -1351,7 +1351,7 @@ Lead to Quotation,Vom Lead zum Angebot,
|
|||||||
Learn,Lernen,
|
Learn,Lernen,
|
||||||
Leave Management,Urlaube verwalten,
|
Leave Management,Urlaube verwalten,
|
||||||
Leave and Attendance,Urlaub und Anwesenheit,
|
Leave and Attendance,Urlaub und Anwesenheit,
|
||||||
Leave application {0} already exists against the student {1},Verlassen der Anwendung {0} ist bereits für den Schüler {1} vorhanden,
|
Leave application {0} already exists against the student {1},Abwesenheitsantrag {0} existiert bereits für den Schüler {1},
|
||||||
Leaves has been granted sucessfully,Urlaub wurde genehmigt,
|
Leaves has been granted sucessfully,Urlaub wurde genehmigt,
|
||||||
Leaves must be allocated in multiples of 0.5,"Abwesenheiten müssen ein Vielfaches von 0,5 sein",
|
Leaves must be allocated in multiples of 0.5,"Abwesenheiten müssen ein Vielfaches von 0,5 sein",
|
||||||
Ledger,Hauptbuch,
|
Ledger,Hauptbuch,
|
||||||
@@ -5655,10 +5655,10 @@ Guardian Details,Erziehungsberechtigten-Details,
|
|||||||
Guardians,Erziehungsberechtigte,
|
Guardians,Erziehungsberechtigte,
|
||||||
Sibling Details,Geschwister-Details,
|
Sibling Details,Geschwister-Details,
|
||||||
Siblings,Geschwister,
|
Siblings,Geschwister,
|
||||||
Exit,Verlassen,
|
Exit,Austritt,
|
||||||
Date of Leaving,Austrittsdatum,
|
Date of Leaving,Austrittsdatum,
|
||||||
Leaving Certificate Number,Leaving Certificate Nummer,
|
Leaving Certificate Number,Leaving Certificate Nummer,
|
||||||
Reason For Leaving,Grund für das Verlassen,
|
Reason For Leaving,Grund für den Austritt,
|
||||||
Student Admission,Studenten Eintritt,
|
Student Admission,Studenten Eintritt,
|
||||||
Admission Start Date,Stichtag zum Zulassungsbeginn,
|
Admission Start Date,Stichtag zum Zulassungsbeginn,
|
||||||
Admission End Date,Stichtag für Zulassungsende,
|
Admission End Date,Stichtag für Zulassungsende,
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user