Merge branch 'develop' of https://github.com/frappe/erpnext into dev_fr_translation

This commit is contained in:
Florian HENRY 2023-09-22 16:53:13 +02:00
commit d1c69aa229
11 changed files with 136 additions and 73 deletions

View File

@ -154,6 +154,12 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm); frm.events.show_general_ledger(frm);
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
}, __('Actions'));
}
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
}, },

View File

@ -33,7 +33,7 @@ class PeriodClosingVoucher(AccountsController):
def on_cancel(self): def on_cancel(self):
self.validate_future_closing_vouchers() self.validate_future_closing_vouchers()
self.db_set("gle_processing_status", "In Progress") self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
gle_count = frappe.db.count( gle_count = frappe.db.count(
"GL Entry", "GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0}, {"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},

View File

@ -1801,6 +1801,10 @@ class TestSalesInvoice(unittest.TestCase):
) )
def test_outstanding_amount_after_advance_payment_entry_cancellation(self): def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
"""Test impact of advance PE submission/cancellation on SI and SO."""
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
pe = frappe.get_doc( pe = frappe.get_doc(
{ {
"doctype": "Payment Entry", "doctype": "Payment Entry",
@ -1820,10 +1824,25 @@ class TestSalesInvoice(unittest.TestCase):
"paid_to": "_Test Cash - _TC", "paid_to": "_Test Cash - _TC",
} }
) )
pe.append(
"references",
{
"reference_doctype": "Sales Order",
"reference_name": sales_order.name,
"total_amount": sales_order.grand_total,
"outstanding_amount": sales_order.grand_total,
"allocated_amount": 300,
},
)
pe.insert() pe.insert()
pe.submit() pe.submit()
sales_order.reload()
self.assertEqual(sales_order.advance_paid, 300)
si = frappe.copy_doc(test_records[0]) si = frappe.copy_doc(test_records[0])
si.items[0].sales_order = sales_order.name
si.items[0].so_detail = sales_order.get("items")[0].name
si.is_pos = 0 si.is_pos = 0
si.append( si.append(
"advances", "advances",
@ -1831,6 +1850,7 @@ class TestSalesInvoice(unittest.TestCase):
"doctype": "Sales Invoice Advance", "doctype": "Sales Invoice Advance",
"reference_type": "Payment Entry", "reference_type": "Payment Entry",
"reference_name": pe.name, "reference_name": pe.name,
"reference_row": pe.references[0].name,
"advance_amount": 300, "advance_amount": 300,
"allocated_amount": 300, "allocated_amount": 300,
"remarks": pe.remarks, "remarks": pe.remarks,
@ -1839,7 +1859,13 @@ class TestSalesInvoice(unittest.TestCase):
si.insert() si.insert()
si.submit() si.submit()
si.load_from_db() si.reload()
pe.reload()
sales_order.reload()
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(sales_order.advance_paid, 0.0)
# check outstanding after advance allocation # check outstanding after advance allocation
self.assertEqual( self.assertEqual(
@ -1847,11 +1873,9 @@ class TestSalesInvoice(unittest.TestCase):
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")), flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
) )
# added to avoid Document has been modified exception
pe = frappe.get_doc("Payment Entry", pe.name)
pe.cancel() pe.cancel()
si.reload()
si.load_from_db()
# check outstanding after advance cancellation # check outstanding after advance cancellation
self.assertEqual( self.assertEqual(
flt(si.outstanding_amount), flt(si.outstanding_amount),

View File

@ -581,6 +581,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
""" """
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they might be getting unlinked
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance # adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@ -647,6 +651,13 @@ def update_reference_in_payment_entry(
if d.voucher_detail_no: if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0] existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they are getting unlinked
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name
).set_total_advance_paid()
original_row = existing_row.as_dict().copy() original_row = existing_row.as_dict().copy()
existing_row.update(reference_details) existing_row.update(reference_details)

View File

@ -17,7 +17,7 @@ erpnext.accounts.unreconcile_payments = {
}, },
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
frm.add_custom_button(__("Un-Reconcile"), function() { frm.add_custom_button(__("UnReconcile"), function() {
erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
}, __('Actions')); }, __('Actions'));
} }
@ -87,11 +87,11 @@ erpnext.accounts.unreconcile_payments = {
unreconcile_dialog_fields[0].get_data = function(){ return r.message}; unreconcile_dialog_fields[0].get_data = function(){ return r.message};
let d = new frappe.ui.Dialog({ let d = new frappe.ui.Dialog({
title: 'Un-Reconcile Allocations', title: 'UnReconcile Allocations',
fields: unreconcile_dialog_fields, fields: unreconcile_dialog_fields,
size: 'large', size: 'large',
cannot_add_rows: true, cannot_add_rows: true,
primary_action_label: 'Un-Reconcile', primary_action_label: 'UnReconcile',
primary_action(values) { primary_action(values) {
let selected_allocations = values.allocations.filter(x=>x.__checked); let selected_allocations = values.allocations.filter(x=>x.__checked);

View File

@ -3,7 +3,7 @@ from frappe.model.db_query import DatabaseQuery
from frappe.utils import cint, flt from frappe.utils import cint, flt
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock, get_sre_reserved_qty_for_items_and_warehouses as get_reserved_stock_details,
) )
@ -61,7 +61,10 @@ def get_data(
limit_page_length=21, limit_page_length=21,
) )
sre_reserved_stock_details = get_reserved_stock(item_code, warehouse) item_code_list = [item_code] if item_code else [i.item_code for i in items]
warehouse_list = [warehouse] if warehouse else [i.warehouse for i in items]
sre_reserved_stock_details = get_reserved_stock_details(item_code_list, warehouse_list)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
for item in items: for item in items:
@ -75,7 +78,8 @@ def get_data(
"reserved_qty_for_production": flt(item.reserved_qty_for_production, precision), "reserved_qty_for_production": flt(item.reserved_qty_for_production, precision),
"reserved_qty_for_sub_contract": flt(item.reserved_qty_for_sub_contract, precision), "reserved_qty_for_sub_contract": flt(item.reserved_qty_for_sub_contract, precision),
"actual_qty": flt(item.actual_qty, precision), "actual_qty": flt(item.actual_qty, precision),
"reserved_stock": sre_reserved_stock_details, "reserved_stock": flt(sre_reserved_stock_details.get((item.item_code, item.warehouse))),
} }
) )
return items return items

View File

@ -346,7 +346,7 @@ class StockReconciliation(StockController):
"""Raises an exception if there is any reserved stock for the items in the Stock Reconciliation.""" """Raises an exception if there is any reserved stock for the items in the Stock Reconciliation."""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_for_item_and_warehouse as get_sre_reserved_qty_details, get_sre_reserved_qty_for_items_and_warehouses as get_sre_reserved_qty_details,
) )
item_code_list, warehouse_list = [], [] item_code_list, warehouse_list = [], []

