Merge branch 'develop' into fix_flaky_test_in_payment_terms_report

This commit is contained in:
ruthra kumar 2023-03-11 13:05:38 +05:30 committed by GitHub
commit a43304b01b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1324 additions and 421 deletions

View File

@ -29,6 +29,7 @@ def create_charts(
"root_type", "root_type",
"is_group", "is_group",
"tax_rate", "tax_rate",
"account_currency",
]: ]:
account_number = cstr(child.get("account_number")).strip() account_number = cstr(child.get("account_number")).strip()
@ -95,7 +96,17 @@ def identify_is_group(child):
is_group = child.get("is_group") is_group = child.get("is_group")
elif len( elif len(
set(child.keys()) 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 is_group = 1
else: else:
@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
"root_type", "root_type",
"tax_rate", "tax_rate",
"account_number", "account_number",
"account_currency",
], ],
order_by="lft, rgt", order_by="lft, rgt",
) )
@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"root_type", "root_type",
"is_group", "is_group",
"tax_rate", "tax_rate",
"account_currency",
]: ]:
continue continue

View File

@ -36,7 +36,7 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data]) no_of_columns = max([len(d) for d in data])
if no_of_columns > 7: if no_of_columns > 8:
frappe.throw( frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"), _("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")), title=(_("Wrong Template")),
@ -233,6 +233,7 @@ def build_forest(data):
is_group, is_group,
account_type, account_type,
root_type, root_type,
account_currency,
) = i ) = i
if not account_name: if not account_name:
@ -253,6 +254,8 @@ def build_forest(data):
charts_map[account_name]["account_type"] = account_type charts_map[account_name]["account_type"] = account_type
if root_type: if root_type:
charts_map[account_name]["root_type"] = 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] path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created paths.append(path) # List of path is created
line_no += 1 line_no += 1
@ -315,6 +318,7 @@ def get_template(template_type):
"Is Group", "Is Group",
"Account Type", "Account Type",
"Root Type", "Root Type",
"Account Currency",
] ]
writer = UnicodeWriter() writer = UnicodeWriter()
writer.writerow(fields) writer.writerow(fields)

View File

@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
# Handle Accounts with '0' balance in Account/Base Currency # Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]: 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 current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0' new_balance_in_account_currency = 0 # this will be '0'
@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document):
journal_entry_accounts = [] journal_entry_accounts = []
for d in accounts: for d in accounts:
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
continue
dr_or_cr = ( dr_or_cr = (
"debit_in_account_currency" "debit_in_account_currency"
if d.get("balance_in_account_currency") > 0 if d.get("balance_in_account_currency") > 0
@ -448,7 +450,13 @@ class ExchangeRateRevaluation(Document):
} }
) )
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, "account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account), "balance": get_balance_on(unrealized_exchange_gain_loss_account),
@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document):
"exchange_rate": 1, "exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
} },
) )
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency() journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit() journal_entry.set_total_debit_credit()
journal_entry.save() journal_entry.save()

View File

@ -137,7 +137,8 @@
"fieldname": "finance_book", "fieldname": "finance_book",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Finance Book", "label": "Finance Book",
"options": "Finance Book" "options": "Finance Book",
"read_only": 1
}, },
{ {
"fieldname": "2_add_edit_gl_entries", "fieldname": "2_add_edit_gl_entries",
@ -538,7 +539,7 @@
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-17 12:53:53.280620", "modified": "2023-03-01 14:58:59.286591",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@ -495,26 +495,22 @@ def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype""" """get amount based on doctype"""
dt = ref_doc.doctype dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]: 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"]: elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency: if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount) grand_total = flt(ref_doc.outstanding_amount)
else: else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
elif dt == "POS Invoice": elif dt == "POS Invoice":
for pay in ref_doc.payments: for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account: if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount grand_total = pay.amount
break break
elif dt == "Fees": elif dt == "Fees":
grand_total = ref_doc.outstanding_amount grand_total = ref_doc.outstanding_amount
if grand_total > 0: if grand_total > 0:
return grand_total return grand_total
else: else:
frappe.throw(_("Payment Entry is already created")) frappe.throw(_("Payment Entry is already created"))

View File

@ -45,7 +45,10 @@ class TestPaymentRequest(unittest.TestCase):
frappe.get_doc(method).insert(ignore_permissions=True) frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self): 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( pr = make_payment_request(
dt="Sales Order", dt="Sales Order",
dn=so_inr.name, dn=so_inr.name,

View File

@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
bold_item_name = frappe.bold(item.item_name) bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold( 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) bold_invalid_batch_no = frappe.bold(item.batch_no)
@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice):
).format(item.idx, bold_invalid_batch_no, bold_item_name), ).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"), 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( frappe.throw(
_( _(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
), ),
title=_("Item Unavailable"), 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( frappe.throw(
_( _(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@ -651,7 +651,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty 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( if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item" "Item", item.item_code, "is_stock_item"
): ):

View File

@ -1485,11 +1485,17 @@ class PurchaseInvoice(BuyingController):
if po_details: if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified) 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): for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
pr_doc = frappe.get_doc("Purchase Receipt", pr) 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): def get_pr_details_billed_amt(self):
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice # Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice

View File

@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
company.enable_provisional_accounting_for_non_stock_items = 0 company.enable_provisional_accounting_for_non_stock_items = 0
company.save() 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): def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice") pi = frappe.new_doc("Purchase Invoice")

View File

@ -38,8 +38,11 @@
{% if(data[i].posting_date) { %} {% if(data[i].posting_date) { %}
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td> <td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
<td>{%= data[i].voucher_type %} <td>{%= data[i].voucher_type %}
<br>{%= data[i].voucher_no %}</td> <br>{%= data[i].voucher_no %}
<td> </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)) { %} {% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %} {%= data[i].party || data[i].account %}
<br> <br>
@ -49,11 +52,14 @@
{% if(data[i].bill_no) { %} {% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} <br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %} {% } %}
</td> </span>
<td style="text-align: right"> </td>
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td> <td style="text-align: right">
<td style="text-align: right"> {%= format_currency(data[i].debit, filters.presentation_currency) %}
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td> </td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency) %}
</td>
{% } else { %} {% } else { %}
<td></td> <td></td>
<td></td> <td></td>

View File

@ -43,9 +43,9 @@ erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return; if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); 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); accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name, frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
"accumulated_depreciation_amount", accumulated_depreciation);
}) })
}; };

View File

@ -10,7 +10,9 @@
"asset", "asset",
"naming_series", "naming_series",
"column_break_2", "column_break_2",
"gross_purchase_amount",
"opening_accumulated_depreciation", "opening_accumulated_depreciation",
"number_of_depreciations_booked",
"finance_book", "finance_book",
"finance_book_id", "finance_book_id",
"depreciation_details_section", "depreciation_details_section",
@ -148,18 +150,36 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation", "fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1,
"label": "Opening Accumulated Depreciation", "label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency", "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 "read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-16 21:08:21.421260", "modified": "2023-02-26 16:37:23.734806",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Depreciation Schedule", "name": "Asset Depreciation Schedule",

View File

