Compare commits

...

12 Commits

Author SHA1 Message Date
Frappe PR Bot
a29468f6fe chore(release): Bumped to Version 15.13.0
# [15.13.0](https://github.com/frappe/erpnext/compare/v15.12.2...v15.13.0) (2024-02-11)

### Features

* get RM costs from consumption entry in manufacture SE (backport [#39822](https://github.com/frappe/erpnext/issues/39822)) (backport [#39847](https://github.com/frappe/erpnext/issues/39847)) ([#39849](https://github.com/frappe/erpnext/issues/39849)) ([beb4137](beb4137dac))
2024-02-11 12:20:07 +00:00
mergify[bot]
beb4137dac feat: get RM costs from consumption entry in manufacture SE (backport #39822) (backport #39847) (#39849)
feat: get RM costs from consumption entry in manufacture SE (backport #39822) (#39847)

feat: get RM costs from consumption entry in manufacture SE (#39822)

(cherry picked from commit 39067c7614)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
(cherry picked from commit 2d5f186812)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2024-02-11 17:48:47 +05:30
ruthra kumar
db4efd3332 Merge pull request #39803 from frappe/mergify/bp/version-15/pr-39783
fix: cancelling cr/dr notes should update the linked Invoice status (backport #39783)
2024-02-08 10:23:53 +05:30
ruthra kumar
7556457e3e refactor(test): Forex Credit Note cancellation against Invoice
(cherry picked from commit 2f676ced5c)
2024-02-08 04:36:15 +00:00
ruthra kumar
5590b04c89 refactor(test): assert Invoice status as well
(cherry picked from commit 33efe0d12d)
2024-02-08 04:36:14 +00:00
ruthra kumar
490cbc53d8 test: Invoice status on Cr/Dr note cancellation
(cherry picked from commit 31a8c3bdc4)
2024-02-08 04:36:14 +00:00
ruthra kumar
592b3ff7b7 refactor: cancel Cr/Dr JE's on Sales/Purchase return cancel
(cherry picked from commit 0549535603)
2024-02-08 04:36:14 +00:00
Frappe PR Bot
bc9a63497a chore(release): Bumped to Version 15.12.2
## [15.12.2](https://github.com/frappe/erpnext/compare/v15.12.1...v15.12.2) (2024-02-07)

### Bug Fixes

* remove duplicates from tax category map ([1f21607](1f21607e4f))
2024-02-07 14:22:43 +00:00
Gursheen Kaur Anand
b9dc0f3896 Merge pull request #39794 from frappe/mergify/bp/version-15/pr-39787
fix: remove duplicates from tax category map (backport #39787)
2024-02-07 19:51:02 +05:30
Gursheen Anand
1f21607e4f fix: remove duplicates from tax category map
(cherry picked from commit 3c6114ab72)
2024-02-07 13:54:03 +00:00
Frappe PR Bot
488e693ed8 chore(release): Bumped to Version 15.12.1
## [15.12.1](https://github.com/frappe/erpnext/compare/v15.12.0...v15.12.1) (2024-02-07)

### Bug Fixes

* set rate for PO created against BO (backport [#39765](https://github.com/frappe/erpnext/issues/39765)) (backport [#39767](https://github.com/frappe/erpnext/issues/39767)) ([#39775](https://github.com/frappe/erpnext/issues/39775)) ([ae2a8db](ae2a8db0d7))
2024-02-07 06:07:15 +00:00
mergify[bot]
ae2a8db0d7 fix: set rate for PO created against BO (backport #39765) (backport #39767) (#39775)
fix: set rate for PO created against BO (backport #39765) (#39767)

* fix: set rate for PO created against BO

(cherry picked from commit 0e5b4e5f07)

# Conflicts:
#	erpnext/manufacturing/doctype/blanket_order/blanket_order.py

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
(cherry picked from commit 22138867f5)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2024-02-07 11:35:55 +05:30
11 changed files with 281 additions and 40 deletions

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "15.12.0"
__version__ = "15.13.0"
def get_default_company(user=None):

View File

@@ -591,6 +591,70 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
def test_invoice_status_after_cr_note_cancellation(self):
# This test case is made after the 'always standalone Credit/Debit notes' feature is introduced
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.is_return = 1
cr_note.return_against = si.name
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
pr.get_unreconciled_entries()
self.assertEqual(pr.get("invoices"), [])
self.assertEqual(pr.get("payments"), [])
journals = frappe.db.get_all(
"Journal Entry",
filters={
"is_system_generated": 1,
"docstatus": 1,
"voucher_type": "Credit Note",
"reference_type": si.doctype,
"reference_name": si.name,
},
pluck="name",
)
self.assertEqual(len(journals), 1)
# assert status and outstanding
si.reload()
self.assertEqual(si.status, "Credit Note Issued")
self.assertEqual(si.outstanding_amount, 0)
cr_note.reload()
cr_note.cancel()
# 'Credit Note' Journal should be auto cancelled
journals = frappe.db.get_all(
"Journal Entry",
filters={
"is_system_generated": 1,
"docstatus": 1,
"voucher_type": "Credit Note",
"reference_type": si.doctype,
"reference_name": si.name,
},
pluck="name",
)
self.assertEqual(len(journals), 0)
# assert status and outstanding
si.reload()
self.assertEqual(si.status, "Unpaid")
self.assertEqual(si.outstanding_amount, 100)
def test_cr_note_partial_against_invoice(self):
transaction_date = nowdate()
amount = 100

View File

@@ -63,16 +63,14 @@ def get_result(
tax_amount += entry.credit - entry.debit
# infer tax withholding category from the account if it's the single account for this category
tax_withholding_category = tds_accounts.get(entry.account)
rate = tax_rate_map.get(tax_withholding_category)
# or else the consolidated value from the voucher document
if not tax_withholding_category:
# or else from the party default
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
# or else from the party default
if not tax_withholding_category:
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category)
rate = tax_rate_map.get(tax_withholding_category)
if net_total_map.get(name):
if voucher_type == "Journal Entry":
# back calcalute total amount from rate and tax_amount
@@ -295,7 +293,7 @@ def get_tds_docs(filters):
tds_accounts = {}
for tds_acc in _tds_accounts:
# if it turns out not to be the only tax withholding category, then don't include in the map
if tds_accounts.get(tds_acc["account"]):
if tds_acc["account"] in tds_accounts:
tds_accounts[tds_acc["account"]] = None
else:
tds_accounts[tds_acc["account"]] = tds_acc["parent"]
@@ -408,7 +406,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
"paid_amount_after_tax",
"base_paid_amount",
],
"Journal Entry": ["tax_withholding_category", "total_amount"],
"Journal Entry": ["total_amount"],
}
entries = frappe.get_all(

View File

@@ -5,7 +5,6 @@ import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
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
@@ -17,36 +16,63 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.clear_old_entries()
create_tax_accounts()
create_tcs_category()
def test_tax_withholding_for_customers(self):
create_tax_category(cumulative_threshold=300)
frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS")
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
jv = create_tcs_journal_entry()
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
# Check for JV totals using back calculation logic
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
]
self.check_expected_values(result, expected_values)
def test_single_account_for_multiple_categories(self):
create_tax_category("TDS - 1", rate=10, account="TDS - _TC")
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_1.tax_withholding_category = "TDS - 1"
inv_1.submit()
create_tax_category("TDS - 2", rate=20, account="TDS - _TC")
inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_2.tax_withholding_category = "TDS - 2"
inv_2.submit()
result = execute(
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
)[1]
expected_values = [
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
]
self.check_expected_values(result, expected_values)
def check_expected_values(self, result, expected_values):
for i in range(len(result)):
voucher = frappe._dict(result[i])
voucher_expected_values = expected_values[i]
self.assertEqual(voucher.ref_no, voucher_expected_values[0])
self.assertEqual(voucher.section_code, voucher_expected_values[1])
self.assertEqual(voucher.rate, voucher_expected_values[2])
self.assertEqual(voucher.base_total, voucher_expected_values[3])
self.assertAlmostEqual(voucher.tax_amount, voucher_expected_values[4])
self.assertAlmostEqual(voucher.grand_total, voucher_expected_values[5])
voucher_actual_values = (
voucher.ref_no,
voucher.section_code,
voucher.rate,
voucher.base_total,
voucher.tax_amount,
voucher.grand_total,
)
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
def tearDown(self):
self.clear_old_entries()
@@ -67,24 +93,20 @@ def create_tax_accounts():
).insert(ignore_if_duplicate=True)
def create_tcs_category():
def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0):
fiscal_year = get_fiscal_year(today(), company="_Test Company")
from_date = fiscal_year[1]
to_date = fiscal_year[2]
tax_category = create_tax_withholding_category(
category_name="TCS",
rate=0.075,
create_tax_withholding_category(
category_name=category,
rate=rate,
from_date=from_date,
to_date=to_date,
account="TCS - _TC",
cumulative_threshold=300,
account=account,
cumulative_threshold=cumulative_threshold,
)
customer = frappe.get_doc("Customer", "_Test Customer")
customer.tax_withholding_category = "TCS"
customer.save()
def create_tcs_payment_entry():
payment_entry = create_payment_entry(
@@ -109,3 +131,32 @@ def create_tcs_payment_entry():
)
payment_entry.submit()
return payment_entry
def create_tcs_journal_entry():
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = "_Test Company"
jv.set(
"accounts",
[
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 10000,
},
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"debit_in_account_currency": 9992.5,
},
{
"account": "TCS - _TC",
"debit_in_account_currency": 7.5,
},
],
)
jv.insert()
return jv.submit()

View File

@@ -1472,6 +1472,24 @@ class AccountsController(TransactionBase):
x.update({dim.fieldname: self.get(dim.fieldname)})
reconcile_against_document(lst, active_dimensions=active_dimensions)
def cancel_system_generated_credit_debit_notes(self):
# Cancel 'Credit/Debit' Note Journal Entries, if found.
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
journals = frappe.db.get_all(
"Journal Entry",
filters={
"is_system_generated": 1,
"reference_type": self.doctype,
"reference_name": self.name,
"voucher_type": voucher_type,
"docstatus": 1,
},
pluck="name",
)
for x in journals:
frappe.get_doc("Journal Entry", x).cancel()
def on_cancel(self):
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
remove_from_bank_transaction,
@@ -1484,6 +1502,8 @@ class AccountsController(TransactionBase):
remove_from_bank_transaction(self.doctype, self.name)
if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
self.cancel_system_generated_credit_debit_notes()
# Cancel Exchange Gain/Loss Journal before unlinking
cancel_exchange_gain_loss_journal(self)

View File

@@ -1041,18 +1041,18 @@ class TestAccountsController(FrappeTestCase):
cr_note.reload()
cr_note.cancel()
# Exchange Gain/Loss Journal should've been created.
# with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller
# JE(Credit Note) will be cancelled once the parent is cancelled
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
self.assertNotEqual(exc_je_for_si, [])
self.assertEqual(len(exc_je_for_si), 1)
self.assertEqual(exc_je_for_si, [])
self.assertEqual(len(exc_je_for_si), 0)
self.assertEqual(len(exc_je_for_cr), 0)
# The Credit Note JE is still active and is referencing the sales invoice
# So, outstanding stays the same
# No references, full outstanding
si.reload()
self.assertEqual(si.outstanding_amount, 1)
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
self.assertEqual(si.outstanding_amount, 2)
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
def test_40_cost_center_from_payment_entry(self):
"""

View File

@@ -90,6 +90,7 @@ def make_order(source_name):
def update_item(source, target, source_parent):
target_qty = source.get("qty") - source.get("ordered_qty")
target.qty = target_qty if not flt(target_qty) < 0 else 0
target.rate = source.get("rate")
item = get_item_defaults(target.item_code, source_parent.company)
if item:
target.item_name = item.get("item_name")
@@ -111,6 +112,10 @@ def make_order(source_name):
},
},
)
if target_doc.doctype == "Purchase Order":
target_doc.set_missing_values()
return target_doc

View File

@@ -7,6 +7,7 @@
"field_order": [
"raw_materials_consumption_section",
"material_consumption",
"get_rm_cost_from_consumption_entry",
"column_break_3",
"backflush_raw_materials_based_on",
"capacity_planning",
@@ -202,13 +203,20 @@
"fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
"fieldtype": "Check",
"label": "Set Operating Cost / Scrape Items From Sub-assemblies"
},
{
"default": "0",
"depends_on": "eval: doc.material_consumption",
"fieldname": "get_rm_cost_from_consumption_entry",
"fieldtype": "Check",
"label": "Get Raw Materials Cost from Consumption Entry"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-12-28 16:37:44.874096",
"modified": "2024-02-08 19:00:37.561244",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",

View File

@@ -26,6 +26,7 @@ class ManufacturingSettings(Document):
default_scrap_warehouse: DF.Link | None
default_wip_warehouse: DF.Link | None
disable_capacity_planning: DF.Check
get_rm_cost_from_consumption_entry: DF.Check
job_card_excess_transfer: DF.Check
make_serial_no_batch_from_work_order: DF.Check
material_consumption: DF.Check

View File

@@ -1775,6 +1775,52 @@ class TestWorkOrder(FrappeTestCase):
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0
)
@change_settings(
"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1}
)
def test_get_rm_cost_from_consumption_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
rm = make_item(properties={"is_stock_item": 1}).name
fg = make_item(properties={"is_stock_item": 1}).name
make_stock_entry_test_record(
purpose="Material Receipt",
item_code=rm,
target="Stores - _TC",
qty=10,
basic_rate=100,
)
make_stock_entry_test_record(
purpose="Material Receipt",
item_code=rm,
target="Stores - _TC",
qty=10,
basic_rate=200,
)
bom = make_bom(item=fg, raw_materials=[rm], rate=150).name
wo = make_wo_order_test_record(
production_item=fg,
bom_no=bom,
qty=10,
)
mte = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
mte.items[0].s_warehouse = "Stores - _TC"
mte.insert().submit()
mce = frappe.get_doc(make_stock_entry(wo.name, "Material Consumption for Manufacture", 10))
mce.insert().submit()
me = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
me.insert().submit()
valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10
self.assertEqual(me.items[0].valuation_rate, valuation_rate)
def prepare_boms_for_sub_assembly_test():
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):

View File

@@ -899,14 +899,62 @@ class StockEntry(StockController):
return flt(outgoing_items_cost / total_fg_qty)
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float:
settings = frappe.get_single("Manufacturing Settings")
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
# Get raw materials cost from BOM if multiple material consumption entries
if not outgoing_items_cost and frappe.db.get_single_value(
"Manufacturing Settings", "material_consumption", cache=True
):
bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()])
if settings.material_consumption:
if settings.get_rm_cost_from_consumption_entry and self.work_order:
# Validate only if Material Consumption Entry exists for the Work Order.
if frappe.db.exists(
"Stock Entry",
{
"docstatus": 1,
"work_order": self.work_order,
"purpose": "Material Consumption for Manufacture",
},
):
for item in self.items:
if not item.is_finished_item and not item.is_scrap_item:
label = frappe.get_meta(settings.doctype).get_label("get_rm_cost_from_consumption_entry")
frappe.throw(
_(
"Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials."
).format(
item.idx,
frappe.bold(label),
frappe.bold("Manufacture"),
frappe.bold("Material Consumption for Manufacture"),
)
)
if frappe.db.exists(
"Stock Entry", {"docstatus": 1, "work_order": self.work_order, "purpose": "Manufacture"}
):
frappe.throw(
_("Only one {0} entry can be created against the Work Order {1}").format(
frappe.bold("Manufacture"), frappe.bold(self.work_order)
)
)
SE = frappe.qb.DocType("Stock Entry")
SE_ITEM = frappe.qb.DocType("Stock Entry Detail")
outgoing_items_cost = (
frappe.qb.from_(SE)
.left_join(SE_ITEM)
.on(SE.name == SE_ITEM.parent)
.select(Sum(SE_ITEM.valuation_rate * SE_ITEM.transfer_qty))
.where(
(SE.docstatus == 1)
& (SE.work_order == self.work_order)
& (SE.purpose == "Material Consumption for Manufacture")
)
).run()[0][0] or 0
elif not outgoing_items_cost:
bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()])
return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)