mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-22 12:57:14 +00:00
Compare commits
4 Commits
develop
...
bill-rejec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09516ab1e8 | ||
|
|
42d8e292cb | ||
|
|
98df30d1da | ||
|
|
6c3046121d |
@@ -10,6 +10,7 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
|
||||
from erpnext.accounts.services.base_gl_composer import BaseGLComposer
|
||||
from erpnext.accounts.services.taxes import TaxService
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.buying.doctype.buying_settings.buying_settings import bills_rejected_quantity
|
||||
|
||||
|
||||
class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
@@ -251,6 +252,10 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
)
|
||||
|
||||
else:
|
||||
self.make_rejected_warehouse_gl_entry(
|
||||
gl_entries, item, voucher_wise_stock_value, inventory_account_map
|
||||
)
|
||||
|
||||
if not doc.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -564,6 +569,41 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
|
||||
return stock_asset_rbnb or item.expense_account
|
||||
|
||||
def make_rejected_warehouse_gl_entry(
|
||||
self, gl_entries, item, voucher_wise_stock_value, inventory_account_map
|
||||
) -> None:
|
||||
"""Book the rejected material of an invoice that bills the received quantity, whose cost the
|
||||
supplier gl entry already carries."""
|
||||
doc = self.doc
|
||||
if not (item.rejected_warehouse and bills_rejected_quantity(doc)):
|
||||
return
|
||||
|
||||
rejected_amount = flt(
|
||||
voucher_wise_stock_value.get((item.name, item.rejected_warehouse)),
|
||||
item.precision("base_net_amount"),
|
||||
)
|
||||
if not rejected_amount:
|
||||
return
|
||||
|
||||
rejected_account = doc.get_inventory_account_dict(item, inventory_account_map, "rejected_warehouse")
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": rejected_account["account"],
|
||||
"against": doc.supplier,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": rejected_amount,
|
||||
"debit_in_transaction_currency": flt(
|
||||
rejected_amount / doc.conversion_rate, item.precision("net_amount")
|
||||
),
|
||||
},
|
||||
rejected_account["account_currency"],
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
|
||||
doc = self.doc
|
||||
net_amt_precision = item.precision("base_net_amount")
|
||||
|
||||
@@ -2624,6 +2624,102 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
return_pi.submit()
|
||||
self.assertEqual(return_pi.docstatus, 1)
|
||||
|
||||
def test_stock_updating_invoice_bills_the_rejected_quantity(self):
|
||||
"""With the rejected quantity billed and valued, the invoice pays for every unit received and
|
||||
the stock it moves matches the entries it books."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
|
||||
rejected_warehouse = create_warehouse("_Test Invoice Billed Rejected Warehouse", company=company)
|
||||
|
||||
settings = frappe.get_doc("Buying Settings")
|
||||
settings.bill_for_rejected_quantity_in_purchase_invoice = 1
|
||||
settings.set_valuation_rate_for_rejected_materials = 1
|
||||
settings.save()
|
||||
self.addCleanup(
|
||||
frappe.db.set_single_value, "Buying Settings", "set_valuation_rate_for_rejected_materials", 0
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item_code=item,
|
||||
company=company,
|
||||
warehouse="Stores - TCP1",
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
cost_center="Main - TCP1",
|
||||
supplier_warehouse="Work In Progress - TCP1",
|
||||
expense_account="_Test Account Cost for Goods Sold - TCP1",
|
||||
update_stock=1,
|
||||
received_qty=10,
|
||||
qty=6,
|
||||
rejected_qty=4,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
self.assertEqual(pi.items[0].amount, 1000)
|
||||
self.assertEqual(pi.items[0].valuation_rate, 100)
|
||||
|
||||
stock_value = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pi.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "stock_value_difference"],
|
||||
)
|
||||
by_warehouse = {d.warehouse: d.stock_value_difference for d in stock_value}
|
||||
|
||||
self.assertEqual(by_warehouse["Stores - TCP1"], 600)
|
||||
self.assertEqual(by_warehouse[rejected_warehouse], 400)
|
||||
|
||||
booked = frappe.get_all(
|
||||
"GL Entry", filters={"voucher_no": pi.name, "is_cancelled": 0}, fields=["debit"]
|
||||
)
|
||||
self.assertEqual(sum(flt(d.debit) for d in booked), 1000)
|
||||
|
||||
def test_rejected_material_is_not_valued_on_a_stock_updating_invoice(self):
|
||||
"""An invoice bills the accepted quantity alone, so its rejected material has no cost and the
|
||||
stock it moves must match the entries it books."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
|
||||
rejected_warehouse = create_warehouse("_Test Invoice Rejected Warehouse", company=company)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 1)
|
||||
self.addCleanup(
|
||||
frappe.db.set_single_value, "Buying Settings", "set_valuation_rate_for_rejected_materials", 0
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item_code=item,
|
||||
company=company,
|
||||
warehouse="Stores - TCP1",
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
cost_center="Main - TCP1",
|
||||
supplier_warehouse="Work In Progress - TCP1",
|
||||
expense_account="_Test Account Cost for Goods Sold - TCP1",
|
||||
update_stock=1,
|
||||
received_qty=10,
|
||||
qty=6,
|
||||
rejected_qty=4,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
stock_value = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pi.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "stock_value_difference"],
|
||||
)
|
||||
by_warehouse = {d.warehouse: d.stock_value_difference for d in stock_value}
|
||||
|
||||
self.assertEqual(by_warehouse["Stores - TCP1"], 600)
|
||||
self.assertEqual(by_warehouse[rejected_warehouse], 0)
|
||||
|
||||
booked = frappe.get_all(
|
||||
"GL Entry", filters={"voucher_no": pi.name, "is_cancelled": 0}, fields=["debit"]
|
||||
)
|
||||
self.assertEqual(sum(flt(d.debit) for d in booked), sum(by_warehouse.values()))
|
||||
|
||||
def test_purchase_invoice_with_use_serial_batch_field_for_rejected_qty(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
"setting_field": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"settings_doctype": "Buying Settings"
|
||||
},
|
||||
{
|
||||
"setting_field": "set_valuation_rate_for_rejected_materials",
|
||||
"settings_doctype": "Buying Settings"
|
||||
},
|
||||
{
|
||||
"setting_field": "unlink_payment_on_cancellation_of_invoice",
|
||||
"settings_doctype": "Accounts Settings"
|
||||
|
||||
@@ -77,3 +77,24 @@ class BuyingSettings(Document):
|
||||
def check_maintain_same_rate(self):
|
||||
if self.maintain_same_rate:
|
||||
self.set_landed_cost_based_on_purchase_invoice_rate = 0
|
||||
|
||||
|
||||
def is_rejected_material_valued(voucher_type: str) -> bool:
|
||||
"""Rejected material carries stock value only when something is going to pay for it. A Purchase
|
||||
Receipt books it against Stock Received But Not Billed. A Purchase Invoice pays for it only when
|
||||
it bills the received quantity, which is what the setting asks for."""
|
||||
if not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials"):
|
||||
return False
|
||||
|
||||
return voucher_type != "Purchase Invoice" or bool(
|
||||
frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")
|
||||
)
|
||||
|
||||
|
||||
def bills_rejected_quantity(doc) -> bool:
|
||||
"""An invoice that moves stock itself has no receipt to bill the rejected material for it, so it
|
||||
bills the received quantity when the settings ask for the material to be valued."""
|
||||
if doc.doctype != "Purchase Invoice" or not doc.get("update_stock"):
|
||||
return False
|
||||
|
||||
return is_rejected_material_valued(doc.doctype)
|
||||
|
||||
@@ -14,6 +14,10 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
from erpnext.buying.doctype.buying_settings.buying_settings import (
|
||||
bills_rejected_quantity,
|
||||
is_rejected_material_valued,
|
||||
)
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
@@ -477,7 +481,7 @@ class BuyingController(SubcontractingController):
|
||||
):
|
||||
net_rate = item.rejected_qty * item.net_rate
|
||||
|
||||
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
||||
qty_in_stock_uom = flt(self.get_valued_qty(item) * item.conversion_factor)
|
||||
if not qty_in_stock_uom and item.get("rejected_qty"):
|
||||
qty_in_stock_uom = flt(item.rejected_qty * item.conversion_factor)
|
||||
|
||||
@@ -492,6 +496,14 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
update_regional_item_valuation_rate(self)
|
||||
|
||||
def get_valued_qty(self, row):
|
||||
"""Quantity the net amount of the row was billed for, which is what its valuation spreads
|
||||
over."""
|
||||
if not flt(row.get("rejected_qty")) or not bills_rejected_quantity(self):
|
||||
return flt(row.qty)
|
||||
|
||||
return flt(row.qty) + flt(row.rejected_qty)
|
||||
|
||||
def get_tax_details(self):
|
||||
tax_accounts = []
|
||||
total_valuation_amount = 0.0
|
||||
@@ -871,7 +883,7 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
if flt(d.rejected_qty) != 0:
|
||||
valuation_rate_for_rejected_item = 0.0
|
||||
if frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials"):
|
||||
if is_rejected_material_valued(self.doctype):
|
||||
valuation_rate_for_rejected_item = d.valuation_rate
|
||||
|
||||
sl_entries.append(
|
||||
|
||||
@@ -13,6 +13,7 @@ from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules
|
||||
from erpnext.buying.doctype.buying_settings.buying_settings import bills_rejected_quantity
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
validate_conversion_rate,
|
||||
validate_inclusive_tax,
|
||||
@@ -241,13 +242,19 @@ class calculate_taxes_and_totals:
|
||||
elif not item.qty and self.doc.get("is_debit_note"):
|
||||
item.amount = flt(item.rate, item.precision("amount"))
|
||||
else:
|
||||
item.amount = flt(item.rate * item.qty, item.precision("amount"))
|
||||
item.amount = flt(item.rate * self.get_billed_qty(item), item.precision("amount"))
|
||||
item.net_amount = item.amount
|
||||
self._set_in_company_currency(
|
||||
item, ["price_list_rate", "rate_with_margin", "rate", "net_rate", "amount", "net_amount"]
|
||||
)
|
||||
item.item_tax_amount = 0.0
|
||||
|
||||
def get_billed_qty(self, item):
|
||||
if not flt(item.get("rejected_qty")) or not bills_rejected_quantity(self.doc):
|
||||
return flt(item.qty)
|
||||
|
||||
return flt(item.qty) + flt(item.rejected_qty)
|
||||
|
||||
def _set_in_company_currency(self, doc, fields):
|
||||
"""set values in base currency"""
|
||||
for f in fields:
|
||||
|
||||
@@ -157,6 +157,20 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
.filter((fieldname) => !do_not_round_fields.includes(fieldname));
|
||||
}
|
||||
|
||||
get_billed_qty(item) {
|
||||
const bills_rejected_quantity =
|
||||
this.frm.doc.doctype === "Purchase Invoice" &&
|
||||
this.frm.doc.update_stock &&
|
||||
this.frm.doc.set_valuation_rate_for_rejected_materials &&
|
||||
this.frm.doc.bill_for_rejected_quantity_in_purchase_invoice;
|
||||
|
||||
if (!flt(item.rejected_qty) || !bills_rejected_quantity) {
|
||||
return flt(item.qty);
|
||||
}
|
||||
|
||||
return flt(item.qty) + flt(item.rejected_qty);
|
||||
}
|
||||
|
||||
calculate_item_values() {
|
||||
var me = this;
|
||||
if (!this.discount_amount_applied) {
|
||||
@@ -167,7 +181,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
|
||||
|
||||
if (!(me.frm.doc.is_return || me.frm.doc.is_debit_note)) {
|
||||
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||
item.net_amount = item.amount = flt(
|
||||
item.rate * me.get_billed_qty(item),
|
||||
precision("amount", item)
|
||||
);
|
||||
} else {
|
||||
// allow for '0' qty on Credit/Debit notes
|
||||
let qty = flt(item.qty);
|
||||
|
||||
@@ -27,6 +27,9 @@ from frappe.utils import (
|
||||
)
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
|
||||
from erpnext.buying.doctype.buying_settings.buying_settings import (
|
||||
is_rejected_material_valued,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt_item.purchase_receipt_item import PurchaseReceiptItem
|
||||
from erpnext.stock.serial_batch_bundle import (
|
||||
BatchNoValuation,
|
||||
@@ -899,9 +902,7 @@ class SerialandBatchBundle(Document):
|
||||
if batches and valuation_method == "FIFO":
|
||||
stock_queue = parse_json(prev_sle.stock_queue)
|
||||
|
||||
set_valuation_rate_for_rejected_materials = frappe.db.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
)
|
||||
set_valuation_rate_for_rejected_materials = is_rejected_material_valued(self.voucher_type)
|
||||
|
||||
precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate")
|
||||
for d in self.entries:
|
||||
|
||||
Reference in New Issue
Block a user