@ -4,7 +4,15 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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): class AssetDepreciationSchedule(Document):
@ -83,15 +91,58 @@ class AssetDepreciationSchedule(Document):
date_of_return=None, date_of_return=None,
update_asset_finance_book_row=True, 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.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): def set_draft_asset_depr_schedule_details(self, asset_doc, row):
self.asset = asset_doc.name self.asset = asset_doc.name
self.finance_book = row.finance_book self.finance_book = row.finance_book
self.finance_book_id = row.idx self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation 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.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations self.total_number_of_depreciations = row.total_number_of_depreciations
self.frequency_of_depreciation = row.frequency_of_depreciation self.frequency_of_depreciation = row.frequency_of_depreciation
@ -102,7 +153,7 @@ class AssetDepreciationSchedule(Document):
def make_depr_schedule( def make_depr_schedule(
self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True 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 = [] self.depreciation_schedule = []
if not asset_doc.available_for_use_date: if not asset_doc.available_for_use_date:
@ -293,7 +344,9 @@ class AssetDepreciationSchedule(Document):
ignore_booked_entry=False, ignore_booked_entry=False,
): ):
straight_line_idx = [ 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) accumulated_depreciation = flt(self.opening_accumulated_depreciation)

View File

@ -18,6 +18,7 @@
"pr_required", "pr_required",
"column_break_12", "column_break_12",
"maintain_same_rate", "maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice", "bill_for_rejected_quantity_in_purchase_invoice",
"disable_last_purchase_rate", "disable_last_purchase_rate",
@ -147,6 +148,14 @@
"fieldname": "show_pay_button", "fieldname": "show_pay_button",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal" "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", "icon": "fa fa-cog",
@ -154,7 +163,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-02-15 14:42:10.200679", "modified": "2023-02-28 15:41:32.686805",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -21,3 +21,10 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series", self.get("supp_master_name") == "Naming Series",
hide_name_field=False, 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

View File

@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Item To Be Received"] = {
fieldname:"from_date", fieldname:"from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "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 reqd: 1
}, },
{ {
fieldname:"to_date", fieldname:"to_date",
label: __("To Date"), label: __("To Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.get_today(),
reqd: 1 reqd: 1
}, },
] ]

View File

@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
fieldname:"from_date", fieldname:"from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "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 reqd: 1
}, },
{ {
fieldname:"to_date", fieldname:"to_date",
label: __("To Date"), label: __("To Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.get_today(),
reqd: 1 reqd: 1
}, },
] ]

View File

@ -265,7 +265,10 @@ class BuyingController(SubcontractingController):
) / qty_in_stock_uom ) / qty_in_stock_uom
else: else:
item.valuation_rate = ( 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 ) / qty_in_stock_uom
else: else:
item.valuation_rate = 0.0 item.valuation_rate = 0.0

View File

@ -131,7 +131,7 @@ def validate_returned_items(doc):
) )
elif ref.serial_no: 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)) frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
else: else:
serial_nos = get_serial_nos(d.serial_no) serial_nos = get_serial_nos(d.serial_no)
@ -252,7 +252,6 @@ def get_already_returned_items(doc):
child.parent = par.name and par.docstatus = 1 child.parent = par.name and par.docstatus = 1
and par.is_return = 1 and par.return_against = %s and par.is_return = 1 and par.return_against = %s
group by item_code group by item_code
for update
""".format( """.format(
column, doc.doctype, doc.doctype column, doc.doctype, doc.doctype
), ),
@ -401,6 +400,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
if serial_nos: if serial_nos:
target_doc.serial_no = "\n".join(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"]: if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row( returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype source_parent.name, source_parent.supplier, source_doc.name, doctype
@ -611,7 +620,7 @@ def get_filters(
return filters 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 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
return_ref_field = frappe.scrub(child_doc.doctype) return_ref_field = frappe.scrub(child_doc.doctype)
@ -620,7 +629,7 @@ def get_returned_serial_nos(child_doc, parent_doc):
serial_nos = [] serial_nos = []
fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)] fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
filters = [ filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "return_against", "=", parent_doc.name],
@ -630,6 +639,6 @@ def get_returned_serial_nos(child_doc, parent_doc):
] ]
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): 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 return serial_nos

View File

@ -136,7 +136,7 @@ class SellingController(StockController):
self.in_words = money_in_words(amount, self.currency) self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self): 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 return
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate")) self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))

View File

@ -24,11 +24,19 @@ class calculate_taxes_and_totals(object):
def __init__(self, doc: Document): def __init__(self, doc: Document):
self.doc = doc self.doc = doc
frappe.flags.round_off_applicable_accounts = [] 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) get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate() 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): def calculate(self):
if not len(self.doc.get("items")): if not len(self._items):
return return
self.discount_amount_applied = False self.discount_amount_applied = False
@ -70,7 +78,7 @@ class calculate_taxes_and_totals(object):
if hasattr(self.doc, "tax_withholding_net_total"): if hasattr(self.doc, "tax_withholding_net_total"):
sum_net_amount = 0 sum_net_amount = 0
sum_base_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: if hasattr(item, "apply_tds") and item.apply_tds:
sum_net_amount += item.net_amount sum_net_amount += item.net_amount
sum_base_net_amount += item.base_net_amount sum_base_net_amount += item.base_net_amount
@ -79,7 +87,7 @@ class calculate_taxes_and_totals(object):
self.doc.base_tax_withholding_net_total = sum_base_net_amount self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self): 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"): if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code) item_doc = frappe.get_cached_doc("Item", item.item_code)
args = { args = {
@ -137,7 +145,7 @@ class calculate_taxes_and_totals(object):
return return
if not self.discount_amount_applied: if not self.discount_amount_applied:
for item in self.doc.get("items"): for item in self._items:
self.doc.round_floats_in(item) self.doc.round_floats_in(item)
if item.discount_percentage == 100: if item.discount_percentage == 100:
@ -236,7 +244,7 @@ class calculate_taxes_and_totals(object):
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")): if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return 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) item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0 cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0 total_inclusive_tax_amount_per_qty = 0
@ -317,7 +325,7 @@ class calculate_taxes_and_totals(object):
self.doc.total self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 ) = 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 += item.amount
self.doc.total_qty += item.qty self.doc.total_qty += item.qty
self.doc.base_total += item.base_amount self.doc.base_total += item.base_amount
@ -354,7 +362,7 @@ class calculate_taxes_and_totals(object):
] ]
) )
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) item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
for i, tax in enumerate(self.doc.get("taxes")): for i, tax in enumerate(self.doc.get("taxes")):
# tax_amount represents the amount of tax for the current step # tax_amount represents the amount of tax for the current step
@ -363,7 +371,7 @@ class calculate_taxes_and_totals(object):
# Adjust divisional loss to the last item # Adjust divisional loss to the last item
if tax.charge_type == "Actual": if tax.charge_type == "Actual":
actual_tax_dict[tax.idx] -= current_tax_amount 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] current_tax_amount += actual_tax_dict[tax.idx]
# accumulate tax amount into tax.tax_amount # accumulate tax amount into tax.tax_amount
@ -391,7 +399,7 @@ class calculate_taxes_and_totals(object):
) )
# set precision in the last item iteration # 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.round_off_totals(tax)
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
@ -570,7 +578,7 @@ class calculate_taxes_and_totals(object):
def calculate_total_net_weight(self): def calculate_total_net_weight(self):
if self.doc.meta.get_field("total_net_weight"): if self.doc.meta.get_field("total_net_weight"):
self.doc.total_net_weight = 0.0 self.doc.total_net_weight = 0.0
for d in self.doc.items: for d in self._items:
if d.total_weight: if d.total_weight:
self.doc.total_net_weight += d.total_weight self.doc.total_net_weight += d.total_weight
@ -630,7 +638,7 @@ class calculate_taxes_and_totals(object):
if total_for_discount_amount: if total_for_discount_amount:
# calculate item amount after 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 = ( distributed_amount = (
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
) )
@ -643,7 +651,7 @@ class calculate_taxes_and_totals(object):
self.doc.apply_discount_on == "Net Total" self.doc.apply_discount_on == "Net Total"
or not taxes or not taxes
or total_for_discount_amount == self.doc.net_total 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( discount_amount_loss = flt(
self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total") self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total")
) )

