mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-03 05:28:27 +00:00
Merge pull request #51391 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -304,6 +304,7 @@ def create_payment_entry_bts(
|
||||
project=None,
|
||||
cost_center=None,
|
||||
allow_edit=None,
|
||||
company_bank_account=None,
|
||||
):
|
||||
# Create a new payment entry based on the bank transaction
|
||||
bank_transaction = frappe.db.get_values(
|
||||
@@ -345,6 +346,9 @@ def create_payment_entry_bts(
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
if company_bank_account:
|
||||
pe.bank_account = company_bank_account
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
|
||||
@@ -435,6 +435,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"paid_to",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
"party_name",
|
||||
],
|
||||
function (i, field) {
|
||||
frm.set_value(field, null);
|
||||
|
||||
@@ -115,6 +115,10 @@ class RepostAccountingLedger(Document):
|
||||
def generate_preview(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
|
||||
|
||||
if not self.vouchers:
|
||||
frappe.msgprint(_("Add vouchers to generate preview."))
|
||||
return
|
||||
|
||||
gl_columns = []
|
||||
gl_data = []
|
||||
|
||||
@@ -142,6 +146,7 @@ class RepostAccountingLedger(Document):
|
||||
account_repost_doc=self.name,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(_("Repost has started in the background"))
|
||||
else:
|
||||
|
||||
@@ -1056,3 +1056,21 @@ def add_party_account(party_type, party, company, account):
|
||||
|
||||
def render_address(address, check_permissions=True):
|
||||
return frappe.call(_render_address, address, check_permissions=check_permissions)
|
||||
|
||||
|
||||
def validate_party_currency_before_merging(party_type, old_party, new_party):
|
||||
for company in frappe.get_all("Company"):
|
||||
old_party_currency = get_party_gle_currency(party_type, old_party, company.name)
|
||||
new_party_currency = get_party_gle_currency(party_type, new_party, company.name)
|
||||
|
||||
if old_party_currency and new_party_currency and old_party_currency != new_party_currency:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'."
|
||||
).format(
|
||||
party_type,
|
||||
old_party,
|
||||
new_party,
|
||||
company.name,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -102,6 +102,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_gl_balance",
|
||||
label: __("Show GL Balance"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
|
||||
onload: function (report) {
|
||||
|
||||
@@ -53,7 +53,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
)
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company, self.account_type)
|
||||
|
||||
for party, party_dict in self.party_total.items():
|
||||
if flt(party_dict.outstanding, self.currency_precision) == 0:
|
||||
@@ -206,11 +206,15 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
)
|
||||
|
||||
|
||||
def get_gl_balance(report_date, company):
|
||||
def get_gl_balance(report_date, company, account_type):
|
||||
if account_type == "Payable":
|
||||
balance_calc_fields = ["party", "SUM(credit - debit) AS balance"]
|
||||
else:
|
||||
balance_calc_fields = ["party", "SUM(debit - credit) AS balance"]
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"GL Entry",
|
||||
fields=["party", "sum(debit - credit)"],
|
||||
fields=balance_calc_fields,
|
||||
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
|
||||
group_by="party",
|
||||
as_list=1,
|
||||
|
||||
@@ -480,6 +480,7 @@ class Asset(AccountsController):
|
||||
|
||||
def set_depreciation_rate(self):
|
||||
for d in self.get("finance_books"):
|
||||
self.validate_asset_finance_books(d)
|
||||
d.rate_of_depreciation = flt(
|
||||
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
|
||||
)
|
||||
@@ -488,6 +489,10 @@ class Asset(AccountsController):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
)
|
||||
|
||||
if flt(row.expected_value_after_useful_life) < 0:
|
||||
frappe.throw(_("Row {0}: Expected Value After Useful Life cannot be negative").format(row.idx))
|
||||
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
|
||||
frappe.throw(
|
||||
_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format(
|
||||
@@ -503,50 +508,71 @@ class Asset(AccountsController):
|
||||
title=_("Invalid Schedule"),
|
||||
)
|
||||
row.depreciation_start_date = get_last_day(self.available_for_use_date)
|
||||
self.validate_depreciation_start_date(row)
|
||||
self.validate_total_number_of_depreciations_and_frequency(row)
|
||||
|
||||
if not self.is_existing_asset:
|
||||
self.opening_accumulated_depreciation = 0
|
||||
self.opening_number_of_booked_depreciations = 0
|
||||
else:
|
||||
depreciable_amount = flt(
|
||||
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
|
||||
self.precision("gross_purchase_amount"),
|
||||
)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
self.validate_opening_depreciation_values(row)
|
||||
|
||||
def validate_depreciation_start_date(self, row):
|
||||
if row.depreciation_start_date:
|
||||
if getdate(row.depreciation_start_date) < getdate(self.purchase_date):
|
||||
frappe.throw(
|
||||
_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
|
||||
depreciable_amount
|
||||
_("Row #{0}: Next Depreciation Date cannot be before Purchase Date").format(row.idx)
|
||||
)
|
||||
|
||||
if getdate(row.depreciation_start_date) < getdate(self.available_for_use_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Next Depreciation Date cannot be before Available-for-use Date").format(
|
||||
row.idx
|
||||
)
|
||||
)
|
||||
|
||||
if self.opening_accumulated_depreciation:
|
||||
if not self.opening_number_of_booked_depreciations:
|
||||
frappe.throw(_("Please set Opening Number of Booked Depreciations"))
|
||||
else:
|
||||
self.opening_number_of_booked_depreciations = 0
|
||||
|
||||
if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
|
||||
).format(row.idx),
|
||||
title=_("Invalid Schedule"),
|
||||
)
|
||||
|
||||
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date").format(
|
||||
row.idx
|
||||
_("Row #{0}: Depreciation Start Date is required").format(row.idx),
|
||||
title=_("Invalid Schedule"),
|
||||
)
|
||||
|
||||
def validate_total_number_of_depreciations_and_frequency(self, row):
|
||||
if row.total_number_of_depreciations <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Total Number of Depreciations must be greater than zero").format(row.idx)
|
||||
)
|
||||
|
||||
if row.frequency_of_depreciation <= 0:
|
||||
frappe.throw(_("Row #{0}: Frequency of Depreciation must be greater than zero").format(row.idx))
|
||||
|
||||
def validate_opening_depreciation_values(self, row):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
)
|
||||
|
||||
depreciable_amount = flt(
|
||||
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
|
||||
self.precision("gross_purchase_amount"),
|
||||
)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
frappe.throw(
|
||||
_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
|
||||
depreciable_amount
|
||||
)
|
||||
)
|
||||
|
||||
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(
|
||||
self.available_for_use_date
|
||||
):
|
||||
if self.opening_accumulated_depreciation:
|
||||
if not self.opening_number_of_booked_depreciations:
|
||||
frappe.throw(_("Please set Opening Number of Booked Depreciations"))
|
||||
else:
|
||||
self.opening_number_of_booked_depreciations = 0
|
||||
|
||||
if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date"
|
||||
).format(row.idx)
|
||||
"Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
|
||||
).format(row.idx),
|
||||
title=_("Invalid Schedule"),
|
||||
)
|
||||
|
||||
def set_total_booked_depreciations(self):
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_
|
||||
from erpnext.accounts.party import (
|
||||
get_dashboard_info,
|
||||
validate_party_accounts,
|
||||
validate_party_currency_before_merging,
|
||||
)
|
||||
from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
@@ -208,6 +209,10 @@ class Supplier(TransactionBase):
|
||||
|
||||
delete_contact_and_address("Supplier", self.name)
|
||||
|
||||
def before_rename(self, olddn, newdn, merge=False):
|
||||
if merge:
|
||||
validate_party_currency_before_merging("Supplier", olddn, newdn)
|
||||
|
||||
def after_rename(self, olddn, newdn, merge=False):
|
||||
if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name":
|
||||
self.db_set("supplier_name", newdn)
|
||||
|
||||
@@ -17,6 +17,15 @@ class TestTimesheet(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Timesheet")
|
||||
|
||||
def test_timesheet_base_amount(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
|
||||
self.assertEqual(timesheet.time_logs[0].base_billing_rate, 50)
|
||||
self.assertEqual(timesheet.time_logs[0].base_costing_rate, 20)
|
||||
self.assertEqual(timesheet.time_logs[0].base_billing_amount, 100)
|
||||
self.assertEqual(timesheet.time_logs[0].base_costing_amount, 40)
|
||||
|
||||
def test_timesheet_billing_amount(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
@@ -236,4 +245,5 @@ def make_timesheet(
|
||||
def update_activity_type(activity_type):
|
||||
activity_type = frappe.get_doc("Activity Type", activity_type)
|
||||
activity_type.billing_rate = 50.0
|
||||
activity_type.costing_rate = 20.0
|
||||
activity_type.save(ignore_permissions=True)
|
||||
|
||||
@@ -296,6 +296,20 @@ class Timesheet(Document):
|
||||
data.billing_amount = data.billing_rate * hours
|
||||
data.costing_amount = data.costing_rate * costing_hours
|
||||
|
||||
exchange_rate = flt(self.get("exchange_rate")) or 1.0
|
||||
data.base_billing_rate = flt(
|
||||
data.billing_rate * exchange_rate, data.precision("base_billing_rate")
|
||||
)
|
||||
data.base_costing_rate = flt(
|
||||
data.costing_rate * exchange_rate, data.precision("base_costing_rate")
|
||||
)
|
||||
data.base_billing_amount = flt(
|
||||
data.billing_amount * exchange_rate, data.precision("base_billing_amount")
|
||||
)
|
||||
data.base_costing_amount = flt(
|
||||
data.costing_amount * exchange_rate, data.precision("base_costing_amount")
|
||||
)
|
||||
|
||||
def update_time_rates(self, ts_detail):
|
||||
if not ts_detail.is_billable:
|
||||
ts_detail.billing_rate = 0.0
|
||||
|
||||
@@ -361,6 +361,21 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
mandatory_depends_on:
|
||||
"eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
|
||||
},
|
||||
{
|
||||
fieldname: "bank_account",
|
||||
fieldtype: "Link",
|
||||
label: "Company Bank Account",
|
||||
options: "Bank Account",
|
||||
depends_on: "eval:doc.party",
|
||||
get_query: function () {
|
||||
return {
|
||||
filters: {
|
||||
is_company_account: 1,
|
||||
company: this.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
@@ -511,6 +526,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
mode_of_payment: values.mode_of_payment,
|
||||
project: values.project,
|
||||
cost_center: values.cost_center,
|
||||
company_bank_account: values?.bank_account || this?.bank_account,
|
||||
},
|
||||
callback: (response) => {
|
||||
const alert_string = __("Bank Transaction {0} added as Payment Entry", [
|
||||
@@ -582,6 +598,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
project: values.project,
|
||||
cost_center: values.cost_center,
|
||||
allow_edit: true,
|
||||
company_bank_account: values?.bank_account || this?.bank_account,
|
||||
},
|
||||
callback: (r) => {
|
||||
const doc = frappe.model.sync(r.message);
|
||||
|
||||
@@ -18,7 +18,11 @@ from frappe.utils import cint, cstr, flt, get_formatted_email, today
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from frappe.utils.user import get_users_with_role
|
||||
|
||||
from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
|
||||
from erpnext.accounts.party import (
|
||||
get_dashboard_info,
|
||||
validate_party_accounts,
|
||||
validate_party_currency_before_merging,
|
||||
)
|
||||
from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
@@ -367,6 +371,10 @@ class Customer(TransactionBase):
|
||||
if self.lead_name:
|
||||
frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name)
|
||||
|
||||
def before_rename(self, olddn, newdn, merge=False):
|
||||
if merge:
|
||||
validate_party_currency_before_merging("Customer", olddn, newdn)
|
||||
|
||||
def after_rename(self, olddn, newdn, merge=False):
|
||||
if frappe.defaults.get_global_default("cust_master_name") == "Customer Name":
|
||||
self.db_set("customer_name", newdn)
|
||||
|
||||
@@ -383,35 +383,23 @@ class PickList(TransactionBase):
|
||||
picked_items = get_picked_items_qty(packed_items, contains_packed_items=True)
|
||||
self.validate_picked_qty(picked_items)
|
||||
|
||||
picked_qty = frappe._dict()
|
||||
doc_updates = {}
|
||||
for d in picked_items:
|
||||
picked_qty[d.product_bundle_item] = d.picked_qty
|
||||
doc_updates[d.product_bundle_item] = {"picked_qty": flt(d.picked_qty)}
|
||||
|
||||
for packed_item in packed_items:
|
||||
frappe.db.set_value(
|
||||
"Packed Item",
|
||||
packed_item,
|
||||
"picked_qty",
|
||||
flt(picked_qty.get(packed_item)),
|
||||
update_modified=False,
|
||||
)
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update("Packed Item", doc_updates, update_modified=False)
|
||||
|
||||
def update_sales_order_item_qty(self, so_items):
|
||||
picked_items = get_picked_items_qty(so_items)
|
||||
self.validate_picked_qty(picked_items)
|
||||
|
||||
picked_qty = frappe._dict()
|
||||
doc_updates = {}
|
||||
for d in picked_items:
|
||||
picked_qty[d.sales_order_item] = d.picked_qty
|
||||
doc_updates[d.sales_order_item] = {"picked_qty": flt(d.picked_qty)}
|
||||
|
||||
for so_item in so_items:
|
||||
frappe.db.set_value(
|
||||
"Sales Order Item",
|
||||
so_item,
|
||||
"picked_qty",
|
||||
flt(picked_qty.get(so_item)),
|
||||
update_modified=False,
|
||||
)
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update("Sales Order Item", doc_updates, update_modified=False)
|
||||
|
||||
def update_sales_order_picking_status(self) -> None:
|
||||
sales_orders = []
|
||||
|
||||
@@ -17,13 +17,6 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
"Landed Cost Voucher": "Landed Cost Voucher",
|
||||
};
|
||||
|
||||
frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: frm.doc.company },
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("wip_composite_asset", "items", function () {
|
||||
return {
|
||||
filters: { is_composite_asset: 1, docstatus: 0 },
|
||||
@@ -171,6 +164,16 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
|
||||
this.frm.set_query("expense_account", "items", () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -312,6 +312,30 @@ class SerialandBatchBundle(Document):
|
||||
SerialNoDuplicateError,
|
||||
)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Stock Entry"
|
||||
and self.type_of_transaction == "Inward"
|
||||
and frappe.get_cached_value("Stock Entry", self.voucher_no, "purpose")
|
||||
in ["Manufacture", "Repack"]
|
||||
):
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial No", filters={"name": ("in", serial_nos), "status": "Delivered"}, pluck="name"
|
||||
)
|
||||
|
||||
if serial_nos:
|
||||
if len(serial_nos) == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Serial No {0} is already Delivered. You cannot use them again in Manufacture / Repack entry."
|
||||
).format(bold(serial_nos[0]))
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Serial Nos {0} are already Delivered. You cannot use them again in Manufacture / Repack entry."
|
||||
).format(bold(", ".join(serial_nos)))
|
||||
)
|
||||
|
||||
def throw_error_message(self, message, exception=frappe.ValidationError):
|
||||
frappe.throw(_(message), exception, title=_("Error"))
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"icon": "fa fa-barcode",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-15 13:40:21.938700",
|
||||
"modified": "2025-12-24 20:14:52.942251",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No",
|
||||
|
||||
@@ -302,3 +302,7 @@ def get_serial_nos_for_outward(kwargs):
|
||||
return []
|
||||
|
||||
return [d.serial_no for d in serial_nos]
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Serial No", ["item_code", "warehouse"])
|
||||
|
||||
@@ -2088,6 +2088,45 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
self.assertEqual(incoming_rate, 125.0)
|
||||
|
||||
def test_prevent_reuse_delivered_serial_no_in_repack(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
item = "Test Prevent Reuse Delivered Serial No"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
item_doc = make_item(item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SHGJ.####"})
|
||||
|
||||
make_stock_entry(item_code="_Test Item", target=warehouse, qty=2, rate=100)
|
||||
make_stock_entry(item_code=item, target=warehouse, qty=2, rate=100)
|
||||
|
||||
dn = create_delivery_note(item_code=item, qty=2)
|
||||
delivered_serial_no = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0]
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code="_Test Item", source=warehouse, qty=1, purpose="Repack", do_not_save=True
|
||||
)
|
||||
se.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_doc.name,
|
||||
"item_name": item_doc.item_name,
|
||||
"s_warehouse": None,
|
||||
"t_warehouse": warehouse,
|
||||
"description": item_doc.description,
|
||||
"uom": item_doc.stock_uom,
|
||||
"qty": 1,
|
||||
"use_serial_batch_fields": 1,
|
||||
"serial_no": delivered_serial_no,
|
||||
},
|
||||
)
|
||||
|
||||
se.save()
|
||||
status = frappe.db.get_value("Serial No", delivered_serial_no, "status")
|
||||
|
||||
self.assertEqual(status, "Delivered")
|
||||
self.assertEqual(se.purpose, "Repack")
|
||||
self.assertRaises(frappe.ValidationError, se.submit)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -20,6 +20,9 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_chart_data(data, filters):
|
||||
def wrap_in_quotes(label):
|
||||
return f"'{label}'"
|
||||
|
||||
if not data:
|
||||
return []
|
||||
|
||||
@@ -36,6 +39,9 @@ def get_chart_data(data, filters):
|
||||
data = data[:10]
|
||||
|
||||
for row in data:
|
||||
if row[0] == wrap_in_quotes(_("Total")):
|
||||
continue
|
||||
|
||||
labels.append(row[0])
|
||||
datapoints.append(row[-1])
|
||||
|
||||
|
||||
@@ -406,12 +406,7 @@ class SerialBatchBundle:
|
||||
|
||||
self.update_serial_no_status_warehouse(self.sle, serial_nos)
|
||||
|
||||
def update_serial_no_status_warehouse(self, sle, serial_nos):
|
||||
warehouse = sle.warehouse if sle.actual_qty > 0 else None
|
||||
|
||||
if isinstance(serial_nos, str):
|
||||
serial_nos = [serial_nos]
|
||||
|
||||
def get_status_for_serial_nos(self, sle):
|
||||
status = "Inactive"
|
||||
if sle.actual_qty < 0:
|
||||
status = "Delivered"
|
||||
@@ -425,6 +420,23 @@ class SerialBatchBundle:
|
||||
]:
|
||||
status = "Consumed"
|
||||
|
||||
if sle.is_cancelled == 1 and (
|
||||
sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed"
|
||||
):
|
||||
status = "Inactive"
|
||||
|
||||
return status
|
||||
|
||||
def update_serial_no_status_warehouse(self, sle, serial_nos):
|
||||
warehouse = sle.warehouse if sle.actual_qty > 0 else None
|
||||
|
||||
if isinstance(serial_nos, str):
|
||||
serial_nos = [serial_nos]
|
||||
|
||||
status = "Active"
|
||||
if not warehouse:
|
||||
status = self.get_status_for_serial_nos(sle)
|
||||
|
||||
customer = None
|
||||
if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0:
|
||||
customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")
|
||||
|
||||
Reference in New Issue
Block a user