Merge pull request #44342 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
ruthra kumar
2024-11-27 20:53:38 +05:30
committed by GitHub
23 changed files with 167 additions and 104 deletions

View File

@@ -94,8 +94,8 @@ frappe.ui.form.on("Account", {
function () { function () {
frappe.route_options = { frappe.route_options = {
account: frm.doc.name, account: frm.doc.name,
from_date: frappe.sys_defaults.year_start_date, from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
to_date: frappe.sys_defaults.year_end_date, to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
company: frm.doc.company, company: frm.doc.company,
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");

View File

@@ -279,8 +279,8 @@ frappe.treeview_settings["Account"] = {
click: function (node, btn) { click: function (node, btn) {
frappe.route_options = { frappe.route_options = {
account: node.label, account: node.label,
from_date: frappe.sys_defaults.year_start_date, from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
to_date: frappe.sys_defaults.year_end_date, to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
company: company:
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(), frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
}; };

View File

@@ -1523,7 +1523,7 @@ class PaymentEntry(AccountsController):
if paid_amount > total_negative_outstanding: if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0: if total_negative_outstanding == 0:
frappe.msgprint( frappe.msgprint(
_("Cannot {0} from {2} without any negative outstanding invoice").format( _("Cannot {0} from {1} without any negative outstanding invoice").format(
self.payment_type, self.payment_type,
self.party_type, self.party_type,
) )

View File

@@ -987,6 +987,45 @@ class TestPricingRule(FrappeTestCase):
so.save() so.save()
self.assertEqual(len(so.items), 1) self.assertEqual(len(so.items), 1)
def test_pricing_rule_for_product_free_item_round_free_qty(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [
{
"item_code": "_Test Item",
}
],
"selling": 1,
"rate": 0,
"min_qty": 100,
"max_qty": 0,
"price_or_product_discount": "Product",
"same_item": 1,
"free_qty": 10,
"round_free_qty": 1,
"is_recursive": 1,
"recurse_for": 100,
"company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=100)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 10)
so = make_sales_order(item_code="_Test Item", qty=150)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 10)
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self): def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")

View File

@@ -642,7 +642,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if transaction_qty: if transaction_qty:
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
if pricing_rule.round_free_qty: if pricing_rule.round_free_qty:
qty = math.floor(qty) qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
if not qty: if not qty:
return return

View File

@@ -27,7 +27,7 @@ class RepostAccountingLedger(Document):
latest_pcv = ( latest_pcv = (
frappe.db.get_all( frappe.db.get_all(
"Period Closing Voucher", "Period Closing Voucher",
filters={"company": self.company}, filters={"company": self.company, "docstatus": 1},
order_by="posting_date desc", order_by="posting_date desc",
pluck="posting_date", pluck="posting_date",
limit=1, limit=1,

View File

@@ -667,20 +667,6 @@ frappe.ui.form.on('Sales Invoice', {
} }
} }
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Company',
link_name: doc.company
}
};
});
frm.set_query('pos_profile', function(doc) { frm.set_query('pos_profile', function(doc) {
if(!doc.company) { if(!doc.company) {
frappe.throw(_('Please set Company')); frappe.throw(_('Please set Company'));

View File

@@ -1004,15 +1004,15 @@ class ReceivablePayableReport:
def get_columns(self): def get_columns(self):
self.columns = [] self.columns = []
self.add_column("Posting Date", fieldtype="Date") self.add_column(_("Posting Date"), fieldtype="Date")
self.add_column( self.add_column(
label="Party Type", label=_("Party Type"),
fieldname="party_type", fieldname="party_type",
fieldtype="Data", fieldtype="Data",
width=100, width=100,
) )
self.add_column( self.add_column(
label="Party", label=_("Party"),
fieldname="party", fieldname="party",
fieldtype="Dynamic Link", fieldtype="Dynamic Link",
options="party_type", options="party_type",
@@ -1028,10 +1028,10 @@ class ReceivablePayableReport:
if self.party_naming_by == "Naming Series": if self.party_naming_by == "Naming Series":
if self.account_type == "Payable": if self.account_type == "Payable":
label = "Supplier Name" label = _("Supplier Name")
fieldname = "supplier_name" fieldname = "supplier_name"
else: else:
label = "Customer Name" label = _("Customer Name")
fieldname = "customer_name" fieldname = "customer_name"
self.add_column( self.add_column(
label=label, label=label,
@@ -1057,7 +1057,7 @@ class ReceivablePayableReport:
width=180, width=180,
) )
self.add_column(label="Due Date", fieldtype="Date") self.add_column(label=_("Due Date"), fieldtype="Date")
if self.account_type == "Payable": if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")

View File

@@ -421,6 +421,7 @@ class GrossProfitGenerator:
if grouped_by_invoice: if grouped_by_invoice:
buying_amount = 0 buying_amount = 0
base_amount = 0
for row in reversed(self.si_list): for row in reversed(self.si_list):
if self.filters.get("group_by") == "Monthly": if self.filters.get("group_by") == "Monthly":
@@ -461,12 +462,11 @@ class GrossProfitGenerator:
else: else:
row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision) row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision)
if grouped_by_invoice: if grouped_by_invoice and row.indent == 0.0:
if row.indent == 1.0: row.buying_amount = buying_amount
buying_amount += row.buying_amount row.base_amount = base_amount
elif row.indent == 0.0: buying_amount = 0
row.buying_amount = buying_amount base_amount = 0
buying_amount = 0
# get buying rate # get buying rate
if flt(row.qty): if flt(row.qty):
@@ -476,11 +476,19 @@ class GrossProfitGenerator:
if self.is_not_invoice_row(row): if self.is_not_invoice_row(row):
row.buying_rate, row.base_rate = 0.0, 0.0 row.buying_rate, row.base_rate = 0.0, 0.0
if self.is_not_invoice_row(row):
self.update_return_invoices(row)
if grouped_by_invoice and row.indent == 1.0:
buying_amount += row.buying_amount
base_amount += row.base_amount
# calculate gross profit # calculate gross profit
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision) row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
if row.base_amount: if row.base_amount:
row.gross_profit_percent = flt( row.gross_profit_percent = flt(
(row.gross_profit / row.base_amount) * 100.0, self.currency_precision (row.gross_profit / row.base_amount) * 100.0,
self.currency_precision,
) )
else: else:
row.gross_profit_percent = 0.0 row.gross_profit_percent = 0.0
@@ -491,33 +499,29 @@ class GrossProfitGenerator:
if self.grouped: if self.grouped:
self.get_average_rate_based_on_group_by() self.get_average_rate_based_on_group_by()
def update_return_invoices(self, row):
if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]:
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
# returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
returned_item_row.qty = 0
returned_item_row.base_amount = 0
else:
row.qty = 0
row.base_amount = 0
returned_item_row.qty += row.qty
returned_item_row.base_amount += row.base_amount
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
def get_average_rate_based_on_group_by(self): def get_average_rate_based_on_group_by(self):
for key in list(self.grouped): for key in list(self.grouped):
if self.filters.get("group_by") == "Invoice": if self.filters.get("group_by") == "Payment Term":
for row in self.grouped[key]:
if row.indent == 1.0:
if (
row.parent in self.returned_invoices
and row.item_code in self.returned_invoices[row.parent]
):
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
# returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
returned_item_row.qty = 0
else:
row.qty = 0
returned_item_row.qty += row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(
flt(row.qty) * flt(row.buying_rate), self.currency_precision
)
if flt(row.qty) or row.base_amount:
row = self.set_average_rate(row)
self.grouped_data.append(row)
elif self.filters.get("group_by") == "Payment Term":
for i, row in enumerate(self.grouped[key]): for i, row in enumerate(self.grouped[key]):
invoice_portion = 0 invoice_portion = 0
@@ -537,7 +541,7 @@ class GrossProfitGenerator:
new_row = self.set_average_rate(new_row) new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row) self.grouped_data.append(new_row)
else: elif self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]): for i, row in enumerate(self.grouped[key]):
if i == 0: if i == 0:
new_row = row new_row = row

View File

@@ -418,12 +418,12 @@ class TestGrossProfit(FrappeTestCase):
"item_name": self.item, "item_name": self.item,
"warehouse": "Stores - _GP", "warehouse": "Stores - _GP",
"qty": 0.0, "qty": 0.0,
"avg._selling_rate": 0.0, "avg._selling_rate": 100,
"valuation_rate": 0.0, "valuation_rate": 0.0,
"selling_amount": -100.0, "selling_amount": 0.0,
"buying_amount": 0.0, "buying_amount": 0.0,
"gross_profit": -100.0, "gross_profit": 0.0,
"gross_profit_%": 100.0, "gross_profit_%": 0.0,
} }
gp_entry = [x for x in data if x.parent_invoice == sinv.name] gp_entry = [x for x in data if x.parent_invoice == sinv.name]
# Both items of Invoice should have '0' qty # Both items of Invoice should have '0' qty

View File

@@ -916,13 +916,12 @@ class Asset(AccountsController):
].expected_value_after_useful_life ].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation value_after_depreciation = self.finance_books[idx].value_after_depreciation
if ( if flt(value_after_depreciation) <= expected_value_after_useful_life:
flt(value_after_depreciation) <= expected_value_after_useful_life
or self.is_fully_depreciated
):
status = "Fully Depreciated" status = "Fully Depreciated"
self.is_fully_depreciated = 1
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount): elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
status = "Partially Depreciated" status = "Partially Depreciated"
self.is_fully_depreciated = 0
elif self.docstatus == 2: elif self.docstatus == 2:
status = "Cancelled" status = "Cancelled"
return status return status

View File

@@ -454,7 +454,7 @@ def restore_asset(asset_name):
def depreciate_asset(asset, date): def depreciate_asset(asset, date):
if not asset.calculate_depreciation: if not asset.calculate_depreciation or asset.is_fully_depreciated:
return return
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True

View File

@@ -18,6 +18,7 @@ def execute(filters=None):
columns = get_columns(filters) columns = get_columns(filters)
data = get_data(filters) data = get_data(filters)
update_received_amount(data)
if not data: if not data:
return [], [], None, [] return [], [], None, []
@@ -60,7 +61,6 @@ def get_data(filters):
(po_item.qty - po_item.received_qty).as_("pending_qty"), (po_item.qty - po_item.received_qty).as_("pending_qty"),
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"), Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
po_item.base_amount.as_("amount"), po_item.base_amount.as_("amount"),
(po_item.received_qty * po_item.base_rate).as_("received_qty_amount"),
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"), (po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
(po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_( (po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_(
"pending_amount" "pending_amount"
@@ -92,6 +92,39 @@ def get_data(filters):
return data return data
def update_received_amount(data):
pr_data = get_received_amount_data(data)
for row in data:
row.received_qty_amount = flt(pr_data.get(row.name))
def get_received_amount_data(data):
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
.on(pr_item.parent == pr.name)
.select(
pr_item.purchase_order_item,
Sum(pr_item.base_amount).as_("received_qty_amount"),
)
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
.groupby(pr_item.purchase_order_item)
)
query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
data = query.run()
if not data:
return frappe._dict()
return frappe._dict(data)
def prepare_data(data, filters): def prepare_data(data, filters):
completed, pending = 0, 0 completed, pending = 0, 0
pending_field = "pending_amount" pending_field = "pending_amount"

View File

@@ -15,6 +15,7 @@
"include_item_in_manufacturing", "include_item_in_manufacturing",
"qty_section", "qty_section",
"required_qty", "required_qty",
"stock_uom",
"rate", "rate",
"amount", "amount",
"column_break_11", "column_break_11",
@@ -138,11 +139,19 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Returned Qty ", "label": "Returned Qty ",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-02-11 15:45:32.318374", "modified": "2024-11-19 15:48:16.823384",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Item", "name": "Work Order Item",
@@ -153,4 +162,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -367,3 +367,4 @@ erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1 erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
erpnext.patches.v14_0.update_stock_uom_in_work_order_item

View File

@@ -0,0 +1,15 @@
import frappe
def execute():
frappe.db.sql(
"""
UPDATE
`tabWork Order Item`, `tabItem`
SET
`tabWork Order Item`.stock_uom = `tabItem`.stock_uom
WHERE
`tabWork Order Item`.item_code = `tabItem`.name
AND `tabWork Order Item`.docstatus = 1
"""
)

View File

@@ -85,9 +85,13 @@ $.extend(erpnext.queries, {
}, },
company_address_query: function (doc) { company_address_query: function (doc) {
if (!doc.company) {
frappe.throw(__("Please set {0}", [frappe.meta.get_label(doc.doctype, "company", doc.name)]));
}
return { return {
query: "frappe.contacts.doctype.address.address.address_query", query: "frappe.contacts.doctype.address.address.address_query",
filters: { is_your_company_address: 1, link_doctype: "Company", link_name: doc.company || "" }, filters: { link_doctype: "Company", link_name: doc.company },
}; };
}, },

View File

@@ -20,20 +20,6 @@ frappe.ui.form.on('Quotation', {
frm.set_df_property('packed_items', 'cannot_add_rows', true); frm.set_df_property('packed_items', 'cannot_add_rows', true);
frm.set_df_property('packed_items', 'cannot_delete_rows', true); frm.set_df_property('packed_items', 'cannot_delete_rows', true);
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Company',
link_name: doc.company
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@@ -21,20 +21,6 @@ frappe.ui.form.on("Sales Order", {
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
function(doc) { return (doc.stock_qty<=doc.delivered_qty) ? "green" : "orange" }) function(doc) { return (doc.stock_qty<=doc.delivered_qty) ? "green" : "orange" })
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Company',
link_name: doc.company
}
};
})
frm.set_query("bom_no", "items", function(doc, cdt, cdn) { frm.set_query("bom_no", "items", function(doc, cdt, cdn) {
var row = locals[cdt][cdn]; var row = locals[cdt][cdn];
return { return {

View File

@@ -40,6 +40,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
me.frm.set_query('customer_address', erpnext.queries.address_query); me.frm.set_query('customer_address', erpnext.queries.address_query);
me.frm.set_query('shipping_address_name', erpnext.queries.address_query); me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query); me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
me.frm.set_query('company_address', erpnext.queries.company_address_query);
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);

View File

@@ -8,7 +8,7 @@ frappe.query_reports["Batch Item Expiry Status"] = {
label: __("From Date"), label: __("From Date"),
fieldtype: "Date", fieldtype: "Date",
width: "80", width: "80",
default: frappe.sys_defaults.year_start_date, default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
reqd: 1, reqd: 1,
}, },
{ {

View File

@@ -16,7 +16,7 @@ frappe.query_reports["Batch-Wise Balance History"] = {
label: __("From Date"), label: __("From Date"),
fieldtype: "Date", fieldtype: "Date",
width: "80", width: "80",
default: frappe.sys_defaults.year_start_date, default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
reqd: 1, reqd: 1,
}, },
{ {

View File

@@ -7,7 +7,7 @@ frappe.query_reports["Itemwise Recommended Reorder Level"] = {
fieldname: "from_date", fieldname: "from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.sys_defaults.year_start_date, default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
}, },
{ {
fieldname: "to_date", fieldname: "to_date",