View File

@ -1,42 +1,42 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Stock Reservation Entry", { frappe.ui.form.on('Stock Reservation Entry', {
refresh(frm) { refresh(frm) {
frm.trigger("set_queries"); frm.trigger('set_queries');
frm.trigger("toggle_read_only_fields"); frm.trigger('toggle_read_only_fields');
frm.trigger("hide_rate_related_fields"); frm.trigger('hide_rate_related_fields');
frm.trigger("hide_primary_action_button"); frm.trigger('hide_primary_action_button');
frm.trigger("make_sb_entries_warehouse_read_only"); frm.trigger('make_sb_entries_warehouse_read_only');
}, },
has_serial_no(frm) { has_serial_no(frm) {
frm.trigger("toggle_read_only_fields"); frm.trigger('toggle_read_only_fields');
}, },
has_batch_no(frm) { has_batch_no(frm) {
frm.trigger("toggle_read_only_fields"); frm.trigger('toggle_read_only_fields');
}, },
warehouse(frm) { warehouse(frm) {
if (frm.doc.warehouse) { if (frm.doc.warehouse) {
frm.doc.sb_entries.forEach((row) => { frm.doc.sb_entries.forEach((row) => {
frappe.model.set_value(row.doctype, row.name, "warehouse", frm.doc.warehouse); frappe.model.set_value(row.doctype, row.name, 'warehouse', frm.doc.warehouse);
}); });
} }
}, },
set_queries(frm) { set_queries(frm) {
frm.set_query("warehouse", () => { frm.set_query('warehouse', () => {
return { return {
filters: { filters: {
"is_group": 0, 'is_group': 0,
"company": frm.doc.company, 'company': frm.doc.company,
} }
}; };
}); });
frm.set_query("serial_no", "sb_entries", function(doc, cdt, cdn) { frm.set_query('serial_no', 'sb_entries', function(doc, cdt, cdn) {
var selected_serial_nos = doc.sb_entries.map(row => { var selected_serial_nos = doc.sb_entries.map(row => {
return row.serial_no; return row.serial_no;
}); });
@ -45,16 +45,16 @@ frappe.ui.form.on("Stock Reservation Entry", {
filters: { filters: {
item_code: doc.item_code, item_code: doc.item_code,
warehouse: row.warehouse, warehouse: row.warehouse,
status: "Active", status: 'Active',
name: ["not in", selected_serial_nos], name: ['not in', selected_serial_nos],
} }
} }
}); });
frm.set_query("batch_no", "sb_entries", function(doc, cdt, cdn) { frm.set_query('batch_no', 'sb_entries', function(doc, cdt, cdn) {
let filters = { let filters = {
item: doc.item_code, item: doc.item_code,
batch_qty: [">", 0], batch_qty: ['>', 0],
disabled: 0, disabled: 0,
} }
@ -63,7 +63,7 @@ frappe.ui.form.on("Stock Reservation Entry", {
return row.batch_no; return row.batch_no;
}); });
filters.name = ["not in", selected_batch_nos]; filters.name = ['not in', selected_batch_nos];
} }
return { filters: filters } return { filters: filters }
@ -74,37 +74,37 @@ frappe.ui.form.on("Stock Reservation Entry", {
if (frm.doc.has_serial_no) { if (frm.doc.has_serial_no) {
frm.doc.sb_entries.forEach(row => { frm.doc.sb_entries.forEach(row => {
if (row.qty !== 1) { if (row.qty !== 1) {
frappe.model.set_value(row.doctype, row.name, "qty", 1); frappe.model.set_value(row.doctype, row.name, 'qty', 1);
} }
}) })
} }
frm.fields_dict.sb_entries.grid.update_docfield_property( frm.fields_dict.sb_entries.grid.update_docfield_property(
"serial_no", "read_only", !frm.doc.has_serial_no 'serial_no', 'read_only', !frm.doc.has_serial_no
); );
frm.fields_dict.sb_entries.grid.update_docfield_property( frm.fields_dict.sb_entries.grid.update_docfield_property(
"batch_no", "read_only", !frm.doc.has_batch_no 'batch_no', 'read_only', !frm.doc.has_batch_no
); );
// Qty will always be 1 for Serial No. // Qty will always be 1 for Serial No.
frm.fields_dict.sb_entries.grid.update_docfield_property( frm.fields_dict.sb_entries.grid.update_docfield_property(
"qty", "read_only", frm.doc.has_serial_no 'qty', 'read_only', frm.doc.has_serial_no
); );
frm.set_df_property("sb_entries", "allow_on_submit", frm.doc.against_pick_list ? 0 : 1); frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1);
}, },
hide_rate_related_fields(frm) { hide_rate_related_fields(frm) {
["incoming_rate", "outgoing_rate", "stock_value_difference", "is_outward", "stock_queue"].forEach(field => { ['incoming_rate', 'outgoing_rate', 'stock_value_difference', 'is_outward', 'stock_queue'].forEach(field => {
frm.fields_dict.sb_entries.grid.update_docfield_property( frm.fields_dict.sb_entries.grid.update_docfield_property(
field, "hidden", 1 field, 'hidden', 1
); );
}); });
}, },
hide_primary_action_button(frm) { hide_primary_action_button(frm) {
// Hide "Amend" button on cancelled document // Hide 'Amend' button on cancelled document
if (frm.doc.docstatus == 2) { if (frm.doc.docstatus == 2) {
frm.page.btn_primary.hide() frm.page.btn_primary.hide()
} }
@ -112,15 +112,15 @@ frappe.ui.form.on("Stock Reservation Entry", {
make_sb_entries_warehouse_read_only(frm) { make_sb_entries_warehouse_read_only(frm) {
frm.fields_dict.sb_entries.grid.update_docfield_property( frm.fields_dict.sb_entries.grid.update_docfield_property(
"warehouse", "read_only", 1 'warehouse', 'read_only', 1
); );
}, },
}); });
frappe.ui.form.on("Serial and Batch Entry", { frappe.ui.form.on('Serial and Batch Entry', {
sb_entries_add(frm, cdt, cdn) { sb_entries_add(frm, cdt, cdn) {
if (frm.doc.warehouse) { if (frm.doc.warehouse) {
frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.warehouse); frappe.model.set_value(cdt, cdn, 'warehouse', frm.doc.warehouse);
} }
}, },
}); });