View File

@ -76,12 +76,9 @@ def get_transaction_list(
ignore_permissions = False ignore_permissions = False
if not filters: if not filters:
filters = [] filters = {}
if doctype in ["Supplier Quotation", "Purchase Invoice"]: filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1
filters.append((doctype, "docstatus", "<", 2))
else:
filters.append((doctype, "docstatus", "=", 1))
if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation": if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation":
parties_doctype = ( parties_doctype = (
@ -92,12 +89,12 @@ def get_transaction_list(
if customers: if customers:
if doctype == "Quotation": if doctype == "Quotation":
filters.append(("quotation_to", "=", "Customer")) filters["quotation_to"] = "Customer"
filters.append(("party_name", "in", customers)) filters["party_name"] = ["in", customers]
else: else:
filters.append(("customer", "in", customers)) filters["customer"] = ["in", customers]
elif suppliers: elif suppliers:
filters.append(("supplier", "in", suppliers)) filters["supplier"] = ["in", suppliers]
elif not custom: elif not custom:
return [] return []
@ -110,7 +107,7 @@ def get_transaction_list(
if not customers and not suppliers and custom: if not customers and not suppliers and custom:
ignore_permissions = False ignore_permissions = False
filters = [] filters = {}
transactions = get_list_for_transactions( transactions = get_list_for_transactions(
doctype, doctype,

View File

@ -19,10 +19,6 @@ frappe.ui.form.on("Opportunity", {
} }
} }
}); });
if (frm.doc.opportunity_from && frm.doc.party_name){
frm.trigger('set_contact_link');
}
}, },
validate: function(frm) { validate: function(frm) {
@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", {
} else { } else {
frappe.contacts.clear_address_and_contact(frm); 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) { set_contact_link: function(frm) {
@ -137,6 +137,8 @@ frappe.ui.form.on("Opportunity", {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'} frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) { } else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'} 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'}
} }
}, },

View File

@ -356,7 +356,7 @@ auto_cancel_exempted_doctypes = [
scheduler_events = { scheduler_events = {
"cron": { "cron": {
"0/5 * * * *": [ "0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs", "erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
], ],
"0/30 * * * *": [ "0/30 * * * *": [

View File

@ -64,8 +64,6 @@
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"fetch_from": "prevdoc_detail_docname.sales_person",
"fetch_if_empty": 1,
"fieldname": "service_person", "fieldname": "service_person",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -110,13 +108,15 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-05-27 17:47:21.474282", "modified": "2023-02-27 11:09:33.114458",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Visit Purpose", "name": "Maintenance Visit Purpose",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs():
["name", "boms_updated", "status"], ["name", "boms_updated", "status"],
) )
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches) 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 continue
# Prep parent BOMs & updated processed BOMs for next level # Prep parent BOMs & updated processed BOMs for next level
@ -252,6 +252,9 @@ def get_processed_current_boms(
current_boms = [] current_boms = []
for row in bom_batches: for row in bom_batches:
if not row.boms_updated:
continue
boms_updated = json.loads(row.boms_updated) boms_updated = json.loads(row.boms_updated)
current_boms.extend(boms_updated) current_boms.extend(boms_updated)
boms_updated_dict = {bom: True for bom in boms_updated} boms_updated_dict = {bom: True for bom in boms_updated}

View File

@ -561,7 +561,34 @@ class JobCard(Document):
) )
def set_transferred_qty_in_job_card_item(self, ste_doc): 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): def _validate_over_transfer(row, transferred_qty):
"Block over transfer of items if not allowed in settings." "Block over transfer of items if not allowed in settings."
@ -578,29 +605,23 @@ class JobCard(Document):
exc=JobCardOverTransferError, exc=JobCardOverTransferError,
) )
for row in ste_doc.items: job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
if not row.job_card_item:
continue
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") 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): def set_transferred_qty(self, update_status=False):
"Set total FG Qty in Job Card for which RM was transferred." "Set total FG Qty in Job Card for which RM was transferred."

View File

