Merge branch 'develop' into fix_flaky_test_in_payment_terms_report
This commit is contained in:
commit
a43304b01b
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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",
|
||||||
|
@ -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"))
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
):
|
):
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"))
|
||||||
|
@ -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")
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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 * * * *": [
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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}
|
||||||
|
@ -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."
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
@ -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)
|
||||||
|
@ -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"]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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},
|
||||||
},
|
},
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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')},
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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);
|
||||||
|
@ -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),
|
||||||
|
@ -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(
|
||||||
|
@ -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");
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user