View File

@ -14,7 +14,7 @@ class StockReservationEntry(Document):
self.validate_amended_doc() self.validate_amended_doc()
self.validate_mandatory() self.validate_mandatory()
self.validate_for_group_warehouse() self.validate_group_warehouse()
validate_disabled_warehouse(self.warehouse) validate_disabled_warehouse(self.warehouse)
validate_warehouse_company(self.warehouse, self.company) validate_warehouse_company(self.warehouse, self.company)
self.validate_uom_is_integer() self.validate_uom_is_integer()
@ -74,7 +74,7 @@ class StockReservationEntry(Document):
msg = _("{0} is required").format(self.meta.get_label(d)) msg = _("{0} is required").format(self.meta.get_label(d))
frappe.throw(msg) frappe.throw(msg)
def validate_for_group_warehouse(self) -> None: def validate_group_warehouse(self) -> None:
"""Raises an exception if `Warehouse` is a Group Warehouse.""" """Raises an exception if `Warehouse` is a Group Warehouse."""
if frappe.get_cached_value("Warehouse", self.warehouse, "is_group"): if frappe.get_cached_value("Warehouse", self.warehouse, "is_group"):
@ -544,10 +544,36 @@ def get_available_serial_nos_to_reserve(
return available_serial_nos_list return available_serial_nos_list
def get_sre_reserved_qty_for_item_and_warehouse( def get_sre_reserved_qty_for_item_and_warehouse(item_code: str, warehouse: str = None) -> float:
item_code: str | list, warehouse: str | list = None """Returns current `Reserved Qty` for Item and Warehouse combination."""
) -> float | dict:
"""Returns `Reserved Qty` for Item and Warehouse combination OR a dict like {("item_code", "warehouse"): "reserved_qty", ... }.""" sre = frappe.qb.DocType("Stock Reservation Entry")
query = (
frappe.qb.from_(sre)
.select(Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"))
.where(
(sre.docstatus == 1)
& (sre.item_code == item_code)
& (sre.status.notin(["Delivered", "Cancelled"]))
)
.groupby(sre.item_code, sre.warehouse)
)
if warehouse:
query = query.where(sre.warehouse == warehouse)
reserved_qty = query.run(as_list=True)
return flt(reserved_qty[0][0]) if reserved_qty else 0.0
def get_sre_reserved_qty_for_items_and_warehouses(
item_code_list: list, warehouse_list: list = None
) -> dict:
"""Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }."""
if not item_code_list:
return {}
sre = frappe.qb.DocType("Stock Reservation Entry") sre = frappe.qb.DocType("Stock Reservation Entry")
query = ( query = (
@ -557,29 +583,20 @@ def get_sre_reserved_qty_for_item_and_warehouse(
sre.warehouse, sre.warehouse,
Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"), Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"),
) )
.where((sre.docstatus == 1) & (sre.status.notin(["Delivered", "Cancelled"]))) .where(
(sre.docstatus == 1)
& sre.item_code.isin(item_code_list)
& (sre.status.notin(["Delivered", "Cancelled"]))
)
.groupby(sre.item_code, sre.warehouse) .groupby(sre.item_code, sre.warehouse)
) )
query = ( if warehouse_list:
query.where(sre.item_code.isin(item_code)) query = query.where(sre.warehouse.isin(warehouse_list))
if isinstance(item_code, list)
else query.where(sre.item_code == item_code)
)
if warehouse:
query = (
query.where(sre.warehouse.isin(warehouse))
if isinstance(warehouse, list)
else query.where(sre.warehouse == warehouse)
)
data = query.run(as_dict=True) data = query.run(as_dict=True)
if isinstance(item_code, str) and isinstance(warehouse, str): return {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in data} if data else {}
return data[0]["reserved_qty"] if data else 0.0
else:
return {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in data} if data else {}
def get_sre_reserved_qty_details_for_voucher(voucher_type: str, voucher_no: str) -> dict: def get_sre_reserved_qty_details_for_voucher(voucher_type: str, voucher_no: str) -> dict:
@ -711,7 +728,7 @@ def get_serial_batch_entries_for_voucher(sre_name: str) -> list[dict]:
).run(as_dict=True) ).run(as_dict=True)
def get_ssb_bundle_for_voucher(sre: dict) -> object | None: def get_ssb_bundle_for_voucher(sre: dict) -> object:
"""Returns a new `Serial and Batch Bundle` against the provided SRE.""" """Returns a new `Serial and Batch Bundle` against the provided SRE."""
sb_entries = get_serial_batch_entries_for_voucher(sre["name"]) sb_entries = get_serial_batch_entries_for_voucher(sre["name"])

View File

@ -4,13 +4,14 @@
frappe.listview_settings['Stock Reservation Entry'] = { frappe.listview_settings['Stock Reservation Entry'] = {
get_indicator: function (doc) { get_indicator: function (doc) {
const status_colors = { const status_colors = {
'Draft': 'red', 'Draft': 'red',
'Partially Reserved': 'orange', 'Partially Reserved': 'orange',
'Reserved': 'blue', 'Reserved': 'blue',
'Partially Delivered': 'purple', 'Partially Delivered': 'purple',
'Delivered': 'green', 'Delivered': 'green',
'Cancelled': 'red', 'Cancelled': 'red',
}; };
return [__(doc.status), status_colors[doc.status], 'status,=,' + doc.status];
return [__(doc.status), status_colors[doc.status], 'status,=,' + doc.status];
}, },
}; };

View File

@ -165,7 +165,7 @@ class StockBalanceReport(object):
def get_sre_reserved_qty_details(self) -> dict: def get_sre_reserved_qty_details(self) -> dict:
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_qty_details, get_sre_reserved_qty_for_items_and_warehouses as get_reserved_qty_details,
) )
item_code_list, warehouse_list = [], [] item_code_list, warehouse_list = [], []