@ -506,7 +506,7 @@ frappe.ui.form.on("Work Order Item", {
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
frappe.model.set_value(cdt, cdn, { frappe.model.set_value(cdt, cdn, {
"required_qty": 1, "required_qty": row.required_qty || 1,
"item_name": r.message.item_name, "item_name": r.message.item_name,
"description": r.message.description, "description": r.message.description,
"source_warehouse": r.message.default_warehouse, "source_warehouse": r.message.default_warehouse,

View File

@ -4,7 +4,8 @@
import frappe import frappe
from frappe import _ 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 from pypika.terms import ExistsCriterion
@ -34,57 +35,55 @@ def get_columns():
def get_bom_stock(filters): def get_bom_stock(filters):
qty_to_produce = filters.get("qty_to_produce") or 1 qty_to_produce = filters.get("qty_to_produce")
if int(qty_to_produce) < 0: if cint(qty_to_produce) <= 0:
frappe.throw(_("Quantity to Produce can not be less than Zero")) frappe.throw(_("Quantity to Produce should be greater than zero."))
if filters.get("show_exploded_view"): if filters.get("show_exploded_view"):
bom_item_table = "BOM Explosion Item" bom_item_table = "BOM Explosion Item"
else: else:
bom_item_table = "BOM Item" bom_item_table = "BOM Item"
bin = frappe.qb.DocType("Bin") warehouse_details = frappe.db.get_value(
bom = frappe.qb.DocType("BOM") "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
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)
) )
if filters.get("warehouse"): BOM = frappe.qb.DocType("BOM")
warehouse_details = frappe.db.get_value( BOM_ITEM = frappe.qb.DocType(bom_item_table)
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 BIN = frappe.qb.DocType("Bin")
) WH = frappe.qb.DocType("Warehouse")
CONDITIONS = ()
if warehouse_details: if warehouse_details:
wh = frappe.qb.DocType("Warehouse") CONDITIONS = ExistsCriterion(
query = query.where( frappe.qb.from_(WH)
ExistsCriterion( .select(WH.name)
frappe.qb.from_(wh) .where(
.select(wh.name) (WH.lft >= warehouse_details.lft)
.where( & (WH.rgt <= warehouse_details.rgt)
(wh.lft >= warehouse_details.lft) & (BIN.warehouse == WH.name)
& (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()

View File

@ -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

View File

@ -27,7 +27,13 @@ def get_details_of_draft_or_submitted_depreciable_assets():
records = ( records = (
frappe.qb.from_(asset) 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.calculate_depreciation == 1)
.where(asset.docstatus < 2) .where(asset.docstatus < 2)
).run(as_dict=True) ).run(as_dict=True)

View File

@ -91,6 +91,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
_calculate_taxes_and_totals() { _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.validate_conversion_rate();
this.calculate_item_values(); this.calculate_item_values();
this.initialize_taxes(); this.initialize_taxes();
@ -122,7 +125,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_item_values() { calculate_item_values() {
var me = this; var me = this;
if (!this.discount_amount_applied) { 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); frappe.model.round_floats_in(item);
item.net_rate = item.rate; item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
@ -131,8 +134,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item)); item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
} }
else { else {
let qty = item.qty || 1; // allow for '0' qty on Credit/Debit notes
qty = me.frm.doc.is_return ? -1 * qty : qty; let qty = item.qty || -1
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
} }
@ -206,7 +209,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}); });
if(has_inclusive_tax==false) return; 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 item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
var cumulated_tax_fraction = 0.0; var cumulated_tax_fraction = 0.0;
var total_inclusive_tax_amount_per_qty = 0; var total_inclusive_tax_amount_per_qty = 0;
@ -277,7 +280,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var me = this; 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; 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 += item.amount;
me.frm.doc.total_qty += item.qty; me.frm.doc.total_qty += item.qty;
me.frm.doc.base_total += item.base_amount; me.frm.doc.base_total += item.base_amount;
@ -330,7 +333,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
}); });
$.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); var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(me.frm.doc["taxes"] || [], function(i, tax) { $.each(me.frm.doc["taxes"] || [], function(i, tax) {
// tax_amount represents the amount of tax for the current step // tax_amount represents the amount of tax for the current step
@ -339,7 +342,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Adjust divisional loss to the last item // Adjust divisional loss to the last item
if (tax.charge_type == "Actual") { if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] -= current_tax_amount; 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]; current_tax_amount += actual_tax_dict[tax.idx];
} }
} }
@ -376,7 +379,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
// set precision in the last item iteration // 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.round_off_totals(tax);
me.set_in_company_currency(tax, me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]); ["tax_amount", "tax_amount_after_discount_amount"]);
@ -599,10 +602,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
_cleanup() { _cleanup() {
this.frm.doc.base_in_words = this.frm.doc.in_words = ""; 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(items && items.length) {
if(!frappe.meta.get_docfield(this.frm.doc["items"][0].doctype, "item_tax_amount", this.frm.doctype)) { if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
$.each(this.frm.doc["items"] || [], function(i, item) { $.each(items || [], function(i, item) {
delete item["item_tax_amount"]; delete item["item_tax_amount"];
}); });
} }
@ -655,7 +659,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var net_total = 0; var net_total = 0;
// calculate item amount after Discount Amount // calculate item amount after Discount Amount
if (total_for_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; distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
item.net_amount = flt(item.net_amount - distributed_amount, item.net_amount = flt(item.net_amount - distributed_amount,
precision("base_amount", item)); precision("base_amount", item));
@ -663,7 +667,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// discount amount rounding loss adjustment if no taxes // 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")) 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 var discount_amount_loss = flt(me.frm.doc.net_total - net_total
- me.frm.doc.discount_amount, precision("net_total")); - me.frm.doc.discount_amount, precision("net_total"));
item.net_amount = flt(item.net_amount + discount_amount_loss, item.net_amount = flt(item.net_amount + discount_amount_loss,
@ -892,4 +896,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
} }
filtered_items() {
return this.frm.doc.items.filter(item => !item["is_alternative"]);
}
}; };

View File

@ -488,7 +488,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
() => { () => {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate); 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); me.apply_product_discount(d);
} }
}, },
@ -1884,11 +1884,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
get_advances() { get_advances() {
if(!this.frm.is_return) { if(!this.frm.is_return) {
var me = this;
return this.frm.call({ return this.frm.call({
method: "set_advances", method: "set_advances",
doc: this.frm.doc, doc: this.frm.doc,
callback: function(r, rt) { callback: function(r, rt) {
refresh_field("advances"); refresh_field("advances");
me.frm.dirty();
} }
}) })
} }

View File

@ -90,7 +90,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|| frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
this.frm.add_custom_button( this.frm.add_custom_button(
__("Sales Order"), __("Sales Order"),
this.frm.cscript["Make Sales Order"], () => this.make_sales_order(),
__("Create") __("Create")
); );
} }
@ -145,6 +145,20 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
} }
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(){ set_dynamic_field_label(){
if (this.frm.doc.quotation_to == "Customer") if (this.frm.doc.quotation_to == "Customer")
{ {
@ -220,17 +234,111 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
} }
}) })
} }
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.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) { 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 // enable tax_amount field if Actual
}) })

View File

