mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-21 12:27:14 +00:00
Compare commits
31 Commits
develop
...
fix/intern
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00c251c2f1 | ||
|
|
d4d704f336 | ||
|
|
6f2981cfe5 | ||
|
|
ca1f4de38d | ||
|
|
fb47d04929 | ||
|
|
a8051b0518 | ||
|
|
d7140099ca | ||
|
|
2c55621bae | ||
|
|
a1dc88e698 | ||
|
|
f6de755c83 | ||
|
|
7a7e186fb4 | ||
|
|
67f63e62c8 | ||
|
|
8290cd6294 | ||
|
|
00ff0684af | ||
|
|
3213810721 | ||
|
|
88860763c1 | ||
|
|
794e3050b7 | ||
|
|
bfcd9a33aa | ||
|
|
c13542f8d0 | ||
|
|
d790e47f90 | ||
|
|
032f36b361 | ||
|
|
4eb9796c04 | ||
|
|
6f5353b280 | ||
|
|
423383fb89 | ||
|
|
4d09a94768 | ||
|
|
87c42b0ec0 | ||
|
|
8b277f4b47 | ||
|
|
ab0d052fa2 | ||
|
|
fb905f9d6f | ||
|
|
8921f160d1 | ||
|
|
3eef93e792 |
@@ -18,6 +18,7 @@ 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
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.get_item_details import (
|
||||
NOT_APPLICABLE_TAX,
|
||||
get_conversion_factor,
|
||||
@@ -67,6 +68,7 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
self.update_valuation_rate()
|
||||
self.sync_accepted_packages()
|
||||
self.set_serial_and_batch_bundle()
|
||||
|
||||
def onload(self):
|
||||
@@ -146,12 +148,8 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.get(field) and not item.serial_and_batch_bundle and bundle_ids.get(item.get(field)):
|
||||
item.serial_and_batch_bundle = self.make_package_for_transfer(
|
||||
bundle_ids.get(item.get(field)),
|
||||
item.from_warehouse,
|
||||
type_of_transaction="Outward",
|
||||
do_not_submit=True,
|
||||
qty=item.qty,
|
||||
item.serial_and_batch_bundle = self.make_accepted_package(
|
||||
item, bundle_ids.get(item.get(field))
|
||||
)
|
||||
elif (
|
||||
not self.is_new()
|
||||
@@ -175,6 +173,76 @@ class BuyingController(SubcontractingController):
|
||||
):
|
||||
frappe.set_value("Serial and Batch Entry", sabe[0], "qty", item.qty)
|
||||
|
||||
def make_accepted_package(self, row, package) -> str:
|
||||
"""Package of the material the row accepts.
|
||||
|
||||
A row that rejects nothing keeps the package of the in-transit warehouse it came out of. A
|
||||
row that rejects material needs a package of the accepted warehouse instead, since that is
|
||||
the entry it belongs to; the material leaving the in-transit warehouse gets a package of its
|
||||
own when the receipt is submitted.
|
||||
"""
|
||||
if not (self.is_internal_receipt() and flt(row.rejected_qty)):
|
||||
return self.make_package_for_transfer(
|
||||
package,
|
||||
row.from_warehouse,
|
||||
type_of_transaction="Outward",
|
||||
do_not_submit=True,
|
||||
qty=flt(row.stock_qty),
|
||||
)
|
||||
|
||||
return self.make_package_for_transfer(
|
||||
package,
|
||||
row.warehouse,
|
||||
type_of_transaction="Inward",
|
||||
do_not_submit=True,
|
||||
qty=flt(row.stock_qty),
|
||||
exclude_serial_nos=self.get_rejected_serial_nos(row),
|
||||
)
|
||||
|
||||
def sync_accepted_packages(self) -> None:
|
||||
"""Move the package of a row to the accepted warehouse once the row rejects material."""
|
||||
if not self.is_internal_receipt() or self.is_return:
|
||||
return
|
||||
|
||||
for row in self.get("items"):
|
||||
package = row.get("serial_and_batch_bundle")
|
||||
if not (package and flt(row.rejected_qty)):
|
||||
continue
|
||||
|
||||
details = frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
package,
|
||||
["warehouse", "type_of_transaction", "docstatus"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not details or details.docstatus != 0:
|
||||
continue
|
||||
|
||||
if details.warehouse == row.warehouse and details.type_of_transaction == "Inward":
|
||||
continue
|
||||
|
||||
row.serial_and_batch_bundle = self.make_accepted_package(row, package)
|
||||
frappe.delete_doc("Serial and Batch Bundle", package, force=True, ignore_permissions=True)
|
||||
|
||||
def get_internal_transfer_qty(self, row) -> float:
|
||||
if flt(row.qty) or not self.is_internal_receipt():
|
||||
return flt(row.qty)
|
||||
|
||||
return flt(row.rejected_qty)
|
||||
|
||||
def get_rejected_serial_nos(self, row) -> list:
|
||||
if not flt(row.get("rejected_qty")):
|
||||
return []
|
||||
|
||||
if row.get("rejected_serial_and_batch_bundle"):
|
||||
return frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.rejected_serial_and_batch_bundle, "serial_no": ("is", "set")},
|
||||
pluck="serial_no",
|
||||
)
|
||||
|
||||
return get_serial_nos(row.get("rejected_serial_no"))
|
||||
|
||||
def set_rate_for_standalone_debit_note(self):
|
||||
if self.get("is_return") and self.get("update_stock") and not self.return_against:
|
||||
for row in self.items:
|
||||
@@ -466,7 +534,7 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
net_rate = self.get_internal_transfer_qty(item) * item.sales_incoming_rate
|
||||
|
||||
if (
|
||||
not net_rate
|
||||
@@ -754,6 +822,98 @@ class BuyingController(SubcontractingController):
|
||||
)
|
||||
)
|
||||
|
||||
def is_internal_receipt(self) -> bool:
|
||||
return self.doctype == "Purchase Receipt" and self.is_internal_transfer()
|
||||
|
||||
def get_source_warehouse_qty(self, row, accepted_qty):
|
||||
if not (self.is_internal_receipt() and flt(row.rejected_qty)):
|
||||
return accepted_qty
|
||||
|
||||
if row.get("serial_and_batch_bundle") and not row.get("rejected_serial_and_batch_bundle"):
|
||||
return accepted_qty
|
||||
|
||||
rejected_qty = flt(flt(row.rejected_qty) * flt(row.conversion_factor), row.precision("stock_qty"))
|
||||
|
||||
return flt(accepted_qty + rejected_qty, row.precision("stock_qty"))
|
||||
|
||||
def get_accepted_warehouse_package(self, row, type_of_transaction, via_landed_cost_voucher):
|
||||
"""Package for the entry into the accepted warehouse, which is the package of the row itself
|
||||
when the row rejects material."""
|
||||
if flt(row.rejected_qty) and self.is_internal_receipt() and not self.is_return:
|
||||
return row.serial_and_batch_bundle
|
||||
|
||||
if self.is_internal_transfer() and not self.is_return and self.docstatus != 2:
|
||||
return self.get_package_for_target_warehouse(
|
||||
row,
|
||||
type_of_transaction=type_of_transaction,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
|
||||
return row.serial_and_batch_bundle
|
||||
|
||||
def get_submitted_package(self, row, warehouse):
|
||||
return frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": row.name, "warehouse": warehouse, "is_cancelled": 0},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
def get_source_warehouse_reversal_package(self, row, package):
|
||||
if not (self.is_internal_transfer() and self.is_return):
|
||||
return package
|
||||
|
||||
if not row.get("rejected_serial_and_batch_bundle"):
|
||||
return self.get_package_for_target_warehouse(row, row.from_warehouse, "Inward")
|
||||
|
||||
return self.get_returned_source_package(row)
|
||||
|
||||
def get_source_warehouse_package(self, row, package):
|
||||
if not (package and row.get("rejected_serial_and_batch_bundle") and self.is_internal_receipt()):
|
||||
return package
|
||||
|
||||
if existing_package := frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
{
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
"warehouse": row.from_warehouse,
|
||||
"docstatus": 1,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
"name",
|
||||
):
|
||||
return existing_package
|
||||
|
||||
return self.make_package_for_transfer(
|
||||
package,
|
||||
row.from_warehouse,
|
||||
type_of_transaction="Outward",
|
||||
include_bundle=row.rejected_serial_and_batch_bundle,
|
||||
)
|
||||
|
||||
def get_returned_source_package(self, row):
|
||||
if existing_package := frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
{
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
"warehouse": row.from_warehouse,
|
||||
"docstatus": 1,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
"name",
|
||||
):
|
||||
return existing_package
|
||||
|
||||
return self.make_package_for_transfer(
|
||||
row.serial_and_batch_bundle,
|
||||
row.from_warehouse,
|
||||
type_of_transaction="Inward",
|
||||
include_bundle=row.rejected_serial_and_batch_bundle,
|
||||
)
|
||||
|
||||
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
self.update_ordered_and_reserved_qty()
|
||||
|
||||
@@ -764,110 +924,108 @@ class BuyingController(SubcontractingController):
|
||||
if d.item_code not in stock_items:
|
||||
continue
|
||||
|
||||
if d.warehouse:
|
||||
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
|
||||
source_reversal_sle = None
|
||||
|
||||
if pr_qty:
|
||||
if d.from_warehouse and (
|
||||
(not cint(self.is_return) and self.docstatus == 1)
|
||||
or (cint(self.is_return) and self.docstatus == 2)
|
||||
):
|
||||
serial_and_batch_bundle = d.get("serial_and_batch_bundle")
|
||||
if self.is_internal_transfer() and self.is_return and self.docstatus == 2:
|
||||
serial_and_batch_bundle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": d.name, "warehouse": d.from_warehouse},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
|
||||
source_qty = self.get_source_warehouse_qty(d, pr_qty)
|
||||
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -1 * pr_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"outgoing_rate": d.rate,
|
||||
"recalculate_rate": 1,
|
||||
"dependant_sle_voucher_detail_no": d.name,
|
||||
"serial_and_batch_bundle": serial_and_batch_bundle,
|
||||
},
|
||||
if source_qty and (d.warehouse or not pr_qty):
|
||||
if d.from_warehouse and (
|
||||
(not cint(self.is_return) and self.docstatus == 1)
|
||||
or (cint(self.is_return) and self.docstatus == 2)
|
||||
):
|
||||
serial_and_batch_bundle = d.get("serial_and_batch_bundle")
|
||||
if self.is_internal_transfer() and self.is_return and self.docstatus == 2:
|
||||
serial_and_batch_bundle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": d.name, "warehouse": d.from_warehouse},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
|
||||
type_of_transaction = "Inward"
|
||||
if self.docstatus == 2:
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
sle = self.get_sl_entries(
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": flt(pr_qty),
|
||||
"serial_and_batch_bundle": (
|
||||
d.serial_and_batch_bundle
|
||||
if not self.is_internal_transfer()
|
||||
or self.is_return
|
||||
or (self.is_internal_transfer() and self.docstatus == 2)
|
||||
else self.get_package_for_target_warehouse(
|
||||
d,
|
||||
type_of_transaction=type_of_transaction,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
"actual_qty": -1 * source_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"outgoing_rate": d.rate,
|
||||
"recalculate_rate": 1,
|
||||
"dependant_sle_voucher_detail_no": d.name,
|
||||
"serial_and_batch_bundle": self.get_source_warehouse_package(
|
||||
d, serial_and_batch_bundle
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
if self.is_return:
|
||||
outgoing_rate = get_rate_for_return(
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
|
||||
type_of_transaction = "Inward"
|
||||
if self.docstatus == 2:
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": flt(pr_qty),
|
||||
"serial_and_batch_bundle": self.get_accepted_warehouse_package(
|
||||
d, type_of_transaction, via_landed_cost_voucher
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
if self.is_return:
|
||||
outgoing_rate = get_rate_for_return(
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
|
||||
sle.update(
|
||||
{
|
||||
"outgoing_rate": outgoing_rate,
|
||||
"recalculate_rate": 1,
|
||||
"serial_and_batch_bundle": d.serial_and_batch_bundle,
|
||||
}
|
||||
)
|
||||
if d.from_warehouse:
|
||||
sle.dependant_sle_voucher_detail_no = d.name
|
||||
else:
|
||||
sle.update(
|
||||
{
|
||||
"incoming_rate": d.valuation_rate,
|
||||
"recalculate_rate": 1
|
||||
if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
|
||||
if d.from_warehouse and (
|
||||
(not cint(self.is_return) and self.docstatus == 2)
|
||||
or (cint(self.is_return) and self.docstatus == 1)
|
||||
):
|
||||
serial_and_batch_bundle = None
|
||||
if self.is_internal_transfer() and self.docstatus == 2:
|
||||
reversed_warehouse = (
|
||||
d.from_warehouse if d.get("rejected_serial_and_batch_bundle") else d.warehouse
|
||||
)
|
||||
serial_and_batch_bundle = self.get_submitted_package(d, reversed_warehouse)
|
||||
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -1 * source_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"recalculate_rate": 1,
|
||||
"serial_and_batch_bundle": self.get_source_warehouse_reversal_package(
|
||||
d, serial_and_batch_bundle
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
if self.is_internal_transfer() and self.is_return:
|
||||
from_warehouse_sle.incoming_rate = get_rate_for_return(
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
|
||||
sle.update(
|
||||
{
|
||||
"outgoing_rate": outgoing_rate,
|
||||
"recalculate_rate": 1,
|
||||
"serial_and_batch_bundle": d.serial_and_batch_bundle,
|
||||
}
|
||||
)
|
||||
if d.from_warehouse:
|
||||
sle.dependant_sle_voucher_detail_no = d.name
|
||||
else:
|
||||
sle.update(
|
||||
{
|
||||
"incoming_rate": d.valuation_rate,
|
||||
"recalculate_rate": 1
|
||||
if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
|
||||
if d.from_warehouse and (
|
||||
(not cint(self.is_return) and self.docstatus == 2)
|
||||
or (cint(self.is_return) and self.docstatus == 1)
|
||||
):
|
||||
serial_and_batch_bundle = None
|
||||
if self.is_internal_transfer() and self.docstatus == 2:
|
||||
serial_and_batch_bundle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": d.name, "warehouse": d.warehouse},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -1 * pr_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"recalculate_rate": 1,
|
||||
"serial_and_batch_bundle": (
|
||||
self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward")
|
||||
if self.is_internal_transfer() and self.is_return
|
||||
else serial_and_batch_bundle
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
source_reversal_sle = from_warehouse_sle
|
||||
|
||||
if flt(d.rejected_qty) != 0:
|
||||
valuation_rate_for_rejected_item = 0.0
|
||||
@@ -889,6 +1047,9 @@ class BuyingController(SubcontractingController):
|
||||
)
|
||||
)
|
||||
|
||||
if source_reversal_sle:
|
||||
sl_entries.append(source_reversal_sle)
|
||||
|
||||
self.make_sl_entries(
|
||||
sl_entries,
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
|
||||
@@ -260,12 +260,25 @@ class StockController(AccountsController):
|
||||
return SerialBatchBundleService(self).set_serial_and_batch_bundle(table_name, ignore_validate)
|
||||
|
||||
def make_package_for_transfer(
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None, qty=0
|
||||
self,
|
||||
serial_and_batch_bundle,
|
||||
warehouse,
|
||||
type_of_transaction=None,
|
||||
do_not_submit=None,
|
||||
qty=0,
|
||||
include_bundle=None,
|
||||
exclude_serial_nos=None,
|
||||
):
|
||||
from erpnext.stock.services.serial_batch_bundle_service import SerialBatchBundleService
|
||||
|
||||
return SerialBatchBundleService(self).make_package_for_transfer(
|
||||
serial_and_batch_bundle, warehouse, type_of_transaction, do_not_submit, qty
|
||||
serial_and_batch_bundle,
|
||||
warehouse,
|
||||
type_of_transaction,
|
||||
do_not_submit,
|
||||
qty,
|
||||
include_bundle,
|
||||
exclude_serial_nos,
|
||||
)
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
@@ -941,10 +954,20 @@ def make_bundle_for_material_transfer(**kwargs):
|
||||
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
if kwargs.include_bundle:
|
||||
for entry in frappe.get_doc("Serial and Batch Bundle", kwargs.include_bundle).entries:
|
||||
bundle_doc.append("entries", entry.as_dict(no_default_fields=True))
|
||||
|
||||
if kwargs.exclude_serial_nos:
|
||||
keep = [row for row in bundle_doc.entries if row.serial_no not in set(kwargs.exclude_serial_nos)]
|
||||
bundle_doc.entries = keep
|
||||
for idx, row in enumerate(keep, start=1):
|
||||
row.idx = idx
|
||||
|
||||
qty = 0
|
||||
if (
|
||||
len(bundle_doc.entries) == 1
|
||||
and flt(kwargs.qty) < flt(bundle_doc.total_qty)
|
||||
and abs(flt(kwargs.qty)) < abs(flt(bundle_doc.total_qty))
|
||||
and not bundle_doc.has_serial_no
|
||||
):
|
||||
qty = kwargs.qty
|
||||
|
||||
@@ -80,6 +80,7 @@ class PurchaseReceiptGLComposer(BaseStockGLComposer):
|
||||
if (
|
||||
doc.get("is_return")
|
||||
and item.return_qty_from_rejected_warehouse
|
||||
and not doc.is_internal_transfer()
|
||||
and not frappe.db.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
)
|
||||
@@ -101,11 +102,15 @@ class PurchaseReceiptGLComposer(BaseStockGLComposer):
|
||||
|
||||
outgoing_amount = item.base_net_amount
|
||||
if doc.is_internal_transfer() and item.valuation_rate:
|
||||
outgoing_amount = abs(get_stock_value_difference(doc.name, item.name, item.from_warehouse))
|
||||
outgoing_amount = -1 * flt(
|
||||
get_stock_value_difference(doc.name, item.name, item.from_warehouse)
|
||||
)
|
||||
credit_amount = outgoing_amount
|
||||
|
||||
if item.get("rejected_qty") and frappe.db.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
if (
|
||||
item.get("rejected_qty")
|
||||
and not doc.is_internal_transfer()
|
||||
and frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
|
||||
):
|
||||
outgoing_amount += get_stock_value_difference(doc.name, item.name, item.rejected_warehouse)
|
||||
credit_amount = outgoing_amount
|
||||
@@ -257,9 +262,7 @@ class PurchaseReceiptGLComposer(BaseStockGLComposer):
|
||||
valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount")
|
||||
)
|
||||
|
||||
if item.get("rejected_qty") and frappe.db.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
):
|
||||
if item.get("rejected_qty") and self.is_rejected_material_valued():
|
||||
rejected_item_cost = get_stock_value_difference(doc.name, item.name, item.rejected_warehouse)
|
||||
divisional_loss -= rejected_item_cost
|
||||
|
||||
@@ -347,7 +350,7 @@ class PurchaseReceiptGLComposer(BaseStockGLComposer):
|
||||
make_sub_contracting_gl_entries(d)
|
||||
make_divisional_loss_gl_entry(d, outgoing_amount)
|
||||
elif (d.warehouse and d.qty and d.warehouse not in warehouse_with_no_account) or (
|
||||
not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
|
||||
not self.is_rejected_material_valued()
|
||||
and d.rejected_warehouse
|
||||
and d.rejected_warehouse not in warehouse_with_no_account
|
||||
):
|
||||
@@ -356,9 +359,7 @@ class PurchaseReceiptGLComposer(BaseStockGLComposer):
|
||||
if d.is_fixed_asset and d.landed_cost_voucher_amount:
|
||||
doc.update_assets(d, d.valuation_rate)
|
||||
|
||||
if d.rejected_qty and frappe.db.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
):
|
||||
if d.rejected_qty and self.is_rejected_material_valued():
|
||||
stock_asset_rbnb = (
|
||||
doc.get_company_default("asset_received_but_not_billed")
|
||||
if d.is_fixed_asset
|
||||
@@ -380,6 +381,16 @@ class PurchaseReceiptGLComposer(BaseStockGLComposer):
|
||||
+ "\n".join(warehouse_with_no_account)
|
||||
)
|
||||
|
||||
def is_rejected_material_valued(self) -> bool:
|
||||
"""Rejected material carries stock value when Buying Settings asks for it, and always on an
|
||||
internal transfer, where that value is credited out of the in-transit warehouse."""
|
||||
if self.doc.is_internal_transfer():
|
||||
return True
|
||||
|
||||
return bool(
|
||||
frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
|
||||
)
|
||||
|
||||
def get_divisional_loss_account(self, item, stock_asset_rbnb):
|
||||
"""Account that absorbs the difference between the document value and the value actually
|
||||
booked into stock. For a Standard Cost item this difference is a purchase price variance
|
||||
|
||||
@@ -2258,6 +2258,723 @@ class TestPurchaseReceipt(ERPNextTestSuite):
|
||||
|
||||
self.assertEqual(query[0].value, 0)
|
||||
|
||||
def test_internal_transfer_pr_rejected_qty_leaves_in_transit_warehouse(self):
|
||||
"""Rejected material of an internal transfer leaves the in-transit warehouse along with the
|
||||
accepted material, and is booked into the rejected warehouse."""
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Rejected Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Rejected Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Rejected Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Rejected Transfer Rejected", company=company)
|
||||
|
||||
item_doc = create_item("Test Rejected Internal Transfer Item")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
warehouse=from_warehouse,
|
||||
qty=10,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 7
|
||||
pr.items[0].rejected_qty = 3
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "stock_value_difference"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
stock_value = {d.warehouse: d.stock_value_difference for d in sl_entries}
|
||||
|
||||
self.assertEqual(stock_qty[transit_warehouse], -10)
|
||||
self.assertEqual(stock_qty[to_warehouse], 7)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], 3)
|
||||
|
||||
self.assertEqual(stock_value[transit_warehouse], -1000)
|
||||
self.assertEqual(stock_value[to_warehouse], 700)
|
||||
self.assertEqual(stock_value[rejected_warehouse], 300)
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
|
||||
booked_value = {d.account: flt(d.debit) - flt(d.credit) for d in gl_entries}
|
||||
|
||||
self.assertEqual(sum(booked_value.values()), 0)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, transit_warehouse)], -1000)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, to_warehouse)], 700)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, rejected_warehouse)], 300)
|
||||
|
||||
def test_internal_transfer_rejected_qty_for_serial_item(self):
|
||||
"""Rejected serial numbers leave the in-transit warehouse and stay out of the package of
|
||||
accepted material."""
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Serial Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Serial Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Serial Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Serial Transfer Rejected", company=company)
|
||||
|
||||
item_doc = make_item(
|
||||
"_Test Serial Item For Rejected Transfer",
|
||||
{"has_serial_no": 1, "serial_no_series": "SN-SIFRT-.####"},
|
||||
)
|
||||
|
||||
receipt = make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
serial_nos = get_serial_nos_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
serial_no=serial_nos,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 7
|
||||
pr.items[0].rejected_qty = 3
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.items[0].rejected_serial_and_batch_bundle = make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_doc.name,
|
||||
"warehouse": rejected_warehouse,
|
||||
"qty": 3,
|
||||
"serial_nos": serial_nos[7:],
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"type_of_transaction": "Inward",
|
||||
"is_rejected": 1,
|
||||
"do_not_submit": True,
|
||||
"posting_date": pr.posting_date,
|
||||
"posting_time": pr.posting_time,
|
||||
}
|
||||
)
|
||||
).name
|
||||
pr.save()
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "serial_and_batch_bundle"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
self.assertEqual(stock_qty[transit_warehouse], -10)
|
||||
self.assertEqual(stock_qty[to_warehouse], 7)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], 3)
|
||||
|
||||
package = {d.warehouse: d.serial_and_batch_bundle for d in sl_entries}
|
||||
self.assertEqual(sorted(get_serial_nos_from_bundle(package[transit_warehouse])), sorted(serial_nos))
|
||||
self.assertEqual(sorted(get_serial_nos_from_bundle(package[to_warehouse])), sorted(serial_nos[:7]))
|
||||
self.assertEqual(
|
||||
sorted(get_serial_nos_from_bundle(package[rejected_warehouse])), sorted(serial_nos[7:])
|
||||
)
|
||||
|
||||
pr.cancel()
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Bin", {"warehouse": transit_warehouse, "item_code": item_doc.name}, "actual_qty"
|
||||
),
|
||||
10,
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"name": ("in", serial_nos), "warehouse": transit_warehouse},
|
||||
pluck="name",
|
||||
order_by="name",
|
||||
),
|
||||
sorted(serial_nos),
|
||||
)
|
||||
|
||||
def test_internal_transfer_rejected_qty_for_batch_item(self):
|
||||
"""A batch item rejected on an internal transfer leaves the in-transit warehouse with the
|
||||
accepted material, and the outgoing package holds both."""
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Batch Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Batch Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Batch Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Batch Transfer Rejected", company=company)
|
||||
|
||||
item_doc = make_item(
|
||||
"_Test Batch Item For Rejected Transfer",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BT-BIFRT-.####"},
|
||||
)
|
||||
|
||||
receipt = make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
batch_no=batch_no,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 7
|
||||
pr.items[0].rejected_qty = 3
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.items[0].rejected_serial_and_batch_bundle = make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_doc.name,
|
||||
"warehouse": rejected_warehouse,
|
||||
"qty": 3,
|
||||
"batches": frappe._dict({batch_no: 3}),
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"type_of_transaction": "Inward",
|
||||
"is_rejected": 1,
|
||||
"do_not_submit": True,
|
||||
"posting_date": pr.posting_date,
|
||||
"posting_time": pr.posting_time,
|
||||
}
|
||||
)
|
||||
).name
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "serial_and_batch_bundle"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
self.assertEqual(stock_qty[transit_warehouse], -10)
|
||||
self.assertEqual(stock_qty[to_warehouse], 7)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], 3)
|
||||
|
||||
package = {d.warehouse: d.serial_and_batch_bundle for d in sl_entries}
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Serial and Batch Bundle", package[transit_warehouse], "total_qty"), -10
|
||||
)
|
||||
|
||||
booked_value = {
|
||||
d.account: flt(d.debit) - flt(d.credit)
|
||||
for d in get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
|
||||
}
|
||||
self.assertEqual(sum(booked_value.values()), 0)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, transit_warehouse)], -1000)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, to_warehouse)], 700)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, rejected_warehouse)], 300)
|
||||
|
||||
pr.cancel()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Bin", {"warehouse": transit_warehouse, "item_code": item_doc.name}, "actual_qty"
|
||||
),
|
||||
10,
|
||||
)
|
||||
|
||||
def test_internal_transfer_of_batch_item_bought_in_another_uom(self):
|
||||
"""The package of the in-transit warehouse is sized in stock UOM, which is what the row is
|
||||
validated against."""
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Box Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Box Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Box Transfer To", company=company)
|
||||
|
||||
item_doc = make_item(
|
||||
"_Test Box Batch Item For Transfer",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BT-BOXT-.####"},
|
||||
)
|
||||
make_uom_conversion_factor("Box", "Nos", 12)
|
||||
|
||||
receipt = make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=12, rate=100
|
||||
)
|
||||
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=12,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
batch_no=batch_no,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].uom = "Box"
|
||||
pr.items[0].conversion_factor = 12
|
||||
pr.items[0].qty = 1
|
||||
pr.items[0].received_qty = 1
|
||||
pr.submit()
|
||||
|
||||
self.assertEqual(pr.items[0].stock_qty, 12)
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
self.assertEqual(stock_qty[transit_warehouse], -12)
|
||||
self.assertEqual(stock_qty[to_warehouse], 12)
|
||||
|
||||
def test_landed_cost_voucher_on_a_receipt_with_rejected_batch_material(self):
|
||||
"""A landed cost voucher rebuilds the entries of the receipt; the package of the in-transit
|
||||
warehouse has to be reused, or the batch is counted twice."""
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
create_landed_cost_voucher,
|
||||
)
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test LCV Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test LCV Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test LCV Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test LCV Transfer Rejected", company=company)
|
||||
|
||||
item_doc = make_item(
|
||||
"_Test Batch Item For LCV Transfer",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BT-BIFLT-.####"},
|
||||
)
|
||||
|
||||
receipt = make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
batch_no=batch_no,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 7
|
||||
pr.items[0].rejected_qty = 3
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.items[0].rejected_serial_and_batch_bundle = make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_doc.name,
|
||||
"warehouse": rejected_warehouse,
|
||||
"qty": 3,
|
||||
"batches": frappe._dict({batch_no: 3}),
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"type_of_transaction": "Inward",
|
||||
"is_rejected": 1,
|
||||
"do_not_submit": True,
|
||||
"posting_date": pr.posting_date,
|
||||
"posting_time": pr.posting_time,
|
||||
}
|
||||
)
|
||||
).name
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=120)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Batch", batch_no, "batch_qty"), 10)
|
||||
|
||||
packages = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
filters={
|
||||
"voucher_no": pr.name,
|
||||
"warehouse": transit_warehouse,
|
||||
"docstatus": 1,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
pluck="total_qty",
|
||||
)
|
||||
|
||||
self.assertEqual(packages, [-10])
|
||||
|
||||
def test_return_of_a_transfer_that_rejected_batch_material(self):
|
||||
"""Returning the whole receipt puts the accepted and the rejected material back into the
|
||||
in-transit warehouse, and leaves the batch qty where it started."""
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_return
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Return Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Return Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Return Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Return Transfer Rejected", company=company)
|
||||
|
||||
item_doc = make_item(
|
||||
"_Test Batch Item For Returned Transfer",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BT-BIFRET-.####"},
|
||||
)
|
||||
|
||||
receipt = make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
batch_no=batch_no,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 7
|
||||
pr.items[0].rejected_qty = 3
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.items[0].rejected_serial_and_batch_bundle = make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_doc.name,
|
||||
"warehouse": rejected_warehouse,
|
||||
"qty": 3,
|
||||
"batches": frappe._dict({batch_no: 3}),
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"type_of_transaction": "Inward",
|
||||
"is_rejected": 1,
|
||||
"do_not_submit": True,
|
||||
"posting_date": pr.posting_date,
|
||||
"posting_time": pr.posting_time,
|
||||
}
|
||||
)
|
||||
).name
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Batch", batch_no, "batch_qty"), 10)
|
||||
|
||||
pr_return = make_purchase_return(pr.name)
|
||||
pr_return.save()
|
||||
pr_return.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pr_return.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "serial_and_batch_bundle"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
self.assertEqual(stock_qty[transit_warehouse], 10)
|
||||
self.assertEqual(stock_qty[to_warehouse], -7)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], -3)
|
||||
|
||||
package = {d.warehouse: d.serial_and_batch_bundle for d in sl_entries}
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Serial and Batch Bundle", package[transit_warehouse], "total_qty"), 10
|
||||
)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Batch", batch_no, "batch_qty"), 10)
|
||||
|
||||
def test_rejecting_serial_numbers_after_the_package_was_built(self):
|
||||
"""Rejecting material after the row already has a package tops that package up, so it still
|
||||
covers everything that left the in-transit warehouse."""
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Top Up From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Top Up Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Top Up To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Top Up Rejected", company=company)
|
||||
|
||||
item_doc = make_item(
|
||||
"_Test Serial Item For Topped Up Package",
|
||||
{"has_serial_no": 1, "serial_no_series": "SN-SIFTUP-.####"},
|
||||
)
|
||||
|
||||
receipt = make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
serial_nos = get_serial_nos_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
serial_no=serial_nos,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 4
|
||||
pr.items[0].rejected_qty = 6
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
|
||||
# the desk fills the package of the row with the accepted serial numbers alone
|
||||
pr.items[0].serial_and_batch_bundle = make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_doc.name,
|
||||
"warehouse": transit_warehouse,
|
||||
"qty": -4,
|
||||
"serial_nos": serial_nos[:4],
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"type_of_transaction": "Outward",
|
||||
"do_not_submit": True,
|
||||
"posting_date": pr.posting_date,
|
||||
"posting_time": pr.posting_time,
|
||||
}
|
||||
)
|
||||
).name
|
||||
pr.items[0].rejected_serial_and_batch_bundle = make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_doc.name,
|
||||
"warehouse": rejected_warehouse,
|
||||
"qty": 6,
|
||||
"serial_nos": serial_nos[4:],
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"type_of_transaction": "Inward",
|
||||
"is_rejected": 1,
|
||||
"do_not_submit": True,
|
||||
"posting_date": pr.posting_date,
|
||||
"posting_time": pr.posting_time,
|
||||
}
|
||||
)
|
||||
).name
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "serial_and_batch_bundle"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
self.assertEqual(stock_qty[transit_warehouse], -10)
|
||||
self.assertEqual(stock_qty[to_warehouse], 4)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], 6)
|
||||
|
||||
package = {d.warehouse: d.serial_and_batch_bundle for d in sl_entries}
|
||||
self.assertEqual(sorted(get_serial_nos_from_bundle(package[transit_warehouse])), sorted(serial_nos))
|
||||
self.assertEqual(sorted(get_serial_nos_from_bundle(package[to_warehouse])), sorted(serial_nos[:4]))
|
||||
|
||||
def test_internal_transfer_with_every_unit_rejected(self):
|
||||
"""A receipt that rejects everything still empties the in-transit warehouse, and its entry
|
||||
balances."""
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test All Rejected From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test All Rejected Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test All Rejected To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test All Rejected Rejected", company=company)
|
||||
|
||||
item_doc = create_item("Test All Rejected Internal Transfer Item")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 0
|
||||
pr.items[0].rejected_qty = 10
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "stock_value_difference"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
self.assertEqual(stock_qty[transit_warehouse], -10)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], 10)
|
||||
self.assertNotIn(to_warehouse, stock_qty)
|
||||
|
||||
stock_value = {d.warehouse: d.stock_value_difference for d in sl_entries}
|
||||
self.assertEqual(stock_value[transit_warehouse], -1000)
|
||||
self.assertEqual(stock_value[rejected_warehouse], 1000)
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
|
||||
booked_value = {d.account: flt(d.debit) - flt(d.credit) for d in gl_entries}
|
||||
|
||||
self.assertEqual(sum(booked_value.values()), 0)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, transit_warehouse)], -1000)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, rejected_warehouse)], 1000)
|
||||
|
||||
def test_return_from_the_rejected_warehouse_of_an_internal_transfer(self):
|
||||
"""Returning rejected material of an internal transfer puts its value back into the
|
||||
in-transit warehouse instead of writing it off."""
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Rejected Return From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Rejected Return Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Rejected Return To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Rejected Return Rejected", company=company)
|
||||
|
||||
item_doc = create_item("Test Rejected Return Internal Transfer Item")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 0
|
||||
pr.items[0].rejected_qty = 10
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.submit()
|
||||
|
||||
pr_return = make_return_doc("Purchase Receipt", pr.name, return_against_rejected_qty=True)
|
||||
pr_return.save()
|
||||
pr_return.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pr_return.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "stock_value_difference"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
stock_value = {d.warehouse: d.stock_value_difference for d in sl_entries}
|
||||
|
||||
self.assertEqual(stock_qty[transit_warehouse], 10)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], -10)
|
||||
self.assertEqual(stock_value[transit_warehouse], 1000)
|
||||
self.assertEqual(stock_value[rejected_warehouse], -1000)
|
||||
|
||||
booked_value = {
|
||||
d.account: flt(d.debit) - flt(d.credit)
|
||||
for d in get_gl_entries("Purchase Receipt", pr_return.name, skip_cancelled=True)
|
||||
}
|
||||
|
||||
self.assertEqual(sum(booked_value.values()), 0)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, transit_warehouse)], 1000)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, rejected_warehouse)], -1000)
|
||||
|
||||
def test_internal_transfer_pr_incoming_sle_anchored_to_dn_rate(self):
|
||||
"""Internal-transfer PR's inward SLE must use DN.incoming_rate even when
|
||||
PR.item.valuation_rate was wrong at submit, so divisional_loss does not
|
||||
|
||||
@@ -843,6 +843,16 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
return return_against
|
||||
|
||||
def is_material_from_in_transit_warehouse(self) -> bool:
|
||||
"""Material of an internal transfer carries the value it had in the in-transit warehouse,
|
||||
rejected or not, because that warehouse is credited for all of it."""
|
||||
if self.voucher_type not in ("Purchase Receipt", "Purchase Invoice") or not self.voucher_detail_no:
|
||||
return False
|
||||
|
||||
return bool(
|
||||
frappe.get_cached_value(self.voucher_type + " Item", self.voucher_detail_no, "from_warehouse")
|
||||
)
|
||||
|
||||
def set_incoming_rate_for_inward_transaction(self, row=None, save=False, prev_sle=None):
|
||||
from erpnext.stock.utils import get_valuation_method
|
||||
|
||||
@@ -903,13 +913,17 @@ class SerialandBatchBundle(Document):
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
)
|
||||
|
||||
values_rejected_material = (
|
||||
set_valuation_rate_for_rejected_materials or self.is_material_from_in_transit_warehouse()
|
||||
)
|
||||
|
||||
precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate")
|
||||
for d in self.entries:
|
||||
fifo_batch_wise_val = True
|
||||
if valuation_method == "FIFO" and d.batch_no in batches:
|
||||
fifo_batch_wise_val = False
|
||||
|
||||
if self.is_rejected and not set_valuation_rate_for_rejected_materials:
|
||||
if self.is_rejected and not values_rejected_material:
|
||||
rate = 0.0
|
||||
elif (
|
||||
(flt(d.incoming_rate, precision) == flt(rate, precision))
|
||||
|
||||
@@ -580,7 +580,14 @@ class SerialBatchBundleService:
|
||||
)
|
||||
|
||||
def make_package_for_transfer(
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None, qty=0
|
||||
self,
|
||||
serial_and_batch_bundle,
|
||||
warehouse,
|
||||
type_of_transaction=None,
|
||||
do_not_submit=None,
|
||||
qty=0,
|
||||
include_bundle=None,
|
||||
exclude_serial_nos=None,
|
||||
):
|
||||
from erpnext.controllers.stock_controller import make_bundle_for_material_transfer
|
||||
|
||||
@@ -594,6 +601,8 @@ class SerialBatchBundleService:
|
||||
type_of_transaction=type_of_transaction,
|
||||
do_not_submit=do_not_submit,
|
||||
qty=qty,
|
||||
include_bundle=include_bundle,
|
||||
exclude_serial_nos=exclude_serial_nos,
|
||||
)
|
||||
|
||||
def validate_reserved_batches(self):
|
||||
|
||||
Reference in New Issue
Block a user