mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-26 22:18:18 +00:00
Compare commits
15 Commits
version-15
...
v15.118.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6b4799b1a | ||
|
|
49eb2366cd | ||
|
|
fb2a4e5f98 | ||
|
|
0ce7917648 | ||
|
|
41038979ec | ||
|
|
88b6779062 | ||
|
|
b5f784612d | ||
|
|
457424f7a4 | ||
|
|
25ee3695f0 | ||
|
|
ff205da810 | ||
|
|
2980171007 | ||
|
|
c6c4815e8d | ||
|
|
82a85818c2 | ||
|
|
57a2be6b56 | ||
|
|
47f54a4725 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.112.0"
|
||||
__version__ = "15.118.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -15,7 +15,7 @@ class ERPNextAddress(Address):
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
if self.get("is_your_company_address"):
|
||||
if self.is_your_company_address:
|
||||
return
|
||||
|
||||
return super().link_address()
|
||||
@@ -26,9 +26,7 @@ class ERPNextAddress(Address):
|
||||
self.is_your_company_address = 1
|
||||
|
||||
def validate_reference(self):
|
||||
if self.get("is_your_company_address") and not [
|
||||
row for row in self.links if row.link_doctype == "Company"
|
||||
]:
|
||||
if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Address needs to be linked to a Company. Please add a row for Company in the Links table."
|
||||
|
||||
@@ -41,7 +41,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) {
|
||||
erpnext.journal_entry.lock_reversal_entry(frm);
|
||||
frm.set_read_only();
|
||||
}
|
||||
|
||||
erpnext.toggle_naming_series();
|
||||
@@ -513,13 +513,6 @@ $.extend(erpnext.journal_entry, {
|
||||
});
|
||||
},
|
||||
|
||||
lock_reversal_entry: function (frm) {
|
||||
frm.fields
|
||||
.filter((field) => field.has_input)
|
||||
.forEach((field) => frm.set_df_property(field.df.fieldname, "read_only", 1));
|
||||
frm.set_df_property("accounts", "read_only", 1);
|
||||
},
|
||||
|
||||
set_debit_credit_in_company_currency: function (frm, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
|
||||
|
||||
@@ -853,7 +853,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
|
||||
doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
companies = frappe.get_list(
|
||||
companies = frappe.get_all(
|
||||
doctype, filters={"docstatus": 1, party_type.lower(): party}, distinct=1, fields=["company"]
|
||||
)
|
||||
|
||||
|
||||
@@ -69,11 +69,6 @@ frappe.ui.form.on("BOM Operation", {
|
||||
const d = locals[cdt][cdn];
|
||||
frm.events.calculate_operating_cost(frm, d);
|
||||
},
|
||||
|
||||
hour_rate: function (frm, cdt, cdn) {
|
||||
const d = locals[cdt][cdn];
|
||||
frm.events.calculate_operating_cost(frm, d);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.tour["Routing"] = [
|
||||
|
||||
@@ -1461,13 +1461,7 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_work_order(
|
||||
bom_no: str,
|
||||
item: str,
|
||||
qty: float = 0,
|
||||
company: str | None = None,
|
||||
project: str | None = None,
|
||||
variant_items: str | list | None = None,
|
||||
use_multi_level_bom: bool | None = None,
|
||||
bom_no, item, qty=0, company=None, project=None, variant_items=None, use_multi_level_bom=None
|
||||
):
|
||||
from erpnext import get_default_company
|
||||
|
||||
@@ -1476,8 +1470,7 @@ def make_work_order(
|
||||
|
||||
item_details = get_item_details(item, project)
|
||||
|
||||
# selected BOM already belongs to this variant — keep it
|
||||
if frappe.db.get_value("Item", item, "variant_of") and frappe.db.get_value("BOM", bom_no, "item") != item:
|
||||
if frappe.db.get_value("Item", item, "variant_of"):
|
||||
if variant_bom := frappe.db.get_value(
|
||||
"BOM",
|
||||
{"item": item, "is_default": 1, "docstatus": 1},
|
||||
|
||||
@@ -298,7 +298,7 @@ def get_batches_by_oldest(item_code, warehouse):
|
||||
"""Returns the oldest batch and qty for the given item_code and warehouse"""
|
||||
batches = get_batch_qty(item_code=item_code, warehouse=warehouse)
|
||||
batches_dates = [[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches]
|
||||
batches_dates.sort(key=lambda tup: (tup[1] is None, tup[1]))
|
||||
batches_dates.sort(key=lambda tup: tup[1])
|
||||
return batches_dates
|
||||
|
||||
|
||||
|
||||
@@ -820,10 +820,7 @@ def raise_work_orders(material_request):
|
||||
|
||||
for d in mr.items:
|
||||
if (d.stock_qty - d.ordered_qty) > 0:
|
||||
if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1, "is_active": 1}) or (
|
||||
(variant_of := frappe.get_value("Item", d.item_code, "variant_of"))
|
||||
and frappe.db.exists("BOM", {"item": variant_of, "is_default": 1, "is_active": 1})
|
||||
):
|
||||
if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}):
|
||||
wo_order = frappe.new_doc("Work Order")
|
||||
wo_order.update(
|
||||
{
|
||||
|
||||
@@ -1339,9 +1339,6 @@ def create_dn_wo_so(pick_list, delivery_note=None):
|
||||
|
||||
delivery_note.company = pick_list.company
|
||||
|
||||
if not delivery_note.customer:
|
||||
delivery_note.customer = pick_list.customer
|
||||
|
||||
item_table_mapper_without_so = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
|
||||
@@ -2751,50 +2751,6 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
self.assertEqual(se.process_loss_qty, 50)
|
||||
|
||||
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||
def test_cancel_seeds_replay_from_before_posting_datetime_bucket(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
item = make_item().name
|
||||
warehouse = create_warehouse("Cancel Replay Warehouse", company="_Test Company")
|
||||
|
||||
def stock_entry(qty, posting_date, **kwargs):
|
||||
return make_stock_entry(
|
||||
item_code=item,
|
||||
qty=qty,
|
||||
company="_Test Company",
|
||||
posting_date=posting_date,
|
||||
posting_time="10:00:00",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
stock_entry(25, add_days(today(), -3), to_warehouse=warehouse, rate=10)
|
||||
|
||||
bucket_date = add_days(today(), -2)
|
||||
stock_entry(20, bucket_date, from_warehouse=warehouse)
|
||||
receipt = stock_entry(75, bucket_date, to_warehouse=warehouse, rate=10)
|
||||
duplicate_issue = stock_entry(20, bucket_date, from_warehouse=warehouse)
|
||||
|
||||
frappe.flags.dont_execute_stock_reposts = True
|
||||
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
|
||||
|
||||
duplicate_issue.reload()
|
||||
duplicate_issue.cancel()
|
||||
|
||||
self.assertEqual(
|
||||
flt(
|
||||
frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": receipt.name, "is_cancelled": 0},
|
||||
"qty_after_transaction",
|
||||
)
|
||||
),
|
||||
80,
|
||||
)
|
||||
|
||||
overdraw = stock_entry(100, add_days(today(), -1), from_warehouse=warehouse, do_not_submit=True)
|
||||
self.assertRaises(NegativeStockError, overdraw.submit)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -325,7 +325,6 @@ class FIFOSlots:
|
||||
del stock_ledger_entries
|
||||
|
||||
self._recompute_moving_average_slots()
|
||||
self._rebalance_negative_batch_slots()
|
||||
|
||||
if not self.filters.get("show_warehouse_wise_stock"):
|
||||
# (Item 1, WH 1), (Item 1, WH 2) => (Item 1)
|
||||
@@ -347,33 +346,6 @@ class FIFOSlots:
|
||||
if is_qty_slot(slot):
|
||||
slot[FIFO_VALUE_INDEX] = flt(slot[FIFO_QTY_INDEX] * rate)
|
||||
|
||||
def _rebalance_negative_batch_slots(self) -> None:
|
||||
for item_dict in self.item_details.values():
|
||||
if item_dict.get("has_batch_no"):
|
||||
self._rebalance_negative_batch_slot_values(item_dict["fifo_queue"])
|
||||
|
||||
def _rebalance_negative_batch_slot_values(self, fifo_queue: list) -> None:
|
||||
"""A batch is one valuation pool, so a slot driven negative by consumption
|
||||
at the pooled rate is stale detail: spread the pool value over its slots."""
|
||||
groups = {}
|
||||
for slot in fifo_queue:
|
||||
if is_batch_slot(slot):
|
||||
key = slot[BATCH_SLOT_BATCH_INDEX] if slot[BATCH_SLOT_VALUATION_INDEX] else None
|
||||
groups.setdefault(key, []).append(slot)
|
||||
|
||||
for slots in groups.values():
|
||||
has_negative_slot = any(
|
||||
flt(slot[BATCH_SLOT_VALUE_INDEX]) < 0 and flt(slot[BATCH_SLOT_QTY_INDEX]) > 0
|
||||
for slot in slots
|
||||
)
|
||||
total_qty = sum(flt(slot[BATCH_SLOT_QTY_INDEX]) for slot in slots)
|
||||
if not has_negative_slot or total_qty <= 0:
|
||||
continue
|
||||
|
||||
rate = sum(flt(slot[BATCH_SLOT_VALUE_INDEX]) for slot in slots) / total_qty
|
||||
for slot in slots:
|
||||
slot[BATCH_SLOT_VALUE_INDEX] = flt(slot[BATCH_SLOT_QTY_INDEX] * rate)
|
||||
|
||||
def _get_bundle_wise_details(self, stock_ledger_entries: list | None) -> tuple[dict, dict]:
|
||||
if stock_ledger_entries is not None:
|
||||
return frappe._dict({}), frappe._dict({})
|
||||
|
||||
@@ -620,58 +620,6 @@ class TestStockAgeing(FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_batch_issue_at_pooled_rate_keeps_slot_values_positive(self):
|
||||
"""Ledger (same wh, batch B): [+10 @ 0, +10 @ 10, -4 @ pooled 5]
|
||||
Consuming the zero-valued head slot at the pooled rate drives it
|
||||
negative; slot values are then rebalanced to the batch pool rate."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = make_item(
|
||||
"Test Stock Ageing Batch Pool Rebalance",
|
||||
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
|
||||
).name
|
||||
|
||||
batch_no = "SA-POOL-REBALANCE-BATCH"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
|
||||
def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
|
||||
return frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=actual_qty,
|
||||
qty_after_transaction=qty_after,
|
||||
stock_value_difference=stock_value_difference,
|
||||
valuation_rate=abs(stock_value_difference / actual_qty) if actual_qty else 0,
|
||||
warehouse="WH 1",
|
||||
posting_date=posting_date,
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no=voucher_no,
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=batch_no,
|
||||
)
|
||||
|
||||
sle = [
|
||||
make_sle("2021-12-01", "001", 10, 10, 0),
|
||||
make_sle("2021-12-02", "002", 10, 20, 100),
|
||||
make_sle("2021-12-03", "003", -4, 16, -20),
|
||||
]
|
||||
|
||||
slots = FIFOSlots(self.filters, sle).generate()
|
||||
queue = slots[item_code]["fifo_queue"]
|
||||
|
||||
self.assertEqual(
|
||||
queue,
|
||||
[
|
||||
[batch_no, 1, 6.0, "2021-12-01", 30.0],
|
||||
[batch_no, 1, 10.0, "2021-12-01", 50.0],
|
||||
],
|
||||
)
|
||||
|
||||
def test_sequential_stock_reco_same_warehouse(self):
|
||||
"""
|
||||
Test back to back stock recos (same warehouse).
|
||||
|
||||
@@ -130,7 +130,7 @@ frappe.query_reports["Stock Balance"] = {
|
||||
fieldname: "include_zero_stock_items",
|
||||
label: __("Include Zero Stock Items"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
fieldname: "show_dimension_wise_stock",
|
||||
|
||||
@@ -1762,7 +1762,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
voucher_no = args.get("voucher_no")
|
||||
voucher_condition = f"and voucher_no != '{voucher_no}'"
|
||||
|
||||
elif args.get("creation") and args.get("sle_id") and not args.get("cancelled"):
|
||||
elif args.get("creation") and args.get("sle_id"):
|
||||
creation = args.get("creation")
|
||||
operator = "<="
|
||||
voucher_condition = f"and creation < '{creation}'"
|
||||
|
||||
Reference in New Issue
Block a user