@ -35,6 +35,9 @@ class Quotation(SellingController):
make_packing_list(self) make_packing_list(self)
def before_submit(self):
self.set_has_alternative_item()
def validate_valid_till(self): def validate_valid_till(self):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date")) frappe.throw(_("Valid till date cannot be before transaction date"))
@ -59,7 +62,18 @@ class Quotation(SellingController):
title=_("Unpublished Item"), 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): def get_ordered_status(self):
status = "Open"
ordered_items = frappe._dict( ordered_items = frappe._dict(
frappe.db.get_all( frappe.db.get_all(
"Sales Order Item", "Sales Order Item",
@ -70,16 +84,40 @@ class Quotation(SellingController):
) )
) )
status = "Open" if not ordered_items:
if 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" status = "Ordered"
for item in self.get("items"):
if item.qty > ordered_items.get(item.item_code, 0.0):
status = "Partially Ordered"
return status 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): def is_fully_ordered(self):
return self.get_ordered_status() == "Ordered" return self.get_ordered_status() == "Ordered"
@ -176,6 +214,22 @@ class Quotation(SellingController):
def on_recurring(self, reference_doc, auto_repeat_doc): def on_recurring(self, reference_doc, auto_repeat_doc):
self.valid_till = None 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): def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context from erpnext.controllers.website_list_for_contact import get_list_context
@ -221,6 +275,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
) )
) )
selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])]
def set_missing_values(source, target): def set_missing_values(source, target):
if customer: if customer:
target.customer = customer.name target.customer = customer.name
@ -244,6 +300,24 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
target.blanket_order = obj.blanket_order target.blanket_order = obj.blanket_order
target.blanket_order_rate = obj.blanket_order_rate 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( doclist = get_mapped_doc(
"Quotation", "Quotation",
source_name, source_name,
@ -253,7 +327,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
"doctype": "Sales Order Item", "doctype": "Sales Order Item",
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
"postprocess": update_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 Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
@ -322,7 +396,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
source_name, source_name,
{ {
"Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, "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 Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
}, },

View File

@ -457,6 +457,139 @@ class TestQuotation(FrappeTestCase):
expected_index = id + 1 expected_index = id + 1
self.assertEqual(item.idx, expected_index) 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") test_records = frappe.get_test_records("Quotation")

View File

@ -49,6 +49,8 @@
"pricing_rules", "pricing_rules",
"stock_uom_rate", "stock_uom_rate",
"is_free_item", "is_free_item",
"is_alternative",
"has_alternative_item",
"section_break_43", "section_break_43",
"valuation_rate", "valuation_rate",
"column_break_45", "column_break_45",
@ -643,12 +645,28 @@
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "currency",
"read_only": 1 "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-12-25 02:49:53.926625", "modified": "2023-02-06 11:00:07.042364",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation Item", "name": "Quotation Item",
@ -656,5 +674,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -275,7 +275,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
if (this.frm.doc.docstatus===0) { if (this.frm.doc.docstatus===0) {
this.frm.add_custom_button(__('Quotation'), this.frm.add_custom_button(__('Quotation'),
function() { function() {
erpnext.utils.map_current_doc({ let d = erpnext.utils.map_current_doc({
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
source_doctype: "Quotation", source_doctype: "Quotation",
target: me.frm, target: me.frm,
@ -293,7 +293,16 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
docstatus: 1, docstatus: 1,
status: ["!=", "Lost"] 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")); }, __("Get Items From"));
} }
@ -309,9 +318,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_work_order() { make_work_order() {
var me = this; var me = this;
this.frm.call({ me.frm.call({
doc: this.frm.doc, method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
method: 'get_work_order_items', args: {
sales_order: this.frm.docname,
},
freeze: true,
callback: function(r) { callback: function(r) {
if(!r.message) { if(!r.message) {
frappe.msgprint({ frappe.msgprint({
@ -321,14 +333,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}); });
return; return;
} }
else if(!r.message) { else {
frappe.msgprint({
title: __('Work Order not created'),
message: __('Work Order already created for all items with BOM'),
indicator: 'orange'
});
return;
} else {
const fields = [{ const fields = [{
label: 'Items', label: 'Items',
fieldtype: 'Table', fieldtype: 'Table',
@ -429,9 +434,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_raw_material_request() { make_raw_material_request() {
var me = this; var me = this;
this.frm.call({ this.frm.call({
doc: this.frm.doc, method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
method: 'get_work_order_items',
args: { args: {
sales_order: this.frm.docname,
for_raw_material_request: 1 for_raw_material_request: 1
}, },
callback: function(r) { callback: function(r) {
@ -450,6 +455,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} }
make_raw_material_request_dialog(r) { make_raw_material_request_dialog(r) {
var me = this;
var fields = [ var fields = [
{fieldtype:'Check', fieldname:'include_exploded_items', {fieldtype:'Check', fieldname:'include_exploded_items',
label: __('Include Exploded Items')}, label: __('Include Exploded Items')},

View File

@ -6,11 +6,12 @@ import json
import frappe import frappe
import frappe.utils import frappe.utils
from frappe import _ from frappe import _, qb
from frappe.contacts.doctype.address.address import get_company_address from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values 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 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 ( from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
@ -414,51 +415,6 @@ class SalesOrder(SellingController):
self.indicator_color = "green" self.indicator_color = "green"
self.indicator_title = _("Paid") 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 on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): 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) delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
@ -1350,3 +1306,57 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
return return
frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty) 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

View File

@ -1217,6 +1217,8 @@ class TestSalesOrder(FrappeTestCase):
self.assertTrue(si.get("payment_schedule")) self.assertTrue(si.get("payment_schedule"))
def test_make_work_order(self): def test_make_work_order(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
# Make a new Sales Order # Make a new Sales Order
so = make_sales_order( so = make_sales_order(
**{ **{
@ -1230,7 +1232,7 @@ class TestSalesOrder(FrappeTestCase):
# Raise Work Orders # Raise Work Orders
po_items = [] po_items = []
so_item_name = {} so_item_name = {}
for item in so.get_work_order_items(): for item in get_work_order_items(so.name):
po_items.append( po_items.append(
{ {
"warehouse": item.get("warehouse"), "warehouse": item.get("warehouse"),
@ -1448,6 +1450,7 @@ class TestSalesOrder(FrappeTestCase):
from erpnext.controllers.item_variant import create_variant from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom 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 make_item( # template item
"Test-WO-Tshirt", "Test-WO-Tshirt",
@ -1487,7 +1490,7 @@ class TestSalesOrder(FrappeTestCase):
] ]
} }
) )
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("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name) self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
@ -1497,6 +1500,8 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(wo_items[1].get("bom"), template_bom.name) self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self): def test_request_for_raw_materials(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
item = make_item( item = make_item(
"_Test Finished Item", "_Test Finished Item",
{ {
@ -1529,7 +1534,7 @@ class TestSalesOrder(FrappeTestCase):
so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]}) so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]})
so.submit() so.submit()
mr_dict = frappe._dict() 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["items"] = items
mr_dict["include_exploded_items"] = 0 mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1 mr_dict["ignore_existing_ordered_qty"] = 1

View File

@ -522,7 +522,7 @@ erpnext.PointOfSale.Controller = class {
const from_selector = field === 'qty' && value === "+1"; const from_selector = field === 'qty' && value === "+1";
if (from_selector) if (from_selector)
value = flt(item_row.qty) + flt(value); value = flt(item_row.stock_qty) + flt(value);
if (item_row_exists) { if (item_row_exists) {
if (field === 'qty') if (field === 'qty')

View File

@ -253,7 +253,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
} }
calculate_commission() { 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) { if(this.frm.doc.commission_rate > 100) {
this.frm.set_value("commission_rate", 100); this.frm.set_value("commission_rate", 100);

View File

@ -33,6 +33,9 @@ frappe.ui.form.on("Item", {
'Material Request': () => { 'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items"); open_form(frm, "Material Request", "Material Request Item", "items");
}, },
'Stock Entry': () => {
open_form(frm, "Stock Entry", "Stock Entry Detail", "items");
},
}; };
}, },
@ -893,6 +896,9 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.item_name = frm.doc.item_name; new_child_doc.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom; new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description; new_child_doc.description = frm.doc.description;
if (!new_child_doc.qty) {
new_child_doc.qty = 1.0;
}
frappe.run_serially([ frappe.run_serially([
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc), () => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),

View File

@ -54,7 +54,7 @@ class ItemAlternative(Document):
if not item_data.allow_alternative_item: if not item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code)) frappe.throw(alternate_item_check_msg.format(self.item_code))
if self.two_way and not alternative_item_data.allow_alternative_item: 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): def validate_duplicate(self):
if frappe.db.get_value( if frappe.db.get_value(

View File

@ -2,7 +2,18 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Item Price", { 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 // Fetch price list details
frm.add_fetch("price_list", "buying", "buying"); frm.add_fetch("price_list", "buying", "buying");
frm.add_fetch("price_list", "selling", "selling"); frm.add_fetch("price_list", "selling", "selling");

View File

@ -3,7 +3,7 @@
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Criterion from frappe.query_builder import Criterion
from frappe.query_builder.functions import Cast_ from frappe.query_builder.functions import Cast_
@ -21,6 +21,7 @@ class ItemPrice(Document):
self.update_price_list_details() self.update_price_list_details()
self.update_item_details() self.update_item_details()
self.check_duplicates() self.check_duplicates()
self.validate_item_template()
def validate_item(self): def validate_item(self):
if not frappe.db.exists("Item", self.item_code): if not frappe.db.exists("Item", self.item_code):
@ -49,6 +50,12 @@ class ItemPrice(Document):
"Item", self.item_code, ["item_name", "description"] "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): def check_duplicates(self):
item_price = frappe.qb.DocType("Item Price") item_price = frappe.qb.DocType("Item Price")

View File

@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase):
frappe.db.sql("delete from `tabItem Price`") frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True) 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): def test_duplicate_item(self):
doc = frappe.copy_doc(test_records[0]) doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.save) self.assertRaises(ItemPriceDuplicateItem, doc.save)

View File

@ -10,6 +10,7 @@ import json
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc 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 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 from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
@ -180,6 +181,34 @@ class MaterialRequest(BuyingController):
self.update_requested_qty() self.update_requested_qty()
self.update_requested_qty_in_production_plan() 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): def update_completed_qty(self, mr_items=None, update_modified=True):
if self.material_request_type == "Purchase": if self.material_request_type == "Purchase":
return return
@ -187,18 +216,13 @@ class MaterialRequest(BuyingController):
if not mr_items: if not mr_items:
mr_items = [d.name for d in self.get("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"): for d in self.get("items"):
if d.name in mr_items: if d.name in mr_items:
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
d.ordered_qty = flt( d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
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")
if mr_qty_allowance: if mr_qty_allowance:
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100)) allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
@ -217,14 +241,7 @@ class MaterialRequest(BuyingController):
) )
elif self.material_request_type == "Manufacture": elif self.material_request_type == "Manufacture":
d.ordered_qty = flt( d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
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]
)
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty) frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
@ -587,6 +604,9 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.purpose = source.material_request_type target.purpose = source.material_request_type
target.from_warehouse = source.set_from_warehouse
target.to_warehouse = source.set_warehouse
if source.job_card: if source.job_card:
target.purpose = "Material Transfer for Manufacture" target.purpose = "Material Transfer for Manufacture"
@ -722,6 +742,7 @@ def create_pick_list(source_name, target_doc=None):
def make_in_transit_stock_entry(source_name, in_transit_warehouse): def make_in_transit_stock_entry(source_name, in_transit_warehouse):
ste_doc = make_stock_entry(source_name) ste_doc = make_stock_entry(source_name)
ste_doc.add_to_transit = 1 ste_doc.add_to_transit = 1
ste_doc.to_warehouse = in_transit_warehouse
for row in ste_doc.items: for row in ste_doc.items:
row.t_warehouse = in_transit_warehouse row.t_warehouse = in_transit_warehouse

View File

@ -293,6 +293,7 @@ class PurchaseReceipt(BuyingController):
get_purchase_document_details, get_purchase_document_details,
) )
stock_rbnb = None
if erpnext.is_perpetual_inventory_enabled(self.company): if erpnext.is_perpetual_inventory_enabled(self.company):
stock_rbnb = self.get_company_default("stock_received_but_not_billed") stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name) landed_cost_entries = get_item_account_wise_additional_cost(self.name)
@ -450,6 +451,21 @@ class PurchaseReceipt(BuyingController):
item=d, 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 # sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
self.add_gl_entry( self.add_gl_entry(
@ -470,10 +486,11 @@ class PurchaseReceipt(BuyingController):
+ flt(d.landed_cost_voucher_amount) + flt(d.landed_cost_voucher_amount)
+ flt(d.rm_supp_cost) + flt(d.rm_supp_cost)
+ flt(d.item_tax_amount) + flt(d.item_tax_amount)
+ flt(d.rate_difference_with_purchase_invoice)
) )
divisional_loss = flt( 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: if divisional_loss:
@ -765,7 +782,7 @@ class PurchaseReceipt(BuyingController):
updated_pr += update_billed_amount_based_on_po(po_details, update_modified) updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
for pr in set(updated_pr): 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) update_billing_percentage(pr_doc, update_modified=update_modified)
self.load_from_db() self.load_from_db()
@ -881,7 +898,7 @@ def get_billed_amount_against_po(po_items):
return {d.po_detail: flt(d.billed_amt) for d in query} 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 # Reload as billed amount was set in db directly
pr_doc.load_from_db() pr_doc.load_from_db()
@ -897,6 +914,12 @@ def update_billing_percentage(pr_doc, update_modified=True):
total_amount += total_billable_amount total_amount += total_billable_amount
total_billed_amount += flt(item.billed_amt) 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) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed) pr_doc.db_set("per_billed", percent_billed)
@ -906,6 +929,26 @@ def update_billing_percentage(pr_doc, update_modified=True):
pr_doc.set_status(update=True) pr_doc.set_status(update=True)
pr_doc.notify_update() 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): def get_item_wise_returned_qty(pr_doc):
items = [d.name for d in pr_doc.items] items = [d.name for d in pr_doc.items]

View File

@ -69,6 +69,7 @@
"item_tax_amount", "item_tax_amount",
"rm_supp_cost", "rm_supp_cost",
"landed_cost_voucher_amount", "landed_cost_voucher_amount",
"rate_difference_with_purchase_invoice",
"billed_amt", "billed_amt",
"warehouse_and_reference", "warehouse_and_reference",
"warehouse", "warehouse",
@ -1007,12 +1008,20 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Has Item Scanned", "label": "Has Item Scanned",
"read_only": 1 "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-01-18 15:48:58.114923", "modified": "2023-02-28 15:43:04.470104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc 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 ( from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import (
get_template_details, get_template_details,
@ -156,7 +156,9 @@ class QualityInspection(Document):
for i in range(1, 11): for i in range(1, 11):
reading_value = reading.get("reading_" + str(i)) reading_value = reading.get("reading_" + str(i))
if reading_value is not None and reading_value.strip(): 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: if not result:
return False return False
return True return True
@ -196,7 +198,7 @@ class QualityInspection(Document):
# numeric readings # numeric readings
for i in range(1, 11): for i in range(1, 11):
field = "reading_" + str(i) field = "reading_" + str(i)
data[field] = flt(reading.get(field)) data[field] = parse_float(reading.get(field))
data["mean"] = self.calculate_mean(reading) data["mean"] = self.calculate_mean(reading)
return data return data
@ -210,7 +212,7 @@ class QualityInspection(Document):
for i in range(1, 11): for i in range(1, 11):
reading_value = reading.get("reading_" + str(i)) reading_value = reading.get("reading_" + str(i))
if reading_value is not None and reading_value.strip(): 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 actual_mean = mean(readings_list) if readings_list else 0
return actual_mean return actual_mean
@ -324,3 +326,19 @@ def make_quality_inspection(source_name, target_doc=None):
) )
return doc 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)

View File

@ -2,7 +2,7 @@
# See license.txt # See license.txt
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.controllers.stock_controller import ( from erpnext.controllers.stock_controller import (
@ -216,6 +216,40 @@ class TestQualityInspection(FrappeTestCase):
qa.save() qa.save()
self.assertEqual(qa.status, "Accepted") 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): def create_quality_inspection(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -397,6 +397,7 @@ class StockReconciliation(StockController):
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"voucher_detail_no": row.name, "voucher_detail_no": row.name,
"actual_qty": 0,
"company": self.company, "company": self.company,
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"is_cancelled": 1 if self.docstatus == 2 else 0, "is_cancelled": 1 if self.docstatus == 2 else 0,
@ -423,6 +424,8 @@ class StockReconciliation(StockController):
data.valuation_rate = flt(row.valuation_rate) data.valuation_rate = flt(row.valuation_rate)
data.stock_value_difference = -1 * flt(row.amount_difference) data.stock_value_difference = -1 * flt(row.amount_difference)
self.update_inventory_dimensions(row, data)
return data return data
def make_sle_on_cancel(self): def make_sle_on_cancel(self):

View File

@ -8,6 +8,7 @@ import frappe
from frappe import _, throw from frappe import _, throw
from frappe.model import child_table_fields, default_fields from frappe.model import child_table_fields, default_fields
from frappe.model.meta import get_field_precision 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 frappe.utils import add_days, add_months, cint, cstr, flt, getdate
from erpnext import get_company_currency from erpnext import get_company_currency
@ -526,12 +527,8 @@ def get_barcode_data(items_list):
itemwise_barcode = {} itemwise_barcode = {}
for item in items_list: for item in items_list:
barcodes = frappe.db.sql( barcodes = frappe.db.get_all(
""" "Item Barcode", filters={"parent": item.item_code}, fields="barcode"
select barcode from `tabItem Barcode` where parent = %s
""",
item.item_code,
as_dict=1,
) )
for barcode in barcodes: for barcode in barcodes:
@ -891,34 +888,36 @@ def get_item_price(args, item_code, ignore_party=False):
:param item_code: str, Item Doctype field item_code :param item_code: str, Item Doctype field item_code
""" """
args["item_code"] = item_code ip = frappe.qb.DocType("Item Price")
query = (
conditions = """where item_code=%(item_code)s frappe.qb.from_(ip)
and price_list=%(price_list)s .select(ip.name, ip.price_list_rate, ip.uom)
and ifnull(uom, '') in ('', %(uom)s)""" .where(
(ip.item_code == item_code)
conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)" & (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 not ignore_party:
if args.get("customer"): if args.get("customer"):
conditions += " and customer=%(customer)s" query = query.where(ip.customer == args.get("customer"))
elif args.get("supplier"): elif args.get("supplier"):
conditions += " and supplier=%(supplier)s" query = query.where(ip.supplier == args.get("supplier"))
else: 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"): if args.get("transaction_date"):
conditions += """ and %(transaction_date)s between query = query.where(
ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" (IfNull(ip.valid_from, "2000-01-01") <= args["transaction_date"])
& (IfNull(ip.valid_upto, "2500-12-31") >= args["transaction_date"])
)
return frappe.db.sql( return query.run()
""" 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,
)
def get_price_list_rate_for(args, item_code): def get_price_list_rate_for(args, item_code):
@ -1091,91 +1090,68 @@ def get_pos_profile(company, pos_profile=None, user=None):
if not user: if not user:
user = frappe.session["user"] user = frappe.session["user"]
condition = "pfu.user = %(user)s AND pfu.default=1" pf = frappe.qb.DocType("POS Profile")
if user and company: pfu = frappe.qb.DocType("POS Profile User")
condition = "pfu.user = %(user)s AND pf.company = %(company)s AND pfu.default=1"
pos_profile = frappe.db.sql( query = (
"""SELECT pf.* frappe.qb.from_(pf)
FROM .left_join(pfu)
`tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu .on(pf.name == pfu.parent)
ON .select(pf.star)
pf.name = pfu.parent .where((pfu.user == user) & (pfu.default == 1))
WHERE
{cond} AND pf.disabled = 0
""".format(
cond=condition
),
{"user": user, "company": company},
as_dict=1,
) )
if company:
query = query.where(pf.company == company)
pos_profile = query.run(as_dict=True)
if not pos_profile and company: if not pos_profile and company:
pos_profile = frappe.db.sql( pos_profile = (
"""SELECT pf.* frappe.qb.from_(pf)
FROM .left_join(pfu)
`tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu .on(pf.name == pfu.parent)
ON .select(pf.star)
pf.name = pfu.parent .where((pf.company == company) & (pf.disabled == 0))
WHERE ).run(as_dict=True)
pf.company = %(company)s AND pf.disabled = 0
""",
{"company": company},
as_dict=1,
)
return pos_profile and pos_profile[0] or None return pos_profile and pos_profile[0] or None
def get_serial_nos_by_fifo(args, sales_order=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"): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"):
return "\n".join( sn = frappe.qb.DocType("Serial No")
frappe.db.sql_list( query = (
"""select name from `tabSerial No` frappe.qb.from_(sn)
where item_code=%(item_code)s and warehouse=%(warehouse)s and .select(sn.name)
sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) .where((sn.item_code == args.item_code) & (sn.warehouse == args.warehouse))
order by timestamp(purchase_date, purchase_time) .orderby(CombineDatetime(sn.purchase_date, sn.purchase_time))
asc limit %(qty)s""", .limit(abs(cint(args.stock_qty)))
{
"item_code": args.item_code,
"warehouse": args.warehouse,
"qty": abs(cint(args.stock_qty)),
"sales_order": sales_order,
},
)
) )
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): serial_nos = query.run(as_list=True)
if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): serial_nos = [s[0] for s in serial_nos]
return "\n".join(
frappe.db.sql_list( return "\n".join(serial_nos)
"""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,
},
)
)
@frappe.whitelist() @frappe.whitelist()
def get_conversion_factor(item_code, uom): def get_conversion_factor(item_code, uom):
variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True) variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True)
filters = {"parent": item_code, "uom": uom} filters = {"parent": item_code, "uom": uom}
if variant_of: if variant_of:
filters["parent"] = ("in", (item_code, variant_of)) filters["parent"] = ("in", (item_code, variant_of))
conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor") conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor")
if not conversion_factor: if not conversion_factor:
stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") stock_uom = frappe.db.get_value("Item", item_code, "stock_uom")
conversion_factor = get_uom_conv_factor(uom, stock_uom) conversion_factor = get_uom_conv_factor(uom, stock_uom)
return {"conversion_factor": conversion_factor or 1.0} return {"conversion_factor": conversion_factor or 1.0}
@ -1217,12 +1193,16 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses
def get_company_total_stock(item_code, company): def get_company_total_stock(item_code, company):
return frappe.db.sql( bin = frappe.qb.DocType("Bin")
"""SELECT sum(actual_qty) from wh = frappe.qb.DocType("Warehouse")
(`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name)
WHERE `tabWarehouse`.company = %s and `tabBin`.item_code = %s""", return (
(company, item_code), frappe.qb.from_(bin)
)[0][0] .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() @frappe.whitelist()
@ -1231,6 +1211,7 @@ def get_serial_no_details(item_code, warehouse, stock_qty, serial_no):
{"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no} {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no}
) )
serial_no = get_serial_no(args) serial_no = get_serial_no(args)
return {"serial_no": serial_no} return {"serial_no": serial_no}
@ -1250,6 +1231,7 @@ def get_bin_details_and_serial_nos(
bin_details_and_serial_nos.update( bin_details_and_serial_nos.update(
get_serial_no_details(item_code, warehouse, stock_qty, serial_no) get_serial_no_details(item_code, warehouse, stock_qty, serial_no)
) )
return bin_details_and_serial_nos return bin_details_and_serial_nos
@ -1264,6 +1246,7 @@ def get_batch_qty_and_serial_no(batch_no, stock_qty, warehouse, item_code, has_s
) )
serial_no = get_serial_no(args) serial_no = get_serial_no(args)
batch_qty_and_serial_no.update({"serial_no": serial_no}) batch_qty_and_serial_no.update({"serial_no": serial_no})
return batch_qty_and_serial_no return batch_qty_and_serial_no
@ -1336,7 +1319,6 @@ def apply_price_list(args, as_doc=False):
def apply_price_list_on_item(args): def apply_price_list_on_item(args):
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1) 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 = get_price_list_rate(args, item_doc)
item_details.update(get_pricing_rule_for_item(args)) item_details.update(get_pricing_rule_for_item(args))
return item_details return item_details
@ -1420,12 +1402,12 @@ def get_valuation_rate(item_code, company, warehouse=None):
) or {"valuation_rate": 0} ) or {"valuation_rate": 0}
elif not item.get("is_stock_item"): elif not item.get("is_stock_item"):
valuation_rate = frappe.db.sql( pi_item = frappe.qb.DocType("Purchase Invoice Item")
"""select sum(base_net_amount) / sum(qty*conversion_factor) valuation_rate = (
from `tabPurchase Invoice Item` frappe.qb.from_(pi_item)
where item_code = %s and docstatus=1""", .select((Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor)))
item_code, .where((pi_item.docstatus == 1) & (pi_item.item_code == item_code))
) ).run()
if valuation_rate: if valuation_rate:
return {"valuation_rate": valuation_rate[0][0] or 0.0} return {"valuation_rate": valuation_rate[0][0] or 0.0}
@ -1451,7 +1433,7 @@ def get_serial_no(args, serial_nos=None, sales_order=None):
if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"): 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") 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: 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: elif has_serial_no == 1:
args = json.dumps( args = json.dumps(
{ {
@ -1483,31 +1465,35 @@ def get_blanket_order_details(args):
args = frappe._dict(json.loads(args)) args = frappe._dict(json.loads(args))
blanket_order_details = None 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( if args.item_code:
""" bo = frappe.qb.DocType("Blanket Order")
select boi.rate as blanket_order_rate, bo.name as blanket_order bo_item = frappe.qb.DocType("Blanket Order Item")
from `tabBlanket Order` bo, `tabBlanket Order Item` boi
where bo.company=%(company)s and boi.item_code=%(item_code)s query = (
and bo.docstatus=1 and bo.name = boi.parent {0} frappe.qb.from_(bo)
""".format( .from_(bo_item)
condition .select(bo_item.rate.as_("blanket_order_rate"), bo.name.as_("blanket_order"))
), .where(
args, (bo.company == args.company)
as_dict=True, & (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 "" blanket_order_details = blanket_order_details[0] if blanket_order_details else ""
return blanket_order_details return blanket_order_details
@ -1517,10 +1503,10 @@ def get_so_reservation_for_item(args):
if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")): if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")):
reserved_so = args.get("against_sales_order") reserved_so = args.get("against_sales_order")
elif args.get("against_sales_invoice"): elif args.get("against_sales_invoice"):
sales_order = frappe.db.sql( sales_order = frappe.db.get_all(
"""select sales_order from `tabSales Invoice Item` where "Sales Invoice Item",
parent=%s and item_code=%s""", filters={"parent": args.get("against_sales_invoice"), "item_code": args.get("item_code")},
(args.get("against_sales_invoice"), args.get("item_code")), fields="sales_order",
) )
if sales_order and sales_order[0]: if sales_order and sales_order[0]:
if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")): if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")):
@ -1532,13 +1518,14 @@ def get_so_reservation_for_item(args):
def get_reserved_qty_for_so(sales_order, item_code): def get_reserved_qty_for_so(sales_order, item_code):
reserved_qty = frappe.db.sql( reserved_qty = frappe.db.get_value(
"""select sum(qty) from `tabSales Order Item` "Sales Order Item",
where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1 filters={
""", "parent": sales_order,
(sales_order, item_code), "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] return reserved_qty or 0
else:
return 0

View File

@ -191,14 +191,17 @@ class SubcontractingReceipt(SubcontractingController):
def validate_available_qty_for_consumption(self): def validate_available_qty_for_consumption(self):
for item in self.get("supplied_items"): for item in self.get("supplied_items"):
precision = item.precision("consumed_qty")
if ( 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( msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)}
_( must be less than or equal to Available Qty For Consumption
"Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table." {flt(item.available_qty_for_consumption, precision)}
).format(item.idx) in Consumed Items Table."""
)
frappe.throw(_(msg))
def validate_items_qty(self): def validate_items_qty(self):
for item in self.items: for item in self.items:

View File

@ -13,8 +13,8 @@ from frappe.utils import (
get_datetime, get_datetime,
get_datetime_str, get_datetime_str,
get_link_to_form, get_link_to_form,
get_system_timezone,
get_time, get_time,
get_time_zone,
get_weekdays, get_weekdays,
getdate, getdate,
nowdate, nowdate,
@ -981,7 +981,7 @@ def convert_utc_to_user_timezone(utc_timestamp, user):
def get_tz(user): 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() @frappe.whitelist()

View File

@ -9916,3 +9916,5 @@ Cost and Freight,Kosten und Fracht,
Delivered at Place,Geliefert benannter Ort, Delivered at Place,Geliefert benannter Ort,
Delivered at Place Unloaded,Geliefert benannter Ort entladen, Delivered at Place Unloaded,Geliefert benannter Ort entladen,
Delivered Duty Paid,Geliefert verzollt, Delivered Duty Paid,Geliefert verzollt,
Discount Validity,Frist für den Rabatt,
Discount Validity Based On,Frist für den Rabatt berechnet sich nach,

Can't render this file because it is too large.

View File

@ -10,6 +10,7 @@ import pytz
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.data import get_system_timezone
from pyyoutube import Api from pyyoutube import Api
@ -64,7 +65,7 @@ def update_youtube_data():
frequency = get_frequency(frequency) frequency = get_frequency(frequency)
time = datetime.now() time = datetime.now()
timezone = pytz.timezone(frappe.utils.get_time_zone()) timezone = pytz.timezone(get_system_timezone())
site_time = time.astimezone(timezone) site_time = time.astimezone(timezone)
if frequency == 30: if frequency == 30:

View File

@ -4,6 +4,7 @@ import json
import frappe import frappe
import pytz import pytz
from frappe import _ from frappe import _
from frappe.utils.data import get_system_timezone
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
@ -125,7 +126,7 @@ def filter_timeslots(date, timeslots):
def convert_to_guest_timezone(guest_tz, datetimeobject): def convert_to_guest_timezone(guest_tz, datetimeobject):
guest_tz = pytz.timezone(guest_tz) 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 = local_timezone.localize(datetimeobject)
datetimeobject = datetimeobject.astimezone(guest_tz) datetimeobject = datetimeobject.astimezone(guest_tz)
return datetimeobject return datetimeobject
@ -134,7 +135,7 @@ def convert_to_guest_timezone(guest_tz, datetimeobject):
def convert_to_system_timezone(guest_tz, datetimeobject): def convert_to_system_timezone(guest_tz, datetimeobject):
guest_tz = pytz.timezone(guest_tz) guest_tz = pytz.timezone(guest_tz)
datetimeobject = guest_tz.localize(datetimeobject) 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) datetimeobject = datetimeobject.astimezone(system_tz)
return datetimeobject return datetimeobject

View File

@ -28,9 +28,6 @@ dependencies = [
requires = ["flit_core >=3.4,<4"] requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi" build-backend = "flit_core.buildapi"
[tool.bench.dev-dependencies]
hypothesis = "~=6.31.0"
[tool.black] [tool.black]
line-length = 99 line-length = 99