Merge branch 'develop' into fix_flaky_test_in_payment_terms_report
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
index 75f8f06..9e67c4c 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
@@ -29,6 +29,7 @@
"root_type",
"is_group",
"tax_rate",
+ "account_currency",
]:
account_number = cstr(child.get("account_number")).strip()
@@ -95,7 +96,17 @@
is_group = child.get("is_group")
elif len(
set(child.keys())
- - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
+ - set(
+ [
+ "account_name",
+ "account_type",
+ "root_type",
+ "is_group",
+ "tax_rate",
+ "account_number",
+ "account_currency",
+ ]
+ )
):
is_group = 1
else:
@@ -185,6 +196,7 @@
"root_type",
"tax_rate",
"account_number",
+ "account_currency",
],
order_by="lft, rgt",
)
@@ -267,6 +279,7 @@
"root_type",
"is_group",
"tax_rate",
+ "account_currency",
]:
continue
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 220b747..cb7da17 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -36,7 +36,7 @@
no_of_columns = max([len(d) for d in data])
- if no_of_columns > 7:
+ if no_of_columns > 8:
frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")),
@@ -233,6 +233,7 @@
is_group,
account_type,
root_type,
+ account_currency,
) = i
if not account_name:
@@ -253,6 +254,8 @@
charts_map[account_name]["account_type"] = account_type
if root_type:
charts_map[account_name]["root_type"] = root_type
+ if account_currency:
+ charts_map[account_name]["account_currency"] = account_currency
path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created
line_no += 1
@@ -315,6 +318,7 @@
"Is Group",
"Account Type",
"Root Type",
+ "Account Currency",
]
writer = UnicodeWriter()
writer.writerow(fields)
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index d67d59b..a4f6a74 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -211,8 +211,7 @@
# Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]:
- # TODO: Set new balance in Base/Account currency
- if d.balance > 0:
+ if d.balance != 0:
current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0'
@@ -399,6 +398,9 @@
journal_entry_accounts = []
for d in accounts:
+ if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
+ continue
+
dr_or_cr = (
"debit_in_account_currency"
if d.get("balance_in_account_currency") > 0
@@ -448,7 +450,13 @@
}
)
- journal_entry_accounts.append(
+ journal_entry.set("accounts", journal_entry_accounts)
+ journal_entry.set_amounts_in_company_currency()
+ journal_entry.set_total_debit_credit()
+
+ self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
+ journal_entry.append(
+ "accounts",
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
@@ -460,10 +468,9 @@
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
- }
+ },
)
- journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
journal_entry.save()
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 498fc7c..80e7222 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -137,7 +137,8 @@
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
- "options": "Finance Book"
+ "options": "Finance Book",
+ "read_only": 1
},
{
"fieldname": "2_add_edit_gl_entries",
@@ -538,7 +539,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2023-01-17 12:53:53.280620",
+ "modified": "2023-03-01 14:58:59.286591",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 2f43914..7005c17 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -495,26 +495,22 @@
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
- grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid)
-
+ grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
-
elif dt == "POS Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
-
elif dt == "Fees":
grand_total = ref_doc.outstanding_amount
if grand_total > 0:
return grand_total
-
else:
frappe.throw(_("Payment Entry is already created"))
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 477c726..4279aa4 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -45,7 +45,10 @@
frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self):
- so_inr = make_sales_order(currency="INR")
+ so_inr = make_sales_order(currency="INR", do_not_save=True)
+ so_inr.disable_rounded_total = 1
+ so_inr.save()
+
pr = make_payment_request(
dt="Sales Order",
dn=so_inr.name,
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index a1239d6..b40649b 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -161,7 +161,7 @@
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(
- abs(available_batch_qty - reserved_batch_qty - item.qty)
+ abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
)
bold_invalid_batch_no = frappe.bold(item.batch_no)
@@ -172,7 +172,7 @@
).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"),
)
- elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
+ elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
frappe.throw(
_(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@@ -246,7 +246,7 @@
),
title=_("Item Unavailable"),
)
- elif is_stock_item and flt(available_stock) < flt(d.qty):
+ elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@@ -651,7 +651,7 @@
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
- max_available_bundles = available_qty / item.qty
+ max_available_bundles = available_qty / item.stock_qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 21addab..b79af71 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1485,11 +1485,17 @@
if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
+ adjust_incoming_rate = frappe.db.get_single_value(
+ "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
+ )
+
for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
pr_doc = frappe.get_doc("Purchase Receipt", pr)
- update_billing_percentage(pr_doc, update_modified=update_modified)
+ update_billing_percentage(
+ pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate
+ )
def get_pr_details_billed_amt(self):
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index f901257..a6d7df6 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1523,6 +1523,94 @@
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
+ def test_adjust_incoming_rate(self):
+ frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
+
+ frappe.db.set_single_value(
+ "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1
+ )
+
+ # Increase the cost of the item
+
+ pr = make_purchase_receipt(qty=1, rate=100)
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 100)
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.rate = 150
+
+ pi.save()
+ pi.submit()
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 150)
+
+ # Reduce the cost of the item
+
+ pr = make_purchase_receipt(qty=1, rate=100)
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 100)
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.rate = 50
+
+ pi.save()
+ pi.submit()
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 50)
+
+ frappe.db.set_single_value(
+ "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0
+ )
+
+ # Don't adjust incoming rate
+
+ pr = make_purchase_receipt(qty=1, rate=100)
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 100)
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.rate = 50
+
+ pi.save()
+ pi.submit()
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 100)
+
+ frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
+
def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice")
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html
index 475be92..2d5ca49 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.html
+++ b/erpnext/accounts/report/general_ledger/general_ledger.html
@@ -38,8 +38,11 @@
{% if(data[i].posting_date) { %}
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
<td>{%= data[i].voucher_type %}
- <br>{%= data[i].voucher_no %}</td>
- <td>
+ <br>{%= data[i].voucher_no %}
+ </td>
+ {% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
+ <td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
+ <span>
{% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %}
<br>
@@ -49,11 +52,14 @@
{% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
- </td>
- <td style="text-align: right">
- {%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td>
- <td style="text-align: right">
- {%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td>
+ </span>
+ </td>
+ <td style="text-align: right">
+ {%= format_currency(data[i].debit, filters.presentation_currency) %}
+ </td>
+ <td style="text-align: right">
+ {%= format_currency(data[i].credit, filters.presentation_currency) %}
+ </td>
{% } else { %}
<td></td>
<td></td>
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
index c28b2b3..3d2dff1 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
@@ -43,9 +43,9 @@
if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
- $.each(frm.doc.schedules || [], function(i, row) {
+
+ $.each(frm.doc.depreciation_schedule || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
- frappe.model.set_value(row.doctype, row.name,
- "accumulated_depreciation_amount", accumulated_depreciation);
+ frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
})
};
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
index 898c482..d38508d 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -10,7 +10,9 @@
"asset",
"naming_series",
"column_break_2",
+ "gross_purchase_amount",
"opening_accumulated_depreciation",
+ "number_of_depreciations_booked",
"finance_book",
"finance_book_id",
"depreciation_details_section",
@@ -148,18 +150,36 @@
"read_only": 1
},
{
- "depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
+ "hidden": 1,
"label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "gross_purchase_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Gross Purchase Amount",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "number_of_depreciations_booked",
+ "fieldtype": "Int",
+ "hidden": 1,
+ "label": "Number of Depreciations Booked",
+ "print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-01-16 21:08:21.421260",
+ "modified": "2023-02-26 16:37:23.734806",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 6f02662..b75fbcb 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -4,7 +4,15 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month
+from frappe.utils import (
+ add_days,
+ add_months,
+ cint,
+ flt,
+ get_last_day,
+ getdate,
+ is_last_day_of_the_month,
+)
class AssetDepreciationSchedule(Document):
@@ -83,15 +91,58 @@
date_of_return=None,
update_asset_finance_book_row=True,
):
+ have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc)
+ not_manual_depr_or_have_manual_depr_details_been_modified = (
+ self.not_manual_depr_or_have_manual_depr_details_been_modified(row)
+ )
+
self.set_draft_asset_depr_schedule_details(asset_doc, row)
- self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
- self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+
+ if self.should_prepare_depreciation_schedule(
+ have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
+ ):
+ self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
+ self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+
+ def have_asset_details_been_modified(self, asset_doc):
+ return (
+ asset_doc.gross_purchase_amount != self.gross_purchase_amount
+ or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
+ or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
+ )
+
+ def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
+ return (
+ self.depreciation_method != "Manual"
+ or row.total_number_of_depreciations != self.total_number_of_depreciations
+ or row.frequency_of_depreciation != self.frequency_of_depreciation
+ or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date
+ or row.expected_value_after_useful_life != self.expected_value_after_useful_life
+ )
+
+ def should_prepare_depreciation_schedule(
+ self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
+ ):
+ if not self.get("depreciation_schedule"):
+ return True
+
+ old_asset_depr_schedule_doc = self.get_doc_before_save()
+
+ if self.docstatus != 0 and not old_asset_depr_schedule_doc:
+ return True
+
+ if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified:
+ return True
+
+ return False
def set_draft_asset_depr_schedule_details(self, asset_doc, row):
self.asset = asset_doc.name
self.finance_book = row.finance_book
self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation
+ self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked
+ self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations
self.frequency_of_depreciation = row.frequency_of_depreciation
@@ -102,7 +153,7 @@
def make_depr_schedule(
self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
):
- if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"):
+ if not self.get("depreciation_schedule"):
self.depreciation_schedule = []
if not asset_doc.available_for_use_date:
@@ -293,7 +344,9 @@
ignore_booked_entry=False,
):
straight_line_idx = [
- d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line"
+ d.idx
+ for d in self.get("depreciation_schedule")
+ if d.depreciation_method == "Straight Line" or d.depreciation_method == "Manual"
]
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 652dcf0..95857e4 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -18,6 +18,7 @@
"pr_required",
"column_break_12",
"maintain_same_rate",
+ "set_landed_cost_based_on_purchase_invoice_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"disable_last_purchase_rate",
@@ -147,6 +148,14 @@
"fieldname": "show_pay_button",
"fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: !doc.maintain_same_rate",
+ "description": "Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.",
+ "fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
+ "fieldtype": "Check",
+ "label": "Set Landed Cost Based on Purchase Invoice Rate"
}
],
"icon": "fa fa-cog",
@@ -154,7 +163,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-02-15 14:42:10.200679",
+ "modified": "2023-02-28 15:41:32.686805",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py
index be1ebde..4680a88 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.py
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.py
@@ -21,3 +21,10 @@
self.get("supp_master_name") == "Naming Series",
hide_name_field=False,
)
+
+ def before_save(self):
+ self.check_maintain_same_rate()
+
+ def check_maintain_same_rate(self):
+ if self.maintain_same_rate:
+ self.set_landed_cost_based_on_purchase_invoice_rate = 0
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
index 6304a09..9db769d 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
@@ -22,14 +22,14 @@
fieldname:"from_date",
label: __("From Date"),
fieldtype: "Date",
- default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
- default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
+ default: frappe.datetime.get_today(),
reqd: 1
},
]
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
index b6739fe..7e5338f 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
@@ -22,14 +22,14 @@
fieldname:"from_date",
label: __("From Date"),
fieldtype: "Date",
- default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
- default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
+ default: frappe.datetime.get_today(),
reqd: 1
},
]
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 4f7d9ad..e15b612 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -265,7 +265,10 @@
) / qty_in_stock_uom
else:
item.valuation_rate = (
- item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
+ item.base_net_amount
+ + item.item_tax_amount
+ + flt(item.landed_cost_voucher_amount)
+ + flt(item.get("rate_difference_with_purchase_invoice"))
) / qty_in_stock_uom
else:
item.valuation_rate = 0.0
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index fc6793a..15c270e 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -131,7 +131,7 @@
)
elif ref.serial_no:
- if not d.serial_no:
+ if d.qty and not d.serial_no:
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
else:
serial_nos = get_serial_nos(d.serial_no)
@@ -252,7 +252,6 @@
child.parent = par.name and par.docstatus = 1
and par.is_return = 1 and par.return_against = %s
group by item_code
- for update
""".format(
column, doc.doctype, doc.doctype
),
@@ -401,6 +400,16 @@
if serial_nos:
target_doc.serial_no = "\n".join(serial_nos)
+ if source_doc.get("rejected_serial_no"):
+ returned_serial_nos = get_returned_serial_nos(
+ source_doc, source_parent, serial_no_field="rejected_serial_no"
+ )
+ rejected_serial_nos = list(
+ set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos)
+ )
+ if rejected_serial_nos:
+ target_doc.rejected_serial_no = "\n".join(rejected_serial_nos)
+
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
@@ -611,7 +620,7 @@
return filters
-def get_returned_serial_nos(child_doc, parent_doc):
+def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
return_ref_field = frappe.scrub(child_doc.doctype)
@@ -620,7 +629,7 @@
serial_nos = []
- fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)]
+ fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name],
@@ -630,6 +639,6 @@
]
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
- serial_nos.extend(get_serial_nos(row.serial_no))
+ serial_nos.extend(get_serial_nos(row.get(serial_no_field)))
return serial_nos
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 8b4d28b..fc16a91 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -136,7 +136,7 @@
self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self):
- if not self.meta.get_field("commission_rate"):
+ if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted():
return
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 8c403aa..1edd7bf 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -24,11 +24,19 @@
def __init__(self, doc: Document):
self.doc = doc
frappe.flags.round_off_applicable_accounts = []
+
+ self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
+
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
+ def filter_rows(self):
+ """Exclude rows, that do not fulfill the filter criteria, from totals computation."""
+ items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
+ return items
+
def calculate(self):
- if not len(self.doc.get("items")):
+ if not len(self._items):
return
self.discount_amount_applied = False
@@ -70,7 +78,7 @@
if hasattr(self.doc, "tax_withholding_net_total"):
sum_net_amount = 0
sum_base_net_amount = 0
- for item in self.doc.get("items"):
+ for item in self._items:
if hasattr(item, "apply_tds") and item.apply_tds:
sum_net_amount += item.net_amount
sum_base_net_amount += item.base_net_amount
@@ -79,7 +87,7 @@
self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self):
- for item in self.doc.get("items"):
+ for item in self._items:
if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
@@ -137,7 +145,7 @@
return
if not self.discount_amount_applied:
- for item in self.doc.get("items"):
+ for item in self._items:
self.doc.round_floats_in(item)
if item.discount_percentage == 100:
@@ -236,7 +244,7 @@
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return
- for item in self.doc.get("items"):
+ for item in self._items:
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0
@@ -317,7 +325,7 @@
self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
- for item in self.doc.get("items"):
+ for item in self._items:
self.doc.total += item.amount
self.doc.total_qty += item.qty
self.doc.base_total += item.base_amount
@@ -354,7 +362,7 @@
]
)
- for n, item in enumerate(self.doc.get("items")):
+ for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
for i, tax in enumerate(self.doc.get("taxes")):
# tax_amount represents the amount of tax for the current step
@@ -363,7 +371,7 @@
# Adjust divisional loss to the last item
if tax.charge_type == "Actual":
actual_tax_dict[tax.idx] -= current_tax_amount
- if n == len(self.doc.get("items")) - 1:
+ if n == len(self._items) - 1:
current_tax_amount += actual_tax_dict[tax.idx]
# accumulate tax amount into tax.tax_amount
@@ -391,7 +399,7 @@
)
# set precision in the last item iteration
- if n == len(self.doc.get("items")) - 1:
+ if n == len(self._items) - 1:
self.round_off_totals(tax)
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
@@ -570,7 +578,7 @@
def calculate_total_net_weight(self):
if self.doc.meta.get_field("total_net_weight"):
self.doc.total_net_weight = 0.0
- for d in self.doc.items:
+ for d in self._items:
if d.total_weight:
self.doc.total_net_weight += d.total_weight
@@ -630,7 +638,7 @@
if total_for_discount_amount:
# calculate item amount after Discount Amount
- for i, item in enumerate(self.doc.get("items")):
+ for i, item in enumerate(self._items):
distributed_amount = (
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
)
@@ -643,7 +651,7 @@
self.doc.apply_discount_on == "Net Total"
or not taxes
or total_for_discount_amount == self.doc.net_total
- ) and i == len(self.doc.get("items")) - 1:
+ ) and i == len(self._items) - 1:
discount_amount_loss = flt(
self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total")
)
diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py
index 4673230..7c3c387 100644
--- a/erpnext/controllers/website_list_for_contact.py
+++ b/erpnext/controllers/website_list_for_contact.py
@@ -76,12 +76,9 @@
ignore_permissions = False
if not filters:
- filters = []
+ filters = {}
- if doctype in ["Supplier Quotation", "Purchase Invoice"]:
- filters.append((doctype, "docstatus", "<", 2))
- else:
- filters.append((doctype, "docstatus", "=", 1))
+ filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1
if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation":
parties_doctype = (
@@ -92,12 +89,12 @@
if customers:
if doctype == "Quotation":
- filters.append(("quotation_to", "=", "Customer"))
- filters.append(("party_name", "in", customers))
+ filters["quotation_to"] = "Customer"
+ filters["party_name"] = ["in", customers]
else:
- filters.append(("customer", "in", customers))
+ filters["customer"] = ["in", customers]
elif suppliers:
- filters.append(("supplier", "in", suppliers))
+ filters["supplier"] = ["in", suppliers]
elif not custom:
return []
@@ -110,7 +107,7 @@
if not customers and not suppliers and custom:
ignore_permissions = False
- filters = []
+ filters = {}
transactions = get_list_for_transactions(
doctype,
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 1f76a1a..b261795 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -19,10 +19,6 @@
}
}
});
-
- if (frm.doc.opportunity_from && frm.doc.party_name){
- frm.trigger('set_contact_link');
- }
},
validate: function(frm) {
@@ -130,6 +126,10 @@
} else {
frappe.contacts.clear_address_and_contact(frm);
}
+
+ if (frm.doc.opportunity_from && frm.doc.party_name) {
+ frm.trigger('set_contact_link');
+ }
},
set_contact_link: function(frm) {
@@ -137,6 +137,8 @@
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'}
+ } else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) {
+ frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'}
}
},
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index fba886c..dbfbcc9 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -356,7 +356,7 @@
scheduler_events = {
"cron": {
- "0/5 * * * *": [
+ "0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
index 158f143..ba05355 100644
--- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
+++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
@@ -64,8 +64,6 @@
"fieldtype": "Section Break"
},
{
- "fetch_from": "prevdoc_detail_docname.sales_person",
- "fetch_if_empty": 1,
"fieldname": "service_person",
"fieldtype": "Link",
"in_list_view": 1,
@@ -110,13 +108,15 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-27 17:47:21.474282",
+ "modified": "2023-02-27 11:09:33.114458",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit Purpose",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index c3f52d4..51f7b24 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -212,7 +212,7 @@
["name", "boms_updated", "status"],
)
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
- if not bom_batches or incomplete_level:
+ if not bom_batches or not incomplete_level:
continue
# Prep parent BOMs & updated processed BOMs for next level
@@ -252,6 +252,9 @@
current_boms = []
for row in bom_batches:
+ if not row.boms_updated:
+ continue
+
boms_updated = json.loads(row.boms_updated)
current_boms.extend(boms_updated)
boms_updated_dict = {bom: True for bom in boms_updated}
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 3133628..e82f379 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -561,7 +561,34 @@
)
def set_transferred_qty_in_job_card_item(self, ste_doc):
- from frappe.query_builder.functions import Sum
+ def _get_job_card_items_transferred_qty(ste_doc):
+ from frappe.query_builder.functions import Sum
+
+ job_card_items_transferred_qty = {}
+ job_card_items = [
+ x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")
+ ]
+
+ if job_card_items:
+ se = frappe.qb.DocType("Stock Entry")
+ sed = frappe.qb.DocType("Stock Entry Detail")
+
+ query = (
+ frappe.qb.from_(sed)
+ .join(se)
+ .on(sed.parent == se.name)
+ .select(sed.job_card_item, Sum(sed.qty))
+ .where(
+ (sed.job_card_item.isin(job_card_items))
+ & (se.docstatus == 1)
+ & (se.purpose == "Material Transfer for Manufacture")
+ )
+ .groupby(sed.job_card_item)
+ )
+
+ job_card_items_transferred_qty = frappe._dict(query.run(as_list=True))
+
+ return job_card_items_transferred_qty
def _validate_over_transfer(row, transferred_qty):
"Block over transfer of items if not allowed in settings."
@@ -578,29 +605,23 @@
exc=JobCardOverTransferError,
)
- for row in ste_doc.items:
- if not row.job_card_item:
- continue
+ job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
- sed = frappe.qb.DocType("Stock Entry Detail")
- se = frappe.qb.DocType("Stock Entry")
- transferred_qty = (
- frappe.qb.from_(sed)
- .join(se)
- .on(sed.parent == se.name)
- .select(Sum(sed.qty))
- .where(
- (sed.job_card_item == row.job_card_item)
- & (se.docstatus == 1)
- & (se.purpose == "Material Transfer for Manufacture")
- )
- ).run()[0][0]
-
+ if job_card_items_transferred_qty:
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
- if not allow_excess:
- _validate_over_transfer(row, transferred_qty)
- frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
+ for row in ste_doc.items:
+ if not row.job_card_item:
+ continue
+
+ transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item))
+
+ if not allow_excess:
+ _validate_over_transfer(row, transferred_qty)
+
+ frappe.db.set_value(
+ "Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)
+ )
def set_transferred_qty(self, update_status=False):
"Set total FG Qty in Job Card for which RM was transferred."
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 4aff42c..97480b2 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -506,7 +506,7 @@
callback: function(r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, {
- "required_qty": 1,
+ "required_qty": row.required_qty || 1,
"item_name": r.message.item_name,
"description": r.message.description,
"source_warehouse": r.message.default_warehouse,
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index cdf1541..3573a3a 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -4,7 +4,8 @@
import frappe
from frappe import _
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import Floor, Sum
+from frappe.utils import cint
from pypika.terms import ExistsCriterion
@@ -34,57 +35,55 @@
def get_bom_stock(filters):
- qty_to_produce = filters.get("qty_to_produce") or 1
- if int(qty_to_produce) < 0:
- frappe.throw(_("Quantity to Produce can not be less than Zero"))
+ qty_to_produce = filters.get("qty_to_produce")
+ if cint(qty_to_produce) <= 0:
+ frappe.throw(_("Quantity to Produce should be greater than zero."))
if filters.get("show_exploded_view"):
bom_item_table = "BOM Explosion Item"
else:
bom_item_table = "BOM Item"
- bin = frappe.qb.DocType("Bin")
- bom = frappe.qb.DocType("BOM")
- bom_item = frappe.qb.DocType(bom_item_table)
-
- query = (
- frappe.qb.from_(bom)
- .inner_join(bom_item)
- .on(bom.name == bom_item.parent)
- .left_join(bin)
- .on(bom_item.item_code == bin.item_code)
- .select(
- bom_item.item_code,
- bom_item.description,
- bom_item.stock_qty,
- bom_item.stock_uom,
- (bom_item.stock_qty / bom.quantity) * qty_to_produce,
- Sum(bin.actual_qty),
- Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
- )
- .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
- .groupby(bom_item.item_code)
+ warehouse_details = frappe.db.get_value(
+ "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
- if filters.get("warehouse"):
- warehouse_details = frappe.db.get_value(
- "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
- )
+ BOM = frappe.qb.DocType("BOM")
+ BOM_ITEM = frappe.qb.DocType(bom_item_table)
+ BIN = frappe.qb.DocType("Bin")
+ WH = frappe.qb.DocType("Warehouse")
+ CONDITIONS = ()
- if warehouse_details:
- wh = frappe.qb.DocType("Warehouse")
- query = query.where(
- ExistsCriterion(
- frappe.qb.from_(wh)
- .select(wh.name)
- .where(
- (wh.lft >= warehouse_details.lft)
- & (wh.rgt <= warehouse_details.rgt)
- & (bin.warehouse == wh.name)
- )
- )
+ if warehouse_details:
+ CONDITIONS = ExistsCriterion(
+ frappe.qb.from_(WH)
+ .select(WH.name)
+ .where(
+ (WH.lft >= warehouse_details.lft)
+ & (WH.rgt <= warehouse_details.rgt)
+ & (BIN.warehouse == WH.name)
)
- else:
- query = query.where(bin.warehouse == filters.get("warehouse"))
+ )
+ else:
+ CONDITIONS = BIN.warehouse == filters.get("warehouse")
- return query.run()
+ QUERY = (
+ frappe.qb.from_(BOM)
+ .inner_join(BOM_ITEM)
+ .on(BOM.name == BOM_ITEM.parent)
+ .left_join(BIN)
+ .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
+ .select(
+ BOM_ITEM.item_code,
+ BOM_ITEM.description,
+ BOM_ITEM.stock_qty,
+ BOM_ITEM.stock_uom,
+ BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
+ Sum(BIN.actual_qty).as_("actual_qty"),
+ Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
+ )
+ .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
+ .groupby(BOM_ITEM.item_code)
+ )
+
+ return QUERY.run()
diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
new file mode 100644
index 0000000..1c56ebe
--- /dev/null
+++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
@@ -0,0 +1,108 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+import frappe
+from frappe.exceptions import ValidationError
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import floor
+
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
+ get_bom_stock as bom_stock_report,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+
+class TestBomStockReport(FrappeTestCase):
+ def setUp(self):
+ self.warehouse = "_Test Warehouse - _TC"
+ self.fg_item, self.rm_items = create_items()
+ make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
+ make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
+ self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
+
+ def test_bom_stock_report(self):
+ # Test 1: When `qty_to_produce` is 0.
+ filters = frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": "Stores - _TC",
+ "qty_to_produce": 0,
+ }
+ )
+ self.assertRaises(ValidationError, bom_stock_report, filters)
+
+ # Test 2: When stock is not available.
+ data = bom_stock_report(
+ frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": "Stores - _TC",
+ "qty_to_produce": 1,
+ }
+ )
+ )
+ expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+ # Test 3: When stock is available.
+ data = bom_stock_report(
+ frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": self.warehouse,
+ "qty_to_produce": 1,
+ }
+ )
+ )
+ expected_data = get_expected_data(self.bom, self.warehouse, 1)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+
+def create_items():
+ fg_item = make_item(properties={"is_stock_item": 1}).name
+ rm_item1 = make_item(
+ properties={
+ "is_stock_item": 1,
+ "standard_rate": 100,
+ "opening_stock": 100,
+ "last_purchase_rate": 100,
+ }
+ ).name
+ rm_item2 = make_item(
+ properties={
+ "is_stock_item": 1,
+ "standard_rate": 200,
+ "opening_stock": 200,
+ "last_purchase_rate": 200,
+ }
+ ).name
+
+ return fg_item, [rm_item1, rm_item2]
+
+
+def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
+ expected_data = []
+
+ for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
+ in_stock_qty = frappe.get_cached_value(
+ "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
+ )
+
+ expected_data.append(
+ [
+ item.item_code,
+ item.description,
+ item.stock_qty,
+ item.stock_uom,
+ item.stock_qty * qty_to_produce / bom.quantity,
+ in_stock_qty,
+ floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
+ if in_stock_qty
+ else None,
+ ]
+ )
+
+ return expected_data
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
index 371ecbc..5c46bf3 100644
--- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -27,7 +27,13 @@
records = (
frappe.qb.from_(asset)
- .select(asset.name, asset.opening_accumulated_depreciation, asset.docstatus)
+ .select(
+ asset.name,
+ asset.opening_accumulated_depreciation,
+ asset.gross_purchase_amount,
+ asset.number_of_depreciations_booked,
+ asset.docstatus,
+ )
.where(asset.calculate_depreciation == 1)
.where(asset.docstatus < 2)
).run(as_dict=True)
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index a87c3ec..d1a55e6 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -91,6 +91,9 @@
}
_calculate_taxes_and_totals() {
+ const is_quotation = this.frm.doc.doctype == "Quotation";
+ this.frm.doc._items = is_quotation ? this.filtered_items() : this.frm.doc.items;
+
this.validate_conversion_rate();
this.calculate_item_values();
this.initialize_taxes();
@@ -122,7 +125,7 @@
calculate_item_values() {
var me = this;
if (!this.discount_amount_applied) {
- for (const item of this.frm.doc.items || []) {
+ for (const item of this.frm.doc._items || []) {
frappe.model.round_floats_in(item);
item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
@@ -131,8 +134,8 @@
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
}
else {
- let qty = item.qty || 1;
- qty = me.frm.doc.is_return ? -1 * qty : qty;
+ // allow for '0' qty on Credit/Debit notes
+ let qty = item.qty || -1
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}
@@ -206,7 +209,7 @@
});
if(has_inclusive_tax==false) return;
- $.each(me.frm.doc["items"] || [], function(n, item) {
+ $.each(me.frm.doc._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
var cumulated_tax_fraction = 0.0;
var total_inclusive_tax_amount_per_qty = 0;
@@ -277,7 +280,7 @@
var me = this;
this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0;
- $.each(this.frm.doc["items"] || [], function(i, item) {
+ $.each(this.frm.doc._items || [], function(i, item) {
me.frm.doc.total += item.amount;
me.frm.doc.total_qty += item.qty;
me.frm.doc.base_total += item.base_amount;
@@ -330,7 +333,7 @@
}
});
- $.each(this.frm.doc["items"] || [], function(n, item) {
+ $.each(this.frm.doc._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
// tax_amount represents the amount of tax for the current step
@@ -339,7 +342,7 @@
// Adjust divisional loss to the last item
if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] -= current_tax_amount;
- if (n == me.frm.doc["items"].length - 1) {
+ if (n == me.frm.doc._items.length - 1) {
current_tax_amount += actual_tax_dict[tax.idx];
}
}
@@ -376,7 +379,7 @@
}
// set precision in the last item iteration
- if (n == me.frm.doc["items"].length - 1) {
+ if (n == me.frm.doc._items.length - 1) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
@@ -599,10 +602,11 @@
_cleanup() {
this.frm.doc.base_in_words = this.frm.doc.in_words = "";
+ let items = this.frm.doc._items;
- if(this.frm.doc["items"] && this.frm.doc["items"].length) {
- if(!frappe.meta.get_docfield(this.frm.doc["items"][0].doctype, "item_tax_amount", this.frm.doctype)) {
- $.each(this.frm.doc["items"] || [], function(i, item) {
+ if(items && items.length) {
+ if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
+ $.each(items || [], function(i, item) {
delete item["item_tax_amount"];
});
}
@@ -655,7 +659,7 @@
var net_total = 0;
// calculate item amount after Discount Amount
if (total_for_discount_amount) {
- $.each(this.frm.doc["items"] || [], function(i, item) {
+ $.each(this.frm.doc._items || [], function(i, item) {
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
item.net_amount = flt(item.net_amount - distributed_amount,
precision("base_amount", item));
@@ -663,7 +667,7 @@
// discount amount rounding loss adjustment if no taxes
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
- && i == (me.frm.doc.items || []).length - 1) {
+ && i == (me.frm.doc._items || []).length - 1) {
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
- me.frm.doc.discount_amount, precision("net_total"));
item.net_amount = flt(item.net_amount + discount_amount_loss,
@@ -892,4 +896,8 @@
}
}
+
+ filtered_items() {
+ return this.frm.doc.items.filter(item => !item["is_alternative"]);
+ }
};
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 09f2c5d..8d69ea0 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -488,7 +488,7 @@
() => {
var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate);
- if (d.free_item_data) {
+ if (d.free_item_data && d.free_item_data.length > 0) {
me.apply_product_discount(d);
}
},
@@ -1884,11 +1884,13 @@
get_advances() {
if(!this.frm.is_return) {
+ var me = this;
return this.frm.call({
method: "set_advances",
doc: this.frm.doc,
callback: function(r, rt) {
refresh_field("advances");
+ me.frm.dirty();
}
})
}
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index b348bd3..81ef44d 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -90,7 +90,7 @@
|| frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
this.frm.add_custom_button(
__("Sales Order"),
- this.frm.cscript["Make Sales Order"],
+ () => this.make_sales_order(),
__("Create")
);
}
@@ -145,6 +145,20 @@
}
+ make_sales_order() {
+ var me = this;
+
+ let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative);
+ if (has_alternative_item) {
+ this.show_alternative_items_dialog();
+ } else {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
+ frm: me.frm
+ });
+ }
+ }
+
set_dynamic_field_label(){
if (this.frm.doc.quotation_to == "Customer")
{
@@ -220,17 +234,111 @@
}
})
}
+
+ show_alternative_items_dialog() {
+ let me = this;
+
+ const table_fields = [
+ {
+ fieldtype:"Data",
+ fieldname:"name",
+ label: __("Name"),
+ read_only: 1,
+ },
+ {
+ fieldtype:"Link",
+ fieldname:"item_code",
+ options: "Item",
+ label: __("Item Code"),
+ read_only: 1,
+ in_list_view: 1,
+ columns: 2,
+ formatter: (value, df, options, doc) => {
+ return doc.is_alternative ? `<span class="indicator yellow">${value}</span>` : value;
+ }
+ },
+ {
+ fieldtype:"Data",
+ fieldname:"description",
+ label: __("Description"),
+ in_list_view: 1,
+ read_only: 1,
+ },
+ {
+ fieldtype:"Currency",
+ fieldname:"amount",
+ label: __("Amount"),
+ options: "currency",
+ in_list_view: 1,
+ read_only: 1,
+ },
+ {
+ fieldtype:"Check",
+ fieldname:"is_alternative",
+ label: __("Is Alternative"),
+ read_only: 1,
+ }];
+
+
+ this.data = this.frm.doc.items.filter(
+ (item) => item.is_alternative || item.has_alternative_item
+ ).map((item) => {
+ return {
+ "name": item.name,
+ "item_code": item.item_code,
+ "description": item.description,
+ "amount": item.amount,
+ "is_alternative": item.is_alternative,
+ }
+ });
+
+ const dialog = new frappe.ui.Dialog({
+ title: __("Select Alternative Items for Sales Order"),
+ fields: [
+ {
+ fieldname: "info",
+ fieldtype: "HTML",
+ read_only: 1
+ },
+ {
+ fieldname: "alternative_items",
+ fieldtype: "Table",
+ cannot_add_rows: true,
+ in_place_edit: true,
+ reqd: 1,
+ data: this.data,
+ description: __("Select an item from each set to be used in the Sales Order."),
+ get_data: () => {
+ return this.data;
+ },
+ fields: table_fields
+ },
+ ],
+ primary_action: function() {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
+ frm: me.frm,
+ args: {
+ selected_items: dialog.fields_dict.alternative_items.grid.get_selected_children()
+ }
+ });
+ dialog.hide();
+ },
+ primary_action_label: __('Continue')
+ });
+
+ dialog.fields_dict.info.$wrapper.html(
+ `<p class="small text-muted">
+ <span class="indicator yellow"></span>
+ Alternative Items
+ </p>`
+ )
+ dialog.show();
+ }
};
cur_frm.script_manager.make(erpnext.selling.QuotationController);
-cur_frm.cscript['Make Sales Order'] = function() {
- frappe.model.open_mapped_doc({
- method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
- frm: cur_frm
- })
-}
-
frappe.ui.form.on("Quotation Item", "items_on_form_rendered", "packed_items_on_form_rendered", function(frm, cdt, cdn) {
// enable tax_amount field if Actual
})
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 063813b..fc66db2 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -35,6 +35,9 @@
make_packing_list(self)
+ def before_submit(self):
+ self.set_has_alternative_item()
+
def validate_valid_till(self):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date"))
@@ -59,7 +62,18 @@
title=_("Unpublished Item"),
)
+ def set_has_alternative_item(self):
+ """Mark 'Has Alternative Item' for rows."""
+ if not any(row.is_alternative for row in self.get("items")):
+ return
+
+ items_with_alternatives = self.get_rows_with_alternatives()
+ for row in self.get("items"):
+ if not row.is_alternative and row.name in items_with_alternatives:
+ row.has_alternative_item = 1
+
def get_ordered_status(self):
+ status = "Open"
ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
@@ -70,16 +84,40 @@
)
)
- status = "Open"
- if ordered_items:
+ if not ordered_items:
+ return status
+
+ has_alternatives = any(row.is_alternative for row in self.get("items"))
+ self._items = self.get_valid_items() if has_alternatives else self.get("items")
+
+ if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items):
+ status = "Partially Ordered"
+ else:
status = "Ordered"
- for item in self.get("items"):
- if item.qty > ordered_items.get(item.item_code, 0.0):
- status = "Partially Ordered"
-
return status
+ def get_valid_items(self):
+ """
+ Filters out items in an alternatives set that were not ordered.
+ """
+
+ def is_in_sales_order(row):
+ in_sales_order = bool(
+ frappe.db.exists(
+ "Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code, "docstatus": 1}
+ )
+ )
+ return in_sales_order
+
+ def can_map(row) -> bool:
+ if row.is_alternative or row.has_alternative_item:
+ return is_in_sales_order(row)
+
+ return True
+
+ return list(filter(can_map, self.get("items")))
+
def is_fully_ordered(self):
return self.get_ordered_status() == "Ordered"
@@ -176,6 +214,22 @@
def on_recurring(self, reference_doc, auto_repeat_doc):
self.valid_till = None
+ def get_rows_with_alternatives(self):
+ rows_with_alternatives = []
+ table_length = len(self.get("items"))
+
+ for idx, row in enumerate(self.get("items")):
+ if row.is_alternative:
+ continue
+
+ if idx == (table_length - 1):
+ break
+
+ if self.get("items")[idx + 1].is_alternative:
+ rows_with_alternatives.append(row.name)
+
+ return rows_with_alternatives
+
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
@@ -221,6 +275,8 @@
)
)
+ selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])]
+
def set_missing_values(source, target):
if customer:
target.customer = customer.name
@@ -244,6 +300,24 @@
target.blanket_order = obj.blanket_order
target.blanket_order_rate = obj.blanket_order_rate
+ def can_map_row(item) -> bool:
+ """
+ Row mapping from Quotation to Sales order:
+ 1. If no selections, map all non-alternative rows (that sum up to the grand total)
+ 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty
+ 3. If selections: Simple row: Map if adequate qty
+ """
+ has_qty = item.qty > 0
+
+ if not selected_rows:
+ return not item.is_alternative
+
+ if selected_rows and (item.is_alternative or item.has_alternative_item):
+ return (item.name in selected_rows) and has_qty
+
+ # Simple row
+ return has_qty
+
doclist = get_mapped_doc(
"Quotation",
source_name,
@@ -253,7 +327,7 @@
"doctype": "Sales Order Item",
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
"postprocess": update_item,
- "condition": lambda doc: doc.qty > 0,
+ "condition": can_map_row,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
@@ -322,7 +396,11 @@
source_name,
{
"Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}},
- "Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item},
+ "Quotation Item": {
+ "doctype": "Sales Invoice Item",
+ "postprocess": update_item,
+ "condition": lambda row: not row.is_alternative,
+ },
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
},
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index cdf5f5d..67f6518 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -457,6 +457,139 @@
expected_index = id + 1
self.assertEqual(item.idx, expected_index)
+ def test_alternative_items_with_stock_items(self):
+ """
+ Check if taxes & totals considers only non-alternative items with:
+ - One set of non-alternative & alternative items [first 3 rows]
+ - One simple stock item
+ """
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_list = []
+ stock_items = {
+ "_Test Simple Item 1": 100,
+ "_Test Alt 1": 120,
+ "_Test Alt 2": 110,
+ "_Test Simple Item 2": 200,
+ }
+
+ for item, rate in stock_items.items():
+ make_item(item, {"is_stock_item": 1})
+ item_list.append(
+ {
+ "item_code": item,
+ "qty": 1,
+ "rate": rate,
+ "is_alternative": bool("Alt" in item),
+ }
+ )
+
+ quotation = make_quotation(item_list=item_list, do_not_submit=1)
+ quotation.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 10,
+ },
+ )
+ quotation.submit()
+
+ self.assertEqual(quotation.net_total, 300)
+ self.assertEqual(quotation.grand_total, 330)
+
+ def test_alternative_items_with_service_items(self):
+ """
+ Check if taxes & totals considers only non-alternative items with:
+ - One set of non-alternative & alternative service items [first 3 rows]
+ - One simple non-alternative service item
+ All having the same item code and unique item name/description due to
+ dynamic services
+ """
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_list = []
+ service_items = {
+ "Tiling with Standard Tiles": 100,
+ "Alt Tiling with Durable Tiles": 150,
+ "Alt Tiling with Premium Tiles": 180,
+ "False Ceiling with Material #234": 190,
+ }
+
+ make_item("_Test Dynamic Service Item", {"is_stock_item": 0})
+
+ for name, rate in service_items.items():
+ item_list.append(
+ {
+ "item_code": "_Test Dynamic Service Item",
+ "item_name": name,
+ "description": name,
+ "qty": 1,
+ "rate": rate,
+ "is_alternative": bool("Alt" in name),
+ }
+ )
+
+ quotation = make_quotation(item_list=item_list, do_not_submit=1)
+ quotation.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 10,
+ },
+ )
+ quotation.submit()
+
+ self.assertEqual(quotation.net_total, 290)
+ self.assertEqual(quotation.grand_total, 319)
+
+ def test_alternative_items_sales_order_mapping_with_stock_items(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ frappe.flags.args = frappe._dict()
+ item_list = []
+ stock_items = {
+ "_Test Simple Item 1": 100,
+ "_Test Alt 1": 120,
+ "_Test Alt 2": 110,
+ "_Test Simple Item 2": 200,
+ }
+
+ for item, rate in stock_items.items():
+ make_item(item, {"is_stock_item": 1})
+ item_list.append(
+ {
+ "item_code": item,
+ "qty": 1,
+ "rate": rate,
+ "is_alternative": bool("Alt" in item),
+ "warehouse": "_Test Warehouse - _TC",
+ }
+ )
+
+ quotation = make_quotation(item_list=item_list)
+
+ frappe.flags.args.selected_items = [quotation.items[2]]
+ sales_order = make_sales_order(quotation.name)
+ sales_order.delivery_date = add_days(sales_order.transaction_date, 10)
+ sales_order.save()
+
+ self.assertEqual(sales_order.items[0].item_code, "_Test Alt 2")
+ self.assertEqual(sales_order.items[1].item_code, "_Test Simple Item 2")
+ self.assertEqual(sales_order.net_total, 310)
+
+ sales_order.submit()
+ quotation.reload()
+ self.assertEqual(quotation.status, "Ordered")
+
test_records = frappe.get_test_records("Quotation")
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index ca7dfd2..f2aabc5 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -49,6 +49,8 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "is_alternative",
+ "has_alternative_item",
"section_break_43",
"valuation_rate",
"column_break_45",
@@ -643,12 +645,28 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_alternative",
+ "fieldtype": "Check",
+ "label": "Is Alternative",
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "has_alternative_item",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Has Alternative Item",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-12-25 02:49:53.926625",
+ "modified": "2023-02-06 11:00:07.042364",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
@@ -656,5 +674,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index fb64772..449d461 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -275,7 +275,7 @@
if (this.frm.doc.docstatus===0) {
this.frm.add_custom_button(__('Quotation'),
function() {
- erpnext.utils.map_current_doc({
+ let d = erpnext.utils.map_current_doc({
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
source_doctype: "Quotation",
target: me.frm,
@@ -293,7 +293,16 @@
docstatus: 1,
status: ["!=", "Lost"]
}
- })
+ });
+
+ setTimeout(() => {
+ d.$parent.append(`
+ <span class='small text-muted'>
+ ${__("Note: Please create Sales Orders from individual Quotations to select from among Alternative Items.")}
+ </span>
+ `);
+ }, 200);
+
}, __("Get Items From"));
}
@@ -309,9 +318,12 @@
make_work_order() {
var me = this;
- this.frm.call({
- doc: this.frm.doc,
- method: 'get_work_order_items',
+ me.frm.call({
+ method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
+ args: {
+ sales_order: this.frm.docname,
+ },
+ freeze: true,
callback: function(r) {
if(!r.message) {
frappe.msgprint({
@@ -321,14 +333,7 @@
});
return;
}
- else if(!r.message) {
- frappe.msgprint({
- title: __('Work Order not created'),
- message: __('Work Order already created for all items with BOM'),
- indicator: 'orange'
- });
- return;
- } else {
+ else {
const fields = [{
label: 'Items',
fieldtype: 'Table',
@@ -429,9 +434,9 @@
make_raw_material_request() {
var me = this;
this.frm.call({
- doc: this.frm.doc,
- method: 'get_work_order_items',
+ method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
args: {
+ sales_order: this.frm.docname,
for_raw_material_request: 1
},
callback: function(r) {
@@ -450,6 +455,7 @@
}
make_raw_material_request_dialog(r) {
+ var me = this;
var fields = [
{fieldtype:'Check', fieldname:'include_exploded_items',
label: __('Include Exploded Items')},
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index ca6a51a..385d0f3 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -6,11 +6,12 @@
import frappe
import frappe.utils
-from frappe import _
+from frappe import _, qb
from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
+from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
@@ -414,51 +415,6 @@
self.indicator_color = "green"
self.indicator_title = _("Paid")
- @frappe.whitelist()
- def get_work_order_items(self, for_raw_material_request=0):
- """Returns items with BOM that already do not have a linked work order"""
- items = []
- item_codes = [i.item_code for i in self.items]
- product_bundle_parents = [
- pb.new_item_code
- for pb in frappe.get_all(
- "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
- )
- ]
-
- for table in [self.items, self.packed_items]:
- for i in table:
- bom = get_default_bom(i.item_code)
- stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
-
- if not for_raw_material_request:
- total_work_order_qty = flt(
- frappe.db.sql(
- """select sum(qty) from `tabWork Order`
- where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""",
- (i.item_code, self.name, i.name),
- )[0][0]
- )
- pending_qty = stock_qty - total_work_order_qty
- else:
- pending_qty = stock_qty
-
- if pending_qty and i.item_code not in product_bundle_parents:
- items.append(
- dict(
- name=i.name,
- item_code=i.item_code,
- description=i.description,
- bom=bom or "",
- warehouse=i.warehouse,
- pending_qty=pending_qty,
- required_qty=pending_qty if for_raw_material_request else 0,
- sales_order_item=i.name,
- )
- )
-
- return items
-
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
@@ -1350,3 +1306,57 @@
return
frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty)
+
+
+@frappe.whitelist()
+def get_work_order_items(sales_order, for_raw_material_request=0):
+ """Returns items with BOM that already do not have a linked work order"""
+ if sales_order:
+ so = frappe.get_doc("Sales Order", sales_order)
+
+ wo = qb.DocType("Work Order")
+
+ items = []
+ item_codes = [i.item_code for i in so.items]
+ product_bundle_parents = [
+ pb.new_item_code
+ for pb in frappe.get_all(
+ "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
+ )
+ ]
+
+ for table in [so.items, so.packed_items]:
+ for i in table:
+ bom = get_default_bom(i.item_code)
+ stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
+
+ if not for_raw_material_request:
+ total_work_order_qty = flt(
+ qb.from_(wo)
+ .select(Sum(wo.qty))
+ .where(
+ (wo.production_item == i.item_code)
+ & (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
+ & (wo.docstatus.lte(2))
+ )
+ .run()[0][0]
+ )
+ pending_qty = stock_qty - total_work_order_qty
+ else:
+ pending_qty = stock_qty
+
+ if pending_qty and i.item_code not in product_bundle_parents:
+ items.append(
+ dict(
+ name=i.name,
+ item_code=i.item_code,
+ description=i.description,
+ bom=bom or "",
+ warehouse=i.warehouse,
+ pending_qty=pending_qty,
+ required_qty=pending_qty if for_raw_material_request else 0,
+ sales_order_item=i.name,
+ )
+ )
+
+ return items
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index d4d7c58..627914f 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1217,6 +1217,8 @@
self.assertTrue(si.get("payment_schedule"))
def test_make_work_order(self):
+ from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
+
# Make a new Sales Order
so = make_sales_order(
**{
@@ -1230,7 +1232,7 @@
# Raise Work Orders
po_items = []
so_item_name = {}
- for item in so.get_work_order_items():
+ for item in get_work_order_items(so.name):
po_items.append(
{
"warehouse": item.get("warehouse"),
@@ -1448,6 +1450,7 @@
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+ from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
make_item( # template item
"Test-WO-Tshirt",
@@ -1487,7 +1490,7 @@
]
}
)
- wo_items = so.get_work_order_items()
+ wo_items = get_work_order_items(so.name)
self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
@@ -1497,6 +1500,8 @@
self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self):
+ from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
+
item = make_item(
"_Test Finished Item",
{
@@ -1529,7 +1534,7 @@
so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]})
so.submit()
mr_dict = frappe._dict()
- items = so.get_work_order_items(1)
+ items = get_work_order_items(so.name, 1)
mr_dict["items"] = items
mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index c442774..46320e5 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -522,7 +522,7 @@
const from_selector = field === 'qty' && value === "+1";
if (from_selector)
- value = flt(item_row.qty) + flt(value);
+ value = flt(item_row.stock_qty) + flt(value);
if (item_row_exists) {
if (field === 'qty')
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 5ce6e9c..f1df3a1 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -253,7 +253,7 @@
}
calculate_commission() {
- if(!this.frm.fields_dict.commission_rate) return;
+ if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
if(this.frm.doc.commission_rate > 100) {
this.frm.set_value("commission_rate", 100);
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 5bcb05a..9a9ddf4 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -33,6 +33,9 @@
'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items");
},
+ 'Stock Entry': () => {
+ open_form(frm, "Stock Entry", "Stock Entry Detail", "items");
+ },
};
},
@@ -893,6 +896,9 @@
new_child_doc.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description;
+ if (!new_child_doc.qty) {
+ new_child_doc.qty = 1.0;
+ }
frappe.run_serially([
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),
diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py
index fb1a28d..0c24d3c 100644
--- a/erpnext/stock/doctype/item_alternative/item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/item_alternative.py
@@ -54,7 +54,7 @@
if not item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code))
if self.two_way and not alternative_item_data.allow_alternative_item:
- frappe.throw(alternate_item_check_msg.format(self.item_code))
+ frappe.throw(alternate_item_check_msg.format(self.alternative_item_code))
def validate_duplicate(self):
if frappe.db.get_value(
diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js
index 12cf6cf..ce489ff 100644
--- a/erpnext/stock/doctype/item_price/item_price.js
+++ b/erpnext/stock/doctype/item_price/item_price.js
@@ -2,7 +2,18 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Item Price", {
- onload: function (frm) {
+ setup(frm) {
+ frm.set_query("item_code", function() {
+ return {
+ filters: {
+ "disabled": 0,
+ "has_variants": 0
+ }
+ };
+ });
+ },
+
+ onload(frm) {
// Fetch price list details
frm.add_fetch("price_list", "buying", "buying");
frm.add_fetch("price_list", "selling", "selling");
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index bcd31ad..54d1ae6 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Cast_
@@ -21,6 +21,7 @@
self.update_price_list_details()
self.update_item_details()
self.check_duplicates()
+ self.validate_item_template()
def validate_item(self):
if not frappe.db.exists("Item", self.item_code):
@@ -49,6 +50,12 @@
"Item", self.item_code, ["item_name", "description"]
)
+ def validate_item_template(self):
+ if frappe.get_cached_value("Item", self.item_code, "has_variants"):
+ msg = f"Item Price cannot be created for the template item {bold(self.item_code)}"
+
+ frappe.throw(_(msg))
+
def check_duplicates(self):
item_price = frappe.qb.DocType("Item Price")
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index 30d933e..8fd4938 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -16,6 +16,28 @@
frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True)
+ def test_template_item_price(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item = make_item(
+ "Test Template Item 1",
+ {
+ "has_variants": 1,
+ "variant_based_on": "Manufacturer",
+ },
+ )
+
+ doc = frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "price_list": "_Test Price List",
+ "item_code": item.name,
+ "price_list_rate": 100,
+ }
+ )
+
+ self.assertRaises(frappe.ValidationError, doc.save)
+
def test_duplicate_item(self):
doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.save)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 6426fe8..8aeb751 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -10,6 +10,7 @@
import frappe
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
@@ -180,6 +181,34 @@
self.update_requested_qty()
self.update_requested_qty_in_production_plan()
+ def get_mr_items_ordered_qty(self, mr_items):
+ mr_items_ordered_qty = {}
+ mr_items = [d.name for d in self.get("items") if d.name in mr_items]
+
+ doctype = qty_field = None
+ if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
+ doctype = frappe.qb.DocType("Stock Entry Detail")
+ qty_field = doctype.transfer_qty
+ elif self.material_request_type == "Manufacture":
+ doctype = frappe.qb.DocType("Work Order")
+ qty_field = doctype.qty
+
+ if doctype and qty_field:
+ query = (
+ frappe.qb.from_(doctype)
+ .select(doctype.material_request_item, Sum(qty_field))
+ .where(
+ (doctype.material_request == self.name)
+ & (doctype.material_request_item.isin(mr_items))
+ & (doctype.docstatus == 1)
+ )
+ .groupby(doctype.material_request_item)
+ )
+
+ mr_items_ordered_qty = frappe._dict(query.run())
+
+ return mr_items_ordered_qty
+
def update_completed_qty(self, mr_items=None, update_modified=True):
if self.material_request_type == "Purchase":
return
@@ -187,18 +216,13 @@
if not mr_items:
mr_items = [d.name for d in self.get("items")]
+ mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items)
+ mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
+
for d in self.get("items"):
if d.name in mr_items:
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
- d.ordered_qty = flt(
- frappe.db.sql(
- """select sum(transfer_qty)
- from `tabStock Entry Detail` where material_request = %s
- and material_request_item = %s and docstatus = 1""",
- (self.name, d.name),
- )[0][0]
- )
- mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
+ d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
if mr_qty_allowance:
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
@@ -217,14 +241,7 @@
)
elif self.material_request_type == "Manufacture":
- d.ordered_qty = flt(
- frappe.db.sql(
- """select sum(qty)
- from `tabWork Order` where material_request = %s
- and material_request_item = %s and docstatus = 1""",
- (self.name, d.name),
- )[0][0]
- )
+ d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
@@ -587,6 +604,9 @@
def set_missing_values(source, target):
target.purpose = source.material_request_type
+ target.from_warehouse = source.set_from_warehouse
+ target.to_warehouse = source.set_warehouse
+
if source.job_card:
target.purpose = "Material Transfer for Manufacture"
@@ -722,6 +742,7 @@
def make_in_transit_stock_entry(source_name, in_transit_warehouse):
ste_doc = make_stock_entry(source_name)
ste_doc.add_to_transit = 1
+ ste_doc.to_warehouse = in_transit_warehouse
for row in ste_doc.items:
row.t_warehouse = in_transit_warehouse
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index bb318f7..c1abd31 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -293,6 +293,7 @@
get_purchase_document_details,
)
+ stock_rbnb = None
if erpnext.is_perpetual_inventory_enabled(self.company):
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
@@ -450,6 +451,21 @@
item=d,
)
+ if d.rate_difference_with_purchase_invoice and stock_rbnb:
+ account_currency = get_account_currency(stock_rbnb)
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=stock_rbnb,
+ cost_center=d.cost_center,
+ debit=0.0,
+ credit=flt(d.rate_difference_with_purchase_invoice),
+ remarks=_("Adjustment based on Purchase Invoice rate"),
+ against_account=warehouse_account_name,
+ account_currency=account_currency,
+ project=d.project,
+ item=d,
+ )
+
# sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
self.add_gl_entry(
@@ -470,10 +486,11 @@
+ flt(d.landed_cost_voucher_amount)
+ flt(d.rm_supp_cost)
+ flt(d.item_tax_amount)
+ + flt(d.rate_difference_with_purchase_invoice)
)
divisional_loss = flt(
- valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount")
+ valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount")
)
if divisional_loss:
@@ -765,7 +782,7 @@
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
for pr in set(updated_pr):
- pr_doc = self if (pr == self.name) else frappe.get_cached_doc("Purchase Receipt", pr)
+ pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
self.load_from_db()
@@ -881,7 +898,7 @@
return {d.po_detail: flt(d.billed_amt) for d in query}
-def update_billing_percentage(pr_doc, update_modified=True):
+def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
# Reload as billed amount was set in db directly
pr_doc.load_from_db()
@@ -897,6 +914,12 @@
total_amount += total_billable_amount
total_billed_amount += flt(item.billed_amt)
+ if adjust_incoming_rate:
+ adjusted_amt = 0.0
+ if item.billed_amt and item.amount:
+ adjusted_amt = flt(item.billed_amt) - flt(item.amount)
+
+ item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)
@@ -906,6 +929,26 @@
pr_doc.set_status(update=True)
pr_doc.notify_update()
+ if adjust_incoming_rate:
+ adjust_incoming_rate_for_pr(pr_doc)
+
+
+def adjust_incoming_rate_for_pr(doc):
+ doc.update_valuation_rate(reset_outgoing_rate=False)
+
+ for item in doc.get("items"):
+ item.db_update()
+
+ doc.docstatus = 2
+ doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
+ doc.make_gl_entries_on_cancel()
+
+ # update stock & gl entries for submit state of PR
+ doc.docstatus = 1
+ doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
+ doc.make_gl_entries()
+ doc.repost_future_sle_and_gle()
+
def get_item_wise_returned_qty(pr_doc):
items = [d.name for d in pr_doc.items]
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 7a350b9..cd320fd 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -69,6 +69,7 @@
"item_tax_amount",
"rm_supp_cost",
"landed_cost_voucher_amount",
+ "rate_difference_with_purchase_invoice",
"billed_amt",
"warehouse_and_reference",
"warehouse",
@@ -1007,12 +1008,20 @@
"fieldtype": "Check",
"label": "Has Item Scanned",
"read_only": 1
+ },
+ {
+ "fieldname": "rate_difference_with_purchase_invoice",
+ "fieldtype": "Currency",
+ "label": "Rate Difference with Purchase Invoice",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-01-18 15:48:58.114923",
+ "modified": "2023-02-28 15:43:04.470104",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 2a9f091..9673c81 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import cint, cstr, flt
+from frappe.utils import cint, cstr, flt, get_number_format_info
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import (
get_template_details,
@@ -156,7 +156,9 @@
for i in range(1, 11):
reading_value = reading.get("reading_" + str(i))
if reading_value is not None and reading_value.strip():
- result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
+ result = (
+ flt(reading.get("min_value")) <= parse_float(reading_value) <= flt(reading.get("max_value"))
+ )
if not result:
return False
return True
@@ -196,7 +198,7 @@
# numeric readings
for i in range(1, 11):
field = "reading_" + str(i)
- data[field] = flt(reading.get(field))
+ data[field] = parse_float(reading.get(field))
data["mean"] = self.calculate_mean(reading)
return data
@@ -210,7 +212,7 @@
for i in range(1, 11):
reading_value = reading.get("reading_" + str(i))
if reading_value is not None and reading_value.strip():
- readings_list.append(flt(reading_value))
+ readings_list.append(parse_float(reading_value))
actual_mean = mean(readings_list) if readings_list else 0
return actual_mean
@@ -324,3 +326,19 @@
)
return doc
+
+
+def parse_float(num: str) -> float:
+ """Since reading_# fields are `Data` field they might contain number which
+ is representation in user's prefered number format instead of machine
+ readable format. This function converts them to machine readable format."""
+
+ number_format = frappe.db.get_default("number_format") or "#,###.##"
+ decimal_str, comma_str, _number_format_precision = get_number_format_info(number_format)
+
+ if decimal_str == "," and comma_str == ".":
+ num = num.replace(",", "#$")
+ num = num.replace(".", ",")
+ num = num.replace("#$", ".")
+
+ return flt(num)
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 4f19643..9d2e139 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -2,7 +2,7 @@
# See license.txt
import frappe
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate
from erpnext.controllers.stock_controller import (
@@ -216,6 +216,40 @@
qa.save()
self.assertEqual(qa.status, "Accepted")
+ @change_settings("System Settings", {"number_format": "#.###,##"})
+ def test_diff_number_format(self):
+ self.assertEqual(frappe.db.get_default("number_format"), "#.###,##") # sanity check
+
+ # Test QI based on acceptance values (Non formula)
+ dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+ readings = [
+ {
+ "specification": "Iron Content", # numeric reading
+ "min_value": 60,
+ "max_value": 100,
+ "reading_1": "70,000",
+ },
+ {
+ "specification": "Iron Content", # numeric reading
+ "min_value": 60,
+ "max_value": 100,
+ "reading_1": "1.100,00",
+ },
+ ]
+
+ qa = create_quality_inspection(
+ reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True
+ )
+
+ qa.save()
+
+ # status must be auto set as per formula
+ self.assertEqual(qa.readings[0].status, "Accepted")
+ self.assertEqual(qa.readings[1].status, "Rejected")
+
+ qa.delete()
+ dn.delete()
+
def create_quality_inspection(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 398b3c9..3f6a2c8 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -397,6 +397,7 @@
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
+ "actual_qty": 0,
"company": self.company,
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"is_cancelled": 1 if self.docstatus == 2 else 0,
@@ -423,6 +424,8 @@
data.valuation_rate = flt(row.valuation_rate)
data.stock_value_difference = -1 * flt(row.amount_difference)
+ self.update_inventory_dimensions(row, data)
+
return data
def make_sle_on_cancel(self):
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index b53f429..489ec6e 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -8,6 +8,7 @@
from frappe import _, throw
from frappe.model import child_table_fields, default_fields
from frappe.model.meta import get_field_precision
+from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
from erpnext import get_company_currency
@@ -526,12 +527,8 @@
itemwise_barcode = {}
for item in items_list:
- barcodes = frappe.db.sql(
- """
- select barcode from `tabItem Barcode` where parent = %s
- """,
- item.item_code,
- as_dict=1,
+ barcodes = frappe.db.get_all(
+ "Item Barcode", filters={"parent": item.item_code}, fields="barcode"
)
for barcode in barcodes:
@@ -891,34 +888,36 @@
:param item_code: str, Item Doctype field item_code
"""
- args["item_code"] = item_code
-
- conditions = """where item_code=%(item_code)s
- and price_list=%(price_list)s
- and ifnull(uom, '') in ('', %(uom)s)"""
-
- conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)"
+ ip = frappe.qb.DocType("Item Price")
+ query = (
+ frappe.qb.from_(ip)
+ .select(ip.name, ip.price_list_rate, ip.uom)
+ .where(
+ (ip.item_code == item_code)
+ & (ip.price_list == args.get("price_list"))
+ & (IfNull(ip.uom, "").isin(["", args.get("uom")]))
+ & (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")]))
+ )
+ .orderby(ip.valid_from, order=frappe.qb.desc)
+ .orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc)
+ .orderby(ip.uom, order=frappe.qb.desc)
+ )
if not ignore_party:
if args.get("customer"):
- conditions += " and customer=%(customer)s"
+ query = query.where(ip.customer == args.get("customer"))
elif args.get("supplier"):
- conditions += " and supplier=%(supplier)s"
+ query = query.where(ip.supplier == args.get("supplier"))
else:
- conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')"
+ query = query.where((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == ""))
if args.get("transaction_date"):
- conditions += """ and %(transaction_date)s between
- ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')"""
+ query = query.where(
+ (IfNull(ip.valid_from, "2000-01-01") <= args["transaction_date"])
+ & (IfNull(ip.valid_upto, "2500-12-31") >= args["transaction_date"])
+ )
- return frappe.db.sql(
- """ select name, price_list_rate, uom
- from `tabItem Price` {conditions}
- order by valid_from desc, ifnull(batch_no, '') desc, uom desc """.format(
- conditions=conditions
- ),
- args,
- )
+ return query.run()
def get_price_list_rate_for(args, item_code):
@@ -1091,91 +1090,68 @@
if not user:
user = frappe.session["user"]
- condition = "pfu.user = %(user)s AND pfu.default=1"
- if user and company:
- condition = "pfu.user = %(user)s AND pf.company = %(company)s AND pfu.default=1"
+ pf = frappe.qb.DocType("POS Profile")
+ pfu = frappe.qb.DocType("POS Profile User")
- pos_profile = frappe.db.sql(
- """SELECT pf.*
- FROM
- `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu
- ON
- pf.name = pfu.parent
- WHERE
- {cond} AND pf.disabled = 0
- """.format(
- cond=condition
- ),
- {"user": user, "company": company},
- as_dict=1,
+ query = (
+ frappe.qb.from_(pf)
+ .left_join(pfu)
+ .on(pf.name == pfu.parent)
+ .select(pf.star)
+ .where((pfu.user == user) & (pfu.default == 1))
)
+ if company:
+ query = query.where(pf.company == company)
+
+ pos_profile = query.run(as_dict=True)
+
if not pos_profile and company:
- pos_profile = frappe.db.sql(
- """SELECT pf.*
- FROM
- `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu
- ON
- pf.name = pfu.parent
- WHERE
- pf.company = %(company)s AND pf.disabled = 0
- """,
- {"company": company},
- as_dict=1,
- )
+ pos_profile = (
+ frappe.qb.from_(pf)
+ .left_join(pfu)
+ .on(pf.name == pfu.parent)
+ .select(pf.star)
+ .where((pf.company == company) & (pf.disabled == 0))
+ ).run(as_dict=True)
return pos_profile and pos_profile[0] or None
def get_serial_nos_by_fifo(args, sales_order=None):
if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"):
- return "\n".join(
- frappe.db.sql_list(
- """select name from `tabSerial No`
- where item_code=%(item_code)s and warehouse=%(warehouse)s and
- sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s)
- order by timestamp(purchase_date, purchase_time)
- asc limit %(qty)s""",
- {
- "item_code": args.item_code,
- "warehouse": args.warehouse,
- "qty": abs(cint(args.stock_qty)),
- "sales_order": sales_order,
- },
- )
+ sn = frappe.qb.DocType("Serial No")
+ query = (
+ frappe.qb.from_(sn)
+ .select(sn.name)
+ .where((sn.item_code == args.item_code) & (sn.warehouse == args.warehouse))
+ .orderby(CombineDatetime(sn.purchase_date, sn.purchase_time))
+ .limit(abs(cint(args.stock_qty)))
)
+ if sales_order:
+ query = query.where(sn.sales_order == sales_order)
+ if args.batch_no:
+ query = query.where(sn.batch_no == args.batch_no)
-def get_serial_no_batchwise(args, sales_order=None):
- if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"):
- return "\n".join(
- frappe.db.sql_list(
- """select name from `tabSerial No`
- where item_code=%(item_code)s and warehouse=%(warehouse)s and
- sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s)
- and batch_no=IF(%(batch_no)s IS NULL, batch_no, %(batch_no)s) order
- by timestamp(purchase_date, purchase_time) asc limit %(qty)s""",
- {
- "item_code": args.item_code,
- "warehouse": args.warehouse,
- "batch_no": args.batch_no,
- "qty": abs(cint(args.stock_qty)),
- "sales_order": sales_order,
- },
- )
- )
+ serial_nos = query.run(as_list=True)
+ serial_nos = [s[0] for s in serial_nos]
+
+ return "\n".join(serial_nos)
@frappe.whitelist()
def get_conversion_factor(item_code, uom):
variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True)
filters = {"parent": item_code, "uom": uom}
+
if variant_of:
filters["parent"] = ("in", (item_code, variant_of))
conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor")
if not conversion_factor:
stock_uom = frappe.db.get_value("Item", item_code, "stock_uom")
conversion_factor = get_uom_conv_factor(uom, stock_uom)
+
return {"conversion_factor": conversion_factor or 1.0}
@@ -1217,12 +1193,16 @@
def get_company_total_stock(item_code, company):
- return frappe.db.sql(
- """SELECT sum(actual_qty) from
- (`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name)
- WHERE `tabWarehouse`.company = %s and `tabBin`.item_code = %s""",
- (company, item_code),
- )[0][0]
+ bin = frappe.qb.DocType("Bin")
+ wh = frappe.qb.DocType("Warehouse")
+
+ return (
+ frappe.qb.from_(bin)
+ .inner_join(wh)
+ .on(bin.warehouse == wh.name)
+ .select(Sum(bin.actual_qty))
+ .where((wh.company == company) & (bin.item_code == item_code))
+ ).run()[0][0]
@frappe.whitelist()
@@ -1231,6 +1211,7 @@
{"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no}
)
serial_no = get_serial_no(args)
+
return {"serial_no": serial_no}
@@ -1250,6 +1231,7 @@
bin_details_and_serial_nos.update(
get_serial_no_details(item_code, warehouse, stock_qty, serial_no)
)
+
return bin_details_and_serial_nos
@@ -1264,6 +1246,7 @@
)
serial_no = get_serial_no(args)
batch_qty_and_serial_no.update({"serial_no": serial_no})
+
return batch_qty_and_serial_no
@@ -1336,7 +1319,6 @@
def apply_price_list_on_item(args):
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1)
item_details = get_price_list_rate(args, item_doc)
-
item_details.update(get_pricing_rule_for_item(args))
return item_details
@@ -1420,12 +1402,12 @@
) or {"valuation_rate": 0}
elif not item.get("is_stock_item"):
- valuation_rate = frappe.db.sql(
- """select sum(base_net_amount) / sum(qty*conversion_factor)
- from `tabPurchase Invoice Item`
- where item_code = %s and docstatus=1""",
- item_code,
- )
+ pi_item = frappe.qb.DocType("Purchase Invoice Item")
+ valuation_rate = (
+ frappe.qb.from_(pi_item)
+ .select((Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor)))
+ .where((pi_item.docstatus == 1) & (pi_item.item_code == item_code))
+ ).run()
if valuation_rate:
return {"valuation_rate": valuation_rate[0][0] or 0.0}
@@ -1451,7 +1433,7 @@
if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"):
has_serial_no = frappe.get_value("Item", {"item_code": args.item_code}, "has_serial_no")
if args.get("batch_no") and has_serial_no == 1:
- return get_serial_no_batchwise(args, sales_order)
+ return get_serial_nos_by_fifo(args, sales_order)
elif has_serial_no == 1:
args = json.dumps(
{
@@ -1483,31 +1465,35 @@
args = frappe._dict(json.loads(args))
blanket_order_details = None
- condition = ""
- if args.item_code:
- if args.customer and args.doctype == "Sales Order":
- condition = " and bo.customer=%(customer)s"
- elif args.supplier and args.doctype == "Purchase Order":
- condition = " and bo.supplier=%(supplier)s"
- if args.blanket_order:
- condition += " and bo.name =%(blanket_order)s"
- if args.transaction_date:
- condition += " and bo.to_date>=%(transaction_date)s"
- blanket_order_details = frappe.db.sql(
- """
- select boi.rate as blanket_order_rate, bo.name as blanket_order
- from `tabBlanket Order` bo, `tabBlanket Order Item` boi
- where bo.company=%(company)s and boi.item_code=%(item_code)s
- and bo.docstatus=1 and bo.name = boi.parent {0}
- """.format(
- condition
- ),
- args,
- as_dict=True,
+ if args.item_code:
+ bo = frappe.qb.DocType("Blanket Order")
+ bo_item = frappe.qb.DocType("Blanket Order Item")
+
+ query = (
+ frappe.qb.from_(bo)
+ .from_(bo_item)
+ .select(bo_item.rate.as_("blanket_order_rate"), bo.name.as_("blanket_order"))
+ .where(
+ (bo.company == args.company)
+ & (bo_item.item_code == args.item_code)
+ & (bo.docstatus == 1)
+ & (bo.name == bo_item.parent)
+ )
)
+ if args.customer and args.doctype == "Sales Order":
+ query = query.where(bo.customer == args.customer)
+ elif args.supplier and args.doctype == "Purchase Order":
+ query = query.where(bo.supplier == args.supplier)
+ if args.blanket_order:
+ query = query.where(bo.name == args.blanket_order)
+ if args.transaction_date:
+ query = query.where(bo.to_date >= args.transaction_date)
+
+ blanket_order_details = query.run(as_dict=True)
blanket_order_details = blanket_order_details[0] if blanket_order_details else ""
+
return blanket_order_details
@@ -1517,10 +1503,10 @@
if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")):
reserved_so = args.get("against_sales_order")
elif args.get("against_sales_invoice"):
- sales_order = frappe.db.sql(
- """select sales_order from `tabSales Invoice Item` where
- parent=%s and item_code=%s""",
- (args.get("against_sales_invoice"), args.get("item_code")),
+ sales_order = frappe.db.get_all(
+ "Sales Invoice Item",
+ filters={"parent": args.get("against_sales_invoice"), "item_code": args.get("item_code")},
+ fields="sales_order",
)
if sales_order and sales_order[0]:
if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")):
@@ -1532,13 +1518,14 @@
def get_reserved_qty_for_so(sales_order, item_code):
- reserved_qty = frappe.db.sql(
- """select sum(qty) from `tabSales Order Item`
- where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1
- """,
- (sales_order, item_code),
+ reserved_qty = frappe.db.get_value(
+ "Sales Order Item",
+ filters={
+ "parent": sales_order,
+ "item_code": item_code,
+ "ensure_delivery_based_on_produced_serial_no": 1,
+ },
+ fieldname="sum(qty)",
)
- if reserved_qty and reserved_qty[0][0]:
- return reserved_qty[0][0]
- else:
- return 0
+
+ return reserved_qty or 0
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index f4fd4de..95dbc83 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -191,14 +191,17 @@
def validate_available_qty_for_consumption(self):
for item in self.get("supplied_items"):
+ precision = item.precision("consumed_qty")
if (
- item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty
+ item.available_qty_for_consumption
+ and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
):
- frappe.throw(
- _(
- "Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table."
- ).format(item.idx)
- )
+ msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)}
+ must be less than or equal to Available Qty For Consumption
+ {flt(item.available_qty_for_consumption, precision)}
+ in Consumed Items Table."""
+
+ frappe.throw(_(msg))
def validate_items_qty(self):
for item in self.items:
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index 7f4e9ef..2a078c4 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -13,8 +13,8 @@
get_datetime,
get_datetime_str,
get_link_to_form,
+ get_system_timezone,
get_time,
- get_time_zone,
get_weekdays,
getdate,
nowdate,
@@ -981,7 +981,7 @@
def get_tz(user):
- return frappe.db.get_value("User", user, "time_zone") or get_time_zone()
+ return frappe.db.get_value("User", user, "time_zone") or get_system_timezone()
@frappe.whitelist()
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 5a0a863..bec3ce2 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -9916,3 +9916,5 @@
Delivered at Place,Geliefert benannter Ort,
Delivered at Place Unloaded,Geliefert benannter Ort entladen,
Delivered Duty Paid,Geliefert verzollt,
+Discount Validity,Frist für den Rabatt,
+Discount Validity Based On,Frist für den Rabatt berechnet sich nach,
diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py
index 13b7877..62033a5 100644
--- a/erpnext/utilities/doctype/video/video.py
+++ b/erpnext/utilities/doctype/video/video.py
@@ -10,6 +10,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
+from frappe.utils.data import get_system_timezone
from pyyoutube import Api
@@ -64,7 +65,7 @@
frequency = get_frequency(frequency)
time = datetime.now()
- timezone = pytz.timezone(frappe.utils.get_time_zone())
+ timezone = pytz.timezone(get_system_timezone())
site_time = time.astimezone(timezone)
if frequency == 30:
diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py
index dfca946..f50c207 100644
--- a/erpnext/www/book_appointment/index.py
+++ b/erpnext/www/book_appointment/index.py
@@ -4,6 +4,7 @@
import frappe
import pytz
from frappe import _
+from frappe.utils.data import get_system_timezone
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
@@ -125,7 +126,7 @@
def convert_to_guest_timezone(guest_tz, datetimeobject):
guest_tz = pytz.timezone(guest_tz)
- local_timezone = pytz.timezone(frappe.utils.get_time_zone())
+ local_timezone = pytz.timezone(get_system_timezone())
datetimeobject = local_timezone.localize(datetimeobject)
datetimeobject = datetimeobject.astimezone(guest_tz)
return datetimeobject
@@ -134,7 +135,7 @@
def convert_to_system_timezone(guest_tz, datetimeobject):
guest_tz = pytz.timezone(guest_tz)
datetimeobject = guest_tz.localize(datetimeobject)
- system_tz = pytz.timezone(frappe.utils.get_time_zone())
+ system_tz = pytz.timezone(get_system_timezone())
datetimeobject = datetimeobject.astimezone(system_tz)
return datetimeobject
diff --git a/pyproject.toml b/pyproject.toml
index 1c93eed..0718e5b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,9 +28,6 @@
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
-[tool.bench.dev-dependencies]
-hypothesis = "~=6.31.0"
-
[tool.black]
line-length = 99