mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-23 06:59:20 +00:00
Merge pull request #42141 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -9,6 +9,7 @@ def get_data():
|
|||||||
),
|
),
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||||
|
"dynamic_links": {"party": ["Supplier", "party_type"]},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
||||||
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ def get_data():
|
|||||||
"Bank Account": "party",
|
"Bank Account": "party",
|
||||||
"Subscription": "party",
|
"Subscription": "party",
|
||||||
},
|
},
|
||||||
"dynamic_links": {"party_name": ["Customer", "quotation_to"]},
|
"dynamic_links": {
|
||||||
|
"party_name": ["Customer", "quotation_to"],
|
||||||
|
"party": ["Customer", "party_type"],
|
||||||
|
},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
|
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
|
||||||
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
||||||
|
|||||||
@@ -343,6 +343,10 @@ class SalesOrder(SellingController):
|
|||||||
def update_status(self, status):
|
def update_status(self, status):
|
||||||
self.check_modified_date()
|
self.check_modified_date()
|
||||||
self.set_status(update=True, status=status)
|
self.set_status(update=True, status=status)
|
||||||
|
# Upon Sales Order Re-open, check for credit limit.
|
||||||
|
# Limit should be checked after the 'Hold/Closed' status is reset.
|
||||||
|
if status == "Draft" and self.docstatus == 1:
|
||||||
|
self.check_credit_limit()
|
||||||
self.update_reserved_qty()
|
self.update_reserved_qty()
|
||||||
self.notify_update()
|
self.notify_update()
|
||||||
clear_doctype_notifications(self)
|
clear_doctype_notifications(self)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user
|
|||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||||
|
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||||
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
||||||
make_maintenance_schedule,
|
make_maintenance_schedule,
|
||||||
@@ -31,7 +32,7 @@ from erpnext.stock.doctype.item.test_item import make_item
|
|||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
|
||||||
class TestSalesOrder(FrappeTestCase):
|
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
@@ -52,6 +53,9 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.create_customer("_Test Customer Credit")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
@@ -2159,6 +2163,28 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
||||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||||
|
|
||||||
|
def test_credit_limit_on_so_reopning(self):
|
||||||
|
# set credit limit
|
||||||
|
company = "_Test Company"
|
||||||
|
customer = frappe.get_doc("Customer", self.customer)
|
||||||
|
customer.credit_limits = []
|
||||||
|
customer.append(
|
||||||
|
"credit_limits", {"company": company, "credit_limit": 1000, "bypass_credit_limit_check": False}
|
||||||
|
)
|
||||||
|
customer.save()
|
||||||
|
|
||||||
|
so1 = make_sales_order(qty=9, rate=100, do_not_submit=True)
|
||||||
|
so1.customer = self.customer
|
||||||
|
so1.save().submit()
|
||||||
|
|
||||||
|
so1.update_status("Closed")
|
||||||
|
|
||||||
|
so2 = make_sales_order(qty=9, rate=100, do_not_submit=True)
|
||||||
|
so2.customer = self.customer
|
||||||
|
so2.save().submit()
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
||||||
|
|
||||||
|
|
||||||
def automatically_fetch_payment_terms(enable=1):
|
def automatically_fetch_payment_terms(enable=1):
|
||||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||||
|
|||||||
@@ -661,6 +661,13 @@ class PurchaseReceipt(BuyingController):
|
|||||||
posting_date=posting_date,
|
posting_date=posting_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_landed_cost_booked_for_any_item(self) -> bool:
|
||||||
|
for x in self.items:
|
||||||
|
if x.landed_cost_voucher_amount != 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False):
|
def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False):
|
||||||
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
|
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
|
||||||
is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
|
is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
|
||||||
@@ -696,7 +703,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
i = 1
|
i = 1
|
||||||
for tax in self.get("taxes"):
|
for tax in self.get("taxes"):
|
||||||
if valuation_tax.get(tax.name):
|
if valuation_tax.get(tax.name):
|
||||||
if via_landed_cost_voucher:
|
if via_landed_cost_voucher or self.is_landed_cost_booked_for_any_item():
|
||||||
account = tax.account_head
|
account = tax.account_head
|
||||||
else:
|
else:
|
||||||
negative_expense_booked_in_pi = frappe.db.sql(
|
negative_expense_booked_in_pi = frappe.db.sql(
|
||||||
|
|||||||
@@ -2349,6 +2349,156 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
self.assertSequenceEqual(expected_gle, gl_entries)
|
self.assertSequenceEqual(expected_gle, gl_entries)
|
||||||
frappe.local.enable_perpetual_inventory["_Test Company"] = old_perpetual_inventory
|
frappe.local.enable_perpetual_inventory["_Test Company"] = old_perpetual_inventory
|
||||||
|
|
||||||
|
def test_tax_account_heads_on_lcv_and_item_repost(self):
|
||||||
|
"""
|
||||||
|
PO -> PR -> PI
|
||||||
|
PR -> LCV
|
||||||
|
Backdated `Repost Item valuation` should not merge tax account heads into stock_rbnb
|
||||||
|
"""
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||||
|
create_purchase_order,
|
||||||
|
make_pr_against_po,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||||
|
|
||||||
|
stock_rbnb = "Stock Received But Not Billed - _TC"
|
||||||
|
stock_in_hand = "Stock In Hand - _TC"
|
||||||
|
test_cc = "_Test Cost Center - _TC"
|
||||||
|
test_company = "_Test Company"
|
||||||
|
creditors = "Creditors - _TC"
|
||||||
|
lcv_expense_account = "Expenses Included In Valuation - _TC"
|
||||||
|
|
||||||
|
company_doc = frappe.get_doc("Company", test_company)
|
||||||
|
company_doc.enable_perpetual_inventory = True
|
||||||
|
company_doc.stock_received_but_not_billed = stock_rbnb
|
||||||
|
company_doc.default_inventory_account = stock_in_hand
|
||||||
|
company_doc.save()
|
||||||
|
|
||||||
|
packaging_charges_account = create_account(
|
||||||
|
account_name="Packaging Charges",
|
||||||
|
parent_account="Indirect Expenses - _TC",
|
||||||
|
company=test_company,
|
||||||
|
account_type="Tax",
|
||||||
|
)
|
||||||
|
|
||||||
|
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||||
|
po.taxes = []
|
||||||
|
po.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"category": "Valuation and Total",
|
||||||
|
"account_head": packaging_charges_account,
|
||||||
|
"cost_center": test_cc,
|
||||||
|
"description": "Test",
|
||||||
|
"add_deduct_tax": "Add",
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"tax_amount": 250,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
po.save().submit()
|
||||||
|
|
||||||
|
pr = make_pr_against_po(po.name, received_qty=10)
|
||||||
|
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
|
expected_pr_gles = [
|
||||||
|
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
|
{"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||||
|
]
|
||||||
|
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||||
|
|
||||||
|
# Make PI against Purchase Receipt
|
||||||
|
pi = make_purchase_invoice(pr.name).save().submit()
|
||||||
|
pi_gl_entries = get_gl_entries(pi.doctype, pi.name, skip_cancelled=True)
|
||||||
|
expected_pi_gles = [
|
||||||
|
{"account": stock_rbnb, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": packaging_charges_account, "debit": 250.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": creditors, "debit": 0.0, "credit": 1250.0, "cost_center": None},
|
||||||
|
]
|
||||||
|
self.assertEqual(expected_pi_gles, pi_gl_entries)
|
||||||
|
|
||||||
|
lcv = self.create_lcv(pr.doctype, pr.name, test_company, lcv_expense_account)
|
||||||
|
pr_gles_after_lcv = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
|
expected_pr_gles_after_lcv = [
|
||||||
|
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
|
{"account": stock_in_hand, "debit": 1300.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||||
|
{"account": lcv_expense_account, "debit": 0.0, "credit": 50.0, "cost_center": test_cc},
|
||||||
|
]
|
||||||
|
self.assertEqual(expected_pr_gles_after_lcv, pr_gles_after_lcv)
|
||||||
|
|
||||||
|
# Trigger Repost Item Valudation on a older date
|
||||||
|
repost_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Repost Item Valuation",
|
||||||
|
"based_on": "Item and Warehouse",
|
||||||
|
"item_code": pr.items[0].item_code,
|
||||||
|
"warehouse": pr.items[0].warehouse,
|
||||||
|
"posting_date": add_days(pr.posting_date, -1),
|
||||||
|
"posting_time": "00:00:00",
|
||||||
|
"company": pr.company,
|
||||||
|
"allow_negative_stock": 1,
|
||||||
|
"via_landed_cost_voucher": 0,
|
||||||
|
"allow_zero_rate": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
repost_doc.save().submit()
|
||||||
|
|
||||||
|
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
|
expected_pr_gles_after_repost = [
|
||||||
|
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
|
{"account": stock_in_hand, "debit": 1300.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||||
|
{"account": lcv_expense_account, "debit": 0.0, "credit": 50.0, "cost_center": test_cc},
|
||||||
|
]
|
||||||
|
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||||
|
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
lcv.reload()
|
||||||
|
lcv.cancel()
|
||||||
|
pi.reload()
|
||||||
|
pi.cancel()
|
||||||
|
pr.reload()
|
||||||
|
pr.cancel()
|
||||||
|
|
||||||
|
company_doc.enable_perpetual_inventory = False
|
||||||
|
company_doc.stock_received_but_not_billed = None
|
||||||
|
company_doc.default_inventory_account = None
|
||||||
|
company_doc.save()
|
||||||
|
|
||||||
|
def create_lcv(self, receipt_document_type, receipt_document, company, expense_account, charges=50):
|
||||||
|
ref_doc = frappe.get_doc(receipt_document_type, receipt_document)
|
||||||
|
|
||||||
|
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||||
|
lcv.company = company
|
||||||
|
lcv.distribute_charges_based_on = "Qty"
|
||||||
|
lcv.set(
|
||||||
|
"purchase_receipts",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"receipt_document_type": receipt_document_type,
|
||||||
|
"receipt_document": receipt_document,
|
||||||
|
"supplier": ref_doc.supplier,
|
||||||
|
"posting_date": ref_doc.posting_date,
|
||||||
|
"grand_total": ref_doc.base_grand_total,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
lcv.set(
|
||||||
|
"taxes",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"description": "Testing",
|
||||||
|
"expense_account": expense_account,
|
||||||
|
"amount": charges,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
lcv.save().submit()
|
||||||
|
return lcv
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
Reference in New Issue
Block a user