Merge pull request #44341 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-11-27 21:30:10 +05:30
committed by GitHub
63 changed files with 708 additions and 315 deletions

View File

@@ -515,6 +515,55 @@ class TestJournalEntry(unittest.TestCase):
self.assertEqual(row.debit_in_account_currency, 100) self.assertEqual(row.debit_in_account_currency, 100)
self.assertEqual(row.credit_in_account_currency, 100) self.assertEqual(row.credit_in_account_currency, 100)
def test_toggle_debit_credit_if_negative(self):
from erpnext.accounts.general_ledger import process_gl_map
# Create JV with defaut cost center - _Test Cost Center
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
jv = frappe.new_doc("Journal Entry")
jv.posting_date = nowdate()
jv.company = "_Test Company"
jv.user_remark = "test"
jv.extend(
"accounts",
[
{
"account": "_Test Cash - _TC",
"debit": 100 * -1,
"debit_in_account_currency": 100 * -1,
"exchange_rate": 1,
},
{
"account": "_Test Bank - _TC",
"credit": 100 * -1,
"credit_in_account_currency": 100 * -1,
"exchange_rate": 1,
},
],
)
jv.flags.ignore_validate = True
jv.save()
self.assertEqual(len(jv.accounts), 2)
gl_map = jv.build_gl_map()
for row in gl_map:
if row.account == "_Test Cash - _TC":
self.assertEqual(row.debit, 100 * -1)
self.assertEqual(row.debit_in_account_currency, 100 * -1)
self.assertEqual(row.debit_in_transaction_currency, 100 * -1)
gl_map = process_gl_map(gl_map, False)
for row in gl_map:
if row.account == "_Test Cash - _TC":
self.assertEqual(row.credit, 100)
self.assertEqual(row.credit_in_account_currency, 100)
self.assertEqual(row.credit_in_transaction_currency, 100)
def test_transaction_exchange_rate_on_journals(self): def test_transaction_exchange_rate_on_journals(self):
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False) jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1}) jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})

View File

@@ -1219,11 +1219,19 @@ class PaymentEntry(AccountsController):
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
gle.update( gle.update(
{ self.get_gl_dict(
dr_or_cr: allocated_amount_in_company_currency, {
dr_or_cr + "_in_account_currency": d.allocated_amount, "account": self.party_account,
"cost_center": cost_center, "party_type": self.party_type,
} "party": self.party,
"against": against_account,
"account_currency": self.party_account_currency,
"cost_center": cost_center,
dr_or_cr + "_in_account_currency": d.allocated_amount,
dr_or_cr: allocated_amount_in_company_currency,
},
item=self,
)
) )
if self.book_advance_payments_in_separate_party_account: if self.book_advance_payments_in_separate_party_account:
@@ -1746,7 +1754,7 @@ class PaymentEntry(AccountsController):
if paid_amount > total_negative_outstanding: if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0: if total_negative_outstanding == 0:
frappe.msgprint( frappe.msgprint(
_("Cannot {0} from {2} without any negative outstanding invoice").format( _("Cannot {0} from {1} without any negative outstanding invoice").format(
self.payment_type, self.payment_type,
self.party_type, self.party_type,
) )

View File

@@ -3,7 +3,7 @@ import json
import frappe import frappe
from frappe import _, qb from frappe import _, qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Abs, Sum
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
@@ -564,6 +564,8 @@ def make_payment_request(**args):
# fetches existing payment request `grand_total` amount # fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name) existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount): def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
grand_total -= existing_payment_request_amount grand_total -= existing_payment_request_amount
if not grand_total: if not grand_total:
@@ -583,6 +585,15 @@ def make_payment_request(**args):
else: else:
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount) grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
if existing_paid_amount:
if ref_doc.party_account_currency == ref_doc.currency:
if ref_doc.conversion_rate:
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
else:
grand_total -= flt(existing_paid_amount)
else:
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
if draft_payment_request: if draft_payment_request:
frappe.db.set_value( frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
@@ -753,6 +764,27 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
return response[0][0] if response[0] else 0 return response[0][0] if response[0] else 0
def get_existing_paid_amount(doctype, name):
PL = frappe.qb.DocType("Payment Ledger Entry")
PER = frappe.qb.DocType("Payment Entry Reference")
query = (
frappe.qb.from_(PL)
.left_join(PER)
.on(
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
)
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
.where(PL.against_voucher_type.eq(doctype))
.where(PL.against_voucher_no.eq(name))
.where(PL.amount < 0)
.where(PER.payment_request.isnull())
)
response = query.run()
return response[0][0] if response[0] else 0
def get_gateway_details(args): # nosemgrep def get_gateway_details(args): # nosemgrep
""" """
Return gateway and payment account of default payment gateway Return gateway and payment account of default payment gateway

View File

@@ -7,6 +7,7 @@ import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -524,3 +525,21 @@ class TestPaymentRequest(FrappeTestCase):
self.assertEqual(pr.grand_total, 1000) self.assertEqual(pr.grand_total, 1000)
so.load_from_db() so.load_from_db()
def test_partial_paid_invoice_with_payment_request(self):
si = create_sales_invoice(currency="INR", qty=1, rate=5000)
si.save()
si.submit()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PAYEE0002"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
si.load_from_db()
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
self.assertEqual(pr.grand_total, si.outstanding_amount)

View File

@@ -171,9 +171,7 @@ class PeriodClosingVoucher(AccountsController):
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss") pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
for dimensions, account_balances in pl_account_balances.items(): for dimensions, account_balances in pl_account_balances.items():
for acc, balances in account_balances.items(): for acc, balances in account_balances.items():
balance_in_company_currency = flt(balances.debit_in_account_currency) - flt( balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
balances.credit_in_account_currency
)
if balance_in_company_currency and acc != "balances": if balance_in_company_currency and acc != "balances":
self.pl_accounts_reverse_gle.append( self.pl_accounts_reverse_gle.append(
self.get_gle_for_pl_account(acc, balances, dimensions) self.get_gle_for_pl_account(acc, balances, dimensions)

View File

@@ -147,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", {
frm.doc.grand_total += flt(doc.grand_total); frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total); frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty); frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm); refresh_payments(doc, frm, false);
refresh_taxes(doc, frm); refresh_taxes(doc, frm);
refresh_fields(frm); refresh_fields(frm);
set_html_data(frm); set_html_data(frm);
@@ -172,7 +172,7 @@ function set_form_data(data, frm) {
frm.doc.grand_total += flt(d.grand_total); frm.doc.grand_total += flt(d.grand_total);
frm.doc.net_total += flt(d.net_total); frm.doc.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty); frm.doc.total_quantity += flt(d.total_qty);
refresh_payments(d, frm); refresh_payments(d, frm, true);
refresh_taxes(d, frm); refresh_taxes(d, frm);
}); });
} }
@@ -186,7 +186,7 @@ function add_to_pos_transaction(d, frm) {
}); });
} }
function refresh_payments(d, frm) { function refresh_payments(d, frm, is_new) {
d.payments.forEach((p) => { d.payments.forEach((p) => {
const payment = frm.doc.payment_reconciliation.find( const payment = frm.doc.payment_reconciliation.find(
(pay) => pay.mode_of_payment === p.mode_of_payment (pay) => pay.mode_of_payment === p.mode_of_payment
@@ -196,9 +196,7 @@ function refresh_payments(d, frm) {
} }
if (payment) { if (payment) {
payment.expected_amount += flt(p.amount); payment.expected_amount += flt(p.amount);
if (payment.closing_amount === 0) { if (is_new) payment.closing_amount = payment.expected_amount;
payment.closing_amount = payment.expected_amount;
}
payment.difference = payment.closing_amount - payment.expected_amount; payment.difference = payment.closing_amount - payment.expected_amount;
} else { } else {
frm.add_child("payment_reconciliation", { frm.add_child("payment_reconciliation", {

View File

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

View File

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

View File

@@ -474,6 +474,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
reference_doctype="Process Statement Of Accounts", reference_doctype="Process Statement Of Accounts",
reference_name=document_name, reference_name=document_name,
attachments=attachments, attachments=attachments,
expose_recipients="header",
) )
if doc.enable_auto_email and from_scheduler: if doc.enable_auto_email and from_scheduler:

View File

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

View File

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

View File

@@ -89,11 +89,14 @@
"incoming_rate", "incoming_rate",
"item_tax_rate", "item_tax_rate",
"actual_batch_qty", "actual_batch_qty",
"actual_qty",
"section_break_eoec", "section_break_eoec",
"serial_no", "serial_no",
"column_break_ytgd", "column_break_ytgd",
"batch_no", "batch_no",
"available_quantity_section",
"actual_qty",
"column_break_ogff",
"company_total_stock",
"edit_references", "edit_references",
"sales_order", "sales_order",
"so_detail", "so_detail",
@@ -675,7 +678,8 @@
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "actual_qty", "fieldname": "actual_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Available Qty at Warehouse", "label": "Qty (Warehouse)",
"no_copy": 1,
"oldfieldname": "actual_qty", "oldfieldname": "actual_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"print_hide": 1, "print_hide": 1,
@@ -923,12 +927,30 @@
{ {
"fieldname": "column_break_ytgd", "fieldname": "column_break_ytgd",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
},
{
"fieldname": "column_break_ogff",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-10-28 15:06:40.980995", "modified": "2024-11-25 16:27:33.287341",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -28,6 +28,7 @@ class SalesInvoiceItem(Document):
base_rate_with_margin: DF.Currency base_rate_with_margin: DF.Currency
batch_no: DF.Link | None batch_no: DF.Link | None
brand: DF.Data | None brand: DF.Data | None
company_total_stock: DF.Float
conversion_factor: DF.Float conversion_factor: DF.Float
cost_center: DF.Link cost_center: DF.Link
customer_item_code: DF.Data | None customer_item_code: DF.Data | None

View File

@@ -315,66 +315,48 @@ def check_if_in_list(gle, gl_map):
def toggle_debit_credit_if_negative(gl_map): def toggle_debit_credit_if_negative(gl_map):
debit_credit_field_map = {
"debit": "credit",
"debit_in_account_currency": "credit_in_account_currency",
"debit_in_transaction_currency": "credit_in_transaction_currency",
}
for entry in gl_map: for entry in gl_map:
# toggle debit, credit if negative entry # toggle debit, credit if negative entry
if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit): for debit_field, credit_field in debit_credit_field_map.items():
entry.credit *= -1 debit = flt(entry.get(debit_field))
entry.debit *= -1 credit = flt(entry.get(credit_field))
if ( if debit < 0 and credit < 0 and debit == credit:
flt(entry.debit_in_account_currency) < 0 debit *= -1
and flt(entry.credit_in_account_currency) < 0 credit *= -1
and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency)
):
entry.credit_in_account_currency *= -1
entry.debit_in_account_currency *= -1
if flt(entry.debit) < 0: if debit < 0:
entry.credit = flt(entry.credit) - flt(entry.debit) credit = credit - debit
entry.debit = 0.0 debit = 0.0
if flt(entry.debit_in_account_currency) < 0: if credit < 0:
entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt( debit = debit - credit
entry.debit_in_account_currency credit = 0.0
)
entry.debit_in_account_currency = 0.0
if flt(entry.credit) < 0: # update net values
entry.debit = flt(entry.debit) - flt(entry.credit) # In some scenarios net value needs to be shown in the ledger
entry.credit = 0.0 # This method updates net values as debit or credit
if entry.post_net_value and debit and credit:
if debit > credit:
debit = debit - credit
credit = 0.0
if flt(entry.credit_in_account_currency) < 0: else:
entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt( credit = credit - debit
entry.credit_in_account_currency debit = 0.0
)
entry.credit_in_account_currency = 0.0
update_net_values(entry) entry[debit_field] = debit
entry[credit_field] = credit
return gl_map return gl_map
def update_net_values(entry):
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and entry.debit and entry.credit:
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = (
entry.debit_in_account_currency - entry.credit_in_account_currency
)
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = (
entry.credit_in_account_currency - entry.debit_in_account_currency
)
entry.debit = 0
entry.debit_in_account_currency = 0
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost: if not from_repost:
validate_cwip_accounts(gl_map) validate_cwip_accounts(gl_map)

View File

@@ -4,13 +4,14 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Purchase Invoice", "document_type": "Purchase Invoice",
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", "filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
"function": "Sum", "function": "Sum",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"label": "Total Incoming Bills", "label": "Total Incoming Bills",
"modified": "2020-07-22 13:06:46.045344", "modified": "2024-11-20 19:08:37.043777",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Total Incoming Bills", "name": "Total Incoming Bills",

View File

@@ -4,6 +4,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Payment Entry", "document_type": "Payment Entry",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]", "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]",
"function": "Sum", "function": "Sum",
"idx": 0, "idx": 0,

View File

@@ -4,6 +4,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Sales Invoice", "document_type": "Sales Invoice",
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", "filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
"function": "Sum", "function": "Sum",
"idx": 0, "idx": 0,

View File

@@ -4,6 +4,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Payment Entry", "document_type": "Payment Entry",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]", "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]",
"function": "Sum", "function": "Sum",
"idx": 0, "idx": 0,

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Asset", "document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[]", "filters_json": "[]",
"function": "Sum", "function": "Sum",
"idx": 0, "idx": 0,

View File

@@ -3,6 +3,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Asset", "document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\",false]]", "filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\",false]]",
"function": "Count", "function": "Count",
"idx": 0, "idx": 0,

View File

@@ -3,6 +3,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Asset", "document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[]", "filters_json": "[]",
"function": "Count", "function": "Count",
"idx": 0, "idx": 0,

View File

@@ -93,7 +93,7 @@ frappe.ui.form.on("Purchase Order", {
get_materials_from_supplier: function (frm) { get_materials_from_supplier: function (frm) {
let po_details = []; let po_details = [];
if (frm.doc.supplied_items && (flt(frm.doc.per_received, 2) == 100 || frm.doc.status === "Closed")) { if (frm.doc.supplied_items && (flt(frm.doc.per_received) == 100 || frm.doc.status === "Closed")) {
frm.doc.supplied_items.forEach((d) => { frm.doc.supplied_items.forEach((d) => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name); po_details.push(d.name);
@@ -329,8 +329,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
if (!["Closed", "Delivered"].includes(doc.status)) { if (!["Closed", "Delivered"].includes(doc.status)) {
if ( if (
this.frm.doc.status !== "Closed" && this.frm.doc.status !== "Closed" &&
flt(this.frm.doc.per_received, 2) < 100 && flt(this.frm.doc.per_received) < 100 &&
flt(this.frm.doc.per_billed, 2) < 100 flt(this.frm.doc.per_billed) < 100
) { ) {
if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) { if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
this.frm.add_custom_button(__("Update Items"), () => { this.frm.add_custom_button(__("Update Items"), () => {
@@ -344,7 +344,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
} }
} }
if (this.frm.has_perm("submit")) { if (this.frm.has_perm("submit")) {
if (flt(doc.per_billed, 2) < 100 || flt(doc.per_received, 2) < 100) { if (flt(doc.per_billed) < 100 || flt(doc.per_received) < 100) {
if (doc.status != "On Hold") { if (doc.status != "On Hold") {
this.frm.add_custom_button( this.frm.add_custom_button(
__("Hold"), __("Hold"),
@@ -383,7 +383,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
if (doc.status != "Closed") { if (doc.status != "Closed") {
if (doc.status != "On Hold") { if (doc.status != "On Hold") {
if (flt(doc.per_received) < 100 && allow_receipt) { if (flt(doc.per_received) < 100 && allow_receipt) {
cur_frm.add_custom_button( this.frm.add_custom_button(
__("Purchase Receipt"), __("Purchase Receipt"),
this.make_purchase_receipt, this.make_purchase_receipt,
__("Create") __("Create")
@@ -408,14 +408,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
} }
} }
} }
// Please do not add precision in the below flt function
if (flt(doc.per_billed) < 100) if (flt(doc.per_billed) < 100)
cur_frm.add_custom_button( this.frm.add_custom_button(
__("Purchase Invoice"), __("Purchase Invoice"),
this.make_purchase_invoice, this.make_purchase_invoice,
__("Create") __("Create")
); );
if (flt(doc.per_billed, 2) < 100 && doc.status != "Delivered") { if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
this.frm.add_custom_button( this.frm.add_custom_button(
__("Payment"), __("Payment"),
() => this.make_payment_entry(), () => this.make_payment_entry(),
@@ -423,7 +424,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
); );
} }
if (flt(doc.per_billed, 2) < 100) { if (flt(doc.per_billed) < 100) {
this.frm.add_custom_button( this.frm.add_custom_button(
__("Payment Request"), __("Payment Request"),
function () { function () {

View File

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

View File

@@ -1036,7 +1036,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
available_serial_nos.append(serial_no) available_serial_nos.append(serial_no)
if available_serial_nos: if available_serial_nos:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]: if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse) available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
if len(available_serial_nos) > qty: if len(available_serial_nos) > qty:
@@ -1052,7 +1052,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
if batch_qty <= 0: if batch_qty <= 0:
continue continue
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]: if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
batch_qty = get_available_batch_qty( batch_qty = get_available_batch_qty(
parent_doc, parent_doc,
batch_no, batch_no,

View File

@@ -21,9 +21,15 @@ class SellingController(StockController):
def onload(self): def onload(self):
super().onload() super().onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice", "Quotation"):
for item in self.get("items") + (self.get("packed_items") or []): for item in self.get("items") + (self.get("packed_items") or []):
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) company = self.company
item.update(
get_bin_details(
item.item_code, item.warehouse, company=company, include_child_warehouses=True
)
)
def validate(self): def validate(self):
super().validate() super().validate()

View File

@@ -1008,11 +1008,13 @@ class StockController(AccountsController):
def validate_qi_presence(self, row): def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing.""" """Check if QI is present on row level. Warn on save and stop on submit if missing."""
if not row.quality_inspection: if not row.quality_inspection:
msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}" msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code)
)
if self.docstatus == 1: if self.docstatus == 1:
frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError) frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError)
else: else:
frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue") frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue")
def validate_qi_submission(self, row): def validate_qi_submission(self, row):
"""Check if QI is submitted on row level, during submission""" """Check if QI is submitted on row level, during submission"""
@@ -1021,11 +1023,13 @@ class StockController(AccountsController):
if not qa_docstatus == 1: if not qa_docstatus == 1:
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection) link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" msg = _("Row #{0}: Quality Inspection {1} is not submitted for the item: {2}").format(
row.idx, link, row.item_code
)
if action == "Stop": if action == "Stop":
frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
else: else:
frappe.msgprint(_(msg), alert=True, indicator="orange") frappe.msgprint(msg, alert=True, indicator="orange")
def validate_qi_rejection(self, row): def validate_qi_rejection(self, row):
"""Check if QI is rejected on row level, during submission""" """Check if QI is rejected on row level, during submission"""
@@ -1034,11 +1038,13 @@ class StockController(AccountsController):
if qa_status == "Rejected": if qa_status == "Rejected":
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection) link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" msg = _("Row #{0}: Quality Inspection {1} was rejected for item {2}").format(
row.idx, link, row.item_code
)
if action == "Stop": if action == "Stop":
frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
else: else:
frappe.msgprint(_(msg), alert=True, indicator="orange") frappe.msgprint(msg, alert=True, indicator="orange")
def update_blanket_order(self): def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))

View File

@@ -3,7 +3,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Opportunity", "document_type": "Opportunity",
"dynamic_filters_json": "[[\"Opportunity\",\"status\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]", "filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]",
"function": "Count", "function": "Count",
"idx": 0, "idx": 0,

View File

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

View File

@@ -25,6 +25,7 @@ class WorkOrderItem(Document):
item_code: DF.Link | None item_code: DF.Link | None
item_name: DF.Data | None item_name: DF.Data | None
operation: DF.Link | None operation: DF.Link | None
operation_row_id: DF.Int
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
@@ -32,6 +33,7 @@ class WorkOrderItem(Document):
required_qty: DF.Float required_qty: DF.Float
returned_qty: DF.Float returned_qty: DF.Float
source_warehouse: DF.Link | None source_warehouse: DF.Link | None
stock_uom: DF.Link | None
transferred_qty: DF.Float transferred_qty: DF.Float
# end: auto-generated types # end: auto-generated types

View File

@@ -3,6 +3,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Work Order", "document_type": "Work Order",
"dynamic_filters_json": "[[\"Work Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Work Order\",\"status\",\"=\",\"Completed\"],[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]", "filters_json": "[[\"Work Order\",\"status\",\"=\",\"Completed\"],[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]",
"function": "Count", "function": "Count",
"idx": 0, "idx": 0,

View File

@@ -3,6 +3,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Work Order", "document_type": "Work Order",
"dynamic_filters_json": "[[\"Work Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]", "filters_json": "[[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]",
"function": "Count", "function": "Count",
"idx": 0, "idx": 0,

View File

@@ -3,6 +3,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Job Card", "document_type": "Job Card",
"dynamic_filters_json": "[[\"Job Card\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Job Card\",\"status\",\"!=\",\"Completed\"],[\"Job Card\",\"docstatus\",\"=\",1]]", "filters_json": "[[\"Job Card\",\"status\",\"!=\",\"Completed\"],[\"Job Card\",\"docstatus\",\"=\",1]]",
"function": "Count", "function": "Count",
"idx": 0, "idx": 0,

View File

@@ -382,3 +382,4 @@ erpnext.patches.v15_0.link_purchase_item_to_asset_doc
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log
erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries
erpnext.patches.v14_0.update_stock_uom_in_work_order_item

View File

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

View File

@@ -1904,8 +1904,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
callback: function(r) { callback: function(r) {
if (!r.exc) { if (!r.exc) {
frappe.run_serially([ frappe.run_serially([
() => me.frm.set_value("price_list_currency", r.message.parent.price_list_currency), () => {
() => me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate), if (r.message.parent.price_list_currency)
me.frm.set_value("price_list_currency", r.message.parent.price_list_currency);
},
() => {
if (r.message.parent.plc_conversion_rate)
me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate);
},
() => { () => {
if(args.items.length) { if(args.items.length) {
me._set_values_for_item_list(r.message.children); me._set_values_for_item_list(r.message.children);

View File

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

View File

@@ -52,6 +52,7 @@ erpnext.sales_common = {
me.frm.set_query("customer_address", erpnext.queries.address_query); me.frm.set_query("customer_address", erpnext.queries.address_query);
me.frm.set_query("shipping_address_name", erpnext.queries.address_query); me.frm.set_query("shipping_address_name", erpnext.queries.address_query);
me.frm.set_query("dispatch_address_name", erpnext.queries.dispatch_address_query); me.frm.set_query("dispatch_address_name", erpnext.queries.dispatch_address_query);
me.frm.set_query("company_address", erpnext.queries.company_address_query);
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);

View File

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.utils import get_link_to_form from frappe.utils import get_link_to_form
@@ -93,15 +94,24 @@ class ProductBundle(Document):
def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name") product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name")
if not searchfield or searchfield == "name":
searchfield = frappe.get_meta("Item").get("search_fields")
searchfield = searchfield.split(",")
searchfield.append("name")
item = frappe.qb.DocType("Item") item = frappe.qb.DocType("Item")
query = ( query = (
frappe.qb.from_(item) frappe.qb.from_(item)
.select(item.item_code, item.item_name) .select(item.name, item.item_name)
.where((item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%"))) .where((item.is_stock_item == 0) & (item.is_fixed_asset == 0))
.limit(page_len) .limit(page_len)
.offset(start) .offset(start)
) )
if searchfield:
query = query.where(Criterion.any([item[fieldname].like(f"%{txt}%") for fieldname in searchfield]))
if product_bundles: if product_bundles:
query = query.where(item.name.notin(product_bundles)) query = query.where(item.name.notin(product_bundles))

View File

@@ -24,20 +24,6 @@ frappe.ui.form.on("Quotation", {
frm.set_df_property("packed_items", "cannot_add_rows", true); frm.set_df_property("packed_items", "cannot_add_rows", true);
frm.set_df_property("packed_items", "cannot_delete_rows", true); frm.set_df_property("packed_items", "cannot_delete_rows", true);
frm.set_query("company_address", function (doc) {
if (!doc.company) {
frappe.throw(__("Please set Company"));
}
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: "Company",
link_name: doc.company,
},
};
});
frm.set_query("serial_and_batch_bundle", "packed_items", (doc, cdt, cdn) => { frm.set_query("serial_and_batch_bundle", "packed_items", (doc, cdt, cdn) => {
let row = locals[cdt][cdn]; let row = locals[cdt][cdn];
return { return {

View File

@@ -24,6 +24,10 @@
"uom", "uom",
"conversion_factor", "conversion_factor",
"stock_qty", "stock_qty",
"available_quantity_section",
"actual_qty",
"column_break_ylrv",
"company_total_stock",
"section_break_16", "section_break_16",
"price_list_rate", "price_list_rate",
"base_price_list_rate", "base_price_list_rate",
@@ -70,7 +74,6 @@
"prevdoc_docname", "prevdoc_docname",
"item_balance", "item_balance",
"projected_qty", "projected_qty",
"actual_qty",
"col_break4", "col_break4",
"stock_balance", "stock_balance",
"item_tax_rate", "item_tax_rate",
@@ -460,9 +463,10 @@
"report_hide": 1 "report_hide": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "actual_qty", "fieldname": "actual_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Actual Qty", "label": "Qty (Warehouse)",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
@@ -662,12 +666,31 @@
"label": "Has Alternative Item", "label": "Has Alternative Item",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
},
{
"fieldname": "column_break_ylrv",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"report_hide": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-14 18:24:24.619832", "modified": "2024-11-24 15:18:43.952844",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation Item", "name": "Quotation Item",
@@ -677,4 +700,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -27,6 +27,7 @@ class QuotationItem(Document):
blanket_order: DF.Link | None blanket_order: DF.Link | None
blanket_order_rate: DF.Currency blanket_order_rate: DF.Currency
brand: DF.Link | None brand: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float conversion_factor: DF.Float
customer_item_code: DF.Data | None customer_item_code: DF.Data | None
description: DF.TextEditor | None description: DF.TextEditor | None

View File

@@ -26,20 +26,6 @@ frappe.ui.form.on("Sales Order", {
return doc.stock_qty <= doc.delivered_qty ? "green" : "orange"; return doc.stock_qty <= doc.delivered_qty ? "green" : "orange";
}); });
frm.set_query("company_address", function (doc) {
if (!doc.company) {
frappe.throw(__("Please set Company"));
}
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: "Company",
link_name: doc.company,
},
};
});
frm.set_query("bom_no", "items", function (doc, cdt, cdn) { frm.set_query("bom_no", "items", function (doc, cdt, cdn) {
var row = locals[cdt][cdn]; var row = locals[cdt][cdn];
return { return {
@@ -57,8 +43,8 @@ frappe.ui.form.on("Sales Order", {
if (frm.doc.docstatus === 1) { if (frm.doc.docstatus === 1) {
if ( if (
frm.doc.status !== "Closed" && frm.doc.status !== "Closed" &&
flt(frm.doc.per_delivered, 2) < 100 && flt(frm.doc.per_delivered) < 100 &&
flt(frm.doc.per_billed, 2) < 100 && flt(frm.doc.per_billed) < 100 &&
frm.has_perm("write") frm.has_perm("write")
) { ) {
frm.add_custom_button(__("Update Items"), () => { frm.add_custom_button(__("Update Items"), () => {
@@ -604,7 +590,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
__("Status") __("Status")
); );
if (flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) { if (flt(doc.per_delivered) < 100 || flt(doc.per_billed) < 100) {
// close // close
this.frm.add_custom_button(__("Close"), () => this.close_sales_order(), __("Status")); this.frm.add_custom_button(__("Close"), () => this.close_sales_order(), __("Status"));
} }
@@ -627,7 +613,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
) && !this.frm.doc.skip_delivery_note; ) && !this.frm.doc.skip_delivery_note;
if (this.frm.has_perm("submit")) { if (this.frm.has_perm("submit")) {
if (flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) { if (flt(doc.per_delivered) < 100 || flt(doc.per_billed) < 100) {
// hold // hold
this.frm.add_custom_button( this.frm.add_custom_button(
__("Hold"), __("Hold"),
@@ -645,8 +631,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
if ( if (
(!doc.__onload || !doc.__onload.has_reserved_stock) && (!doc.__onload || !doc.__onload.has_reserved_stock) &&
flt(doc.per_picked, 2) < 100 && flt(doc.per_picked) < 100 &&
flt(doc.per_delivered, 2) < 100 && flt(doc.per_delivered) < 100 &&
frappe.model.can_create("Pick List") frappe.model.can_create("Pick List")
) { ) {
this.frm.add_custom_button( this.frm.add_custom_button(
@@ -664,7 +650,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
// delivery note // delivery note
if ( if (
flt(doc.per_delivered, 2) < 100 && flt(doc.per_delivered) < 100 &&
(order_is_a_sale || order_is_a_custom_sale) && (order_is_a_sale || order_is_a_custom_sale) &&
allow_delivery allow_delivery
) { ) {
@@ -686,7 +672,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} }
// sales invoice // sales invoice
if (flt(doc.per_billed, 2) < 100 && frappe.model.can_create("Sales Invoice")) { if (flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice")) {
this.frm.add_custom_button( this.frm.add_custom_button(
__("Sales Invoice"), __("Sales Invoice"),
() => me.make_sales_invoice(), () => me.make_sales_invoice(),
@@ -697,8 +683,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
// material request // material request
if ( if (
(!doc.order_type || (!doc.order_type ||
((order_is_a_sale || order_is_a_custom_sale) && ((order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered) < 100)) &&
flt(doc.per_delivered, 2) < 100)) &&
frappe.model.can_create("Material Request") frappe.model.can_create("Material Request")
) { ) {
this.frm.add_custom_button( this.frm.add_custom_button(
@@ -723,7 +708,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} }
// maintenance // maintenance
if (flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) { if (flt(doc.per_delivered) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
if (frappe.model.can_create("Maintenance Visit")) { if (frappe.model.can_create("Maintenance Visit")) {
this.frm.add_custom_button( this.frm.add_custom_button(
__("Maintenance Visit"), __("Maintenance Visit"),
@@ -741,7 +726,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} }
// project // project
if (flt(doc.per_delivered, 2) < 100 && frappe.model.can_create("Project")) { if (flt(doc.per_delivered) < 100 && frappe.model.can_create("Project")) {
this.frm.add_custom_button(__("Project"), () => this.make_project(), __("Create")); this.frm.add_custom_button(__("Project"), () => this.make_project(), __("Create"));
} }
@@ -769,10 +754,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} }
} }
// payment request // payment request
if ( if (flt(doc.per_billed) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
flt(doc.per_billed, precision("per_billed", doc)) <
100 + frappe.boot.sysdefaults.over_billing_allowance
) {
this.frm.add_custom_button( this.frm.add_custom_button(
__("Payment Request"), __("Payment Request"),
() => this.make_payment_request(), () => this.make_payment_request(),

View File

@@ -20,14 +20,14 @@ frappe.listview_settings["Sales Order"] = {
return [__("On Hold"), "orange", "status,=,On Hold"]; return [__("On Hold"), "orange", "status,=,On Hold"];
} else if (doc.status === "Completed") { } else if (doc.status === "Completed") {
return [__("Completed"), "green", "status,=,Completed"]; return [__("Completed"), "green", "status,=,Completed"];
} else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) { } else if (!doc.skip_delivery_note && flt(doc.per_delivered) < 100) {
if (frappe.datetime.get_diff(doc.delivery_date) < 0) { if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
// not delivered & overdue // not delivered & overdue
return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"]; return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
} else if (flt(doc.grand_total) === 0) { } else if (flt(doc.grand_total) === 0) {
// not delivered (zeroount order) // not delivered (zeroount order)
return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"]; return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
} else if (flt(doc.per_billed, 2) < 100) { } else if (flt(doc.per_billed) < 100) {
// not delivered & not billed // not delivered & not billed
return [ return [
__("To Deliver and Bill"), __("To Deliver and Bill"),
@@ -39,13 +39,13 @@ frappe.listview_settings["Sales Order"] = {
return [__("To Deliver"), "orange", "per_delivered,<,100|per_billed,=,100|status,!=,Closed"]; return [__("To Deliver"), "orange", "per_delivered,<,100|per_billed,=,100|status,!=,Closed"];
} }
} else if ( } else if (
flt(doc.per_delivered, 2) === 100 && flt(doc.per_delivered) === 100 &&
flt(doc.grand_total) !== 0 && flt(doc.grand_total) !== 0 &&
flt(doc.per_billed, 2) < 100 flt(doc.per_billed) < 100
) { ) {
// to bill // to bill
return [__("To Bill"), "orange", "per_delivered,=,100|per_billed,<,100|status,!=,Closed"]; return [__("To Bill"), "orange", "per_delivered,=,100|per_billed,<,100|status,!=,Closed"];
} else if (doc.skip_delivery_note && flt(doc.per_billed, 2) < 100) { } else if (doc.skip_delivery_note && flt(doc.per_billed) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100|status,!=,Closed"]; return [__("To Bill"), "orange", "per_billed,<,100|status,!=,Closed"];
} }
}, },

View File

@@ -78,11 +78,14 @@
"against_blanket_order", "against_blanket_order",
"blanket_order", "blanket_order",
"blanket_order_rate", "blanket_order_rate",
"available_quantity_section",
"actual_qty",
"column_break_jpky",
"company_total_stock",
"manufacturing_section_section", "manufacturing_section_section",
"bom_no", "bom_no",
"planning_section", "planning_section",
"projected_qty", "projected_qty",
"actual_qty",
"ordered_qty", "ordered_qty",
"planned_qty", "planned_qty",
"production_plan_qty", "production_plan_qty",
@@ -636,7 +639,7 @@
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "actual_qty", "fieldname": "actual_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Actual Qty", "label": "Qty (Warehouse)",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"print_width": "70px", "print_width": "70px",
@@ -905,12 +908,30 @@
"label": "Is Stock Item", "label": "Is Stock Item",
"print_hide": 1, "print_hide": 1,
"report_hide": 1 "report_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_jpky",
"fieldtype": "Column Break"
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-21 18:15:56.625005", "modified": "2024-11-21 14:21:29.743474",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",
@@ -921,4 +942,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -30,6 +30,7 @@ class SalesOrderItem(Document):
blanket_order_rate: DF.Currency blanket_order_rate: DF.Currency
bom_no: DF.Link | None bom_no: DF.Link | None
brand: DF.Link | None brand: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float conversion_factor: DF.Float
customer_item_code: DF.Data | None customer_item_code: DF.Data | None
delivered_by_supplier: DF.Check delivered_by_supplier: DF.Check

View File

@@ -3,7 +3,6 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Customer", "document_type": "Customer",
"dynamic_filters_json": "",
"filters_json": "[[\"Customer\",\"disabled\",\"=\",\"0\"]]", "filters_json": "[[\"Customer\",\"disabled\",\"=\",\"0\"]]",
"function": "Count", "function": "Count",
"idx": 0, "idx": 0,

View File

@@ -96,6 +96,8 @@ def search_by_term(search_term, warehouse, price_list):
def filter_result_items(result, pos_profile): def filter_result_items(result, pos_profile):
if result and result.get("items"): if result and result.get("items"):
pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group") pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group")
if not pos_item_groups:
return
result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups] result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups]

View File

@@ -556,7 +556,7 @@ erpnext.PointOfSale.Controller = class {
const item_row_exists = !$.isEmptyObject(item_row); const item_row_exists = !$.isEmptyObject(item_row);
const from_selector = field === "qty" && value === "+1"; const from_selector = field === "qty" && value === "+1";
if (from_selector) value = flt(item_row.stock_qty) + flt(value); if (from_selector) value = flt(item_row.qty) + flt(value);
if (item_row_exists) { if (item_row_exists) {
if (field === "qty") value = flt(value); if (field === "qty") value = flt(value);
@@ -687,7 +687,7 @@ erpnext.PointOfSale.Controller = class {
const is_stock_item = resp[1]; const is_stock_item = resp[1];
frappe.dom.unfreeze(); frappe.dom.unfreeze();
const bold_uom = item_row.uom.bold(); const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold(); const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold(); const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold(); const bold_available_qty = available_qty.toString().bold();

View File

@@ -73,6 +73,11 @@ frappe.query_reports["Sales Analytics"] = {
default: "Monthly", default: "Monthly",
reqd: 1, reqd: 1,
}, },
{
fieldname: "show_aggregate_value_from_subsidiary_companies",
label: __("Show Aggregate Value from Subsidiary Companies"),
fieldtype: "Check",
},
], ],
get_datatable_options(options) { get_datatable_options(options) {
return Object.assign(options, { return Object.assign(options, {

View File

@@ -4,6 +4,8 @@
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.query_builder import DocType
from frappe.query_builder.functions import IfNull
from frappe.utils import add_days, add_to_date, flt, getdate from frappe.utils import add_days, add_to_date, flt, getdate
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
@@ -37,7 +39,26 @@ class Analytics:
] ]
self.get_period_date_ranges() self.get_period_date_ranges()
def update_company_list_for_parent_company(self):
company_list = [self.filters.get("company")]
selected_company = self.filters.get("company")
if (
selected_company
and self.filters.get("show_aggregate_value_from_subsidiary_companies")
and frappe.db.get_value("Company", selected_company, "is_group")
):
lft, rgt = frappe.db.get_value("Company", selected_company, ["lft", "rgt"])
child_companies = frappe.db.get_list(
"Company", filters={"lft": [">", lft], "rgt": ["<", rgt]}, pluck="name"
)
company_list.extend(child_companies)
self.filters["company"] = company_list
def run(self): def run(self):
self.update_company_list_for_parent_company()
self.get_columns() self.get_columns()
self.get_data() self.get_data()
self.get_chart_data() self.get_chart_data()
@@ -123,14 +144,23 @@ class Analytics:
else: else:
value_field = "total_qty" value_field = "total_qty"
self.entries = frappe.db.sql( doctype = DocType(self.filters.doc_type)
""" select s.order_type as entity, s.{value_field} as value_field, s.{date_field}
from `tab{doctype}` s where s.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s self.entries = (
and ifnull(s.order_type, '') != '' order by s.order_type frappe.qb.from_(doctype)
""".format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), .select(
(self.filters.company, self.filters.from_date, self.filters.to_date), doctype.order_type.as_("entity"),
as_dict=1, doctype[self.date_field],
) doctype[value_field].as_("value_field"),
)
.where(
(doctype.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
& (IfNull(doctype.order_type, "") != "")
)
.orderby(doctype.order_type)
).run(as_dict=True)
self.get_teams() self.get_teams()
@@ -152,7 +182,7 @@ class Analytics:
fields=[entity, entity_name, value_field, self.date_field], fields=[entity, entity_name, value_field, self.date_field],
filters={ filters={
"docstatus": 1, "docstatus": 1,
"company": self.filters.company, "company": ["in", self.filters.company],
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
}, },
) )
@@ -167,16 +197,26 @@ class Analytics:
else: else:
value_field = "stock_qty" value_field = "stock_qty"
self.entries = frappe.db.sql( doctype = DocType(self.filters.doc_type)
""" doctype_item = DocType(f"{self.filters.doc_type} Item")
select i.item_code as entity, i.item_name as entity_name, i.stock_uom, i.{value_field} as value_field, s.{date_field}
from `tab{doctype} Item` i , `tab{doctype}` s self.entries = (
where s.name = i.parent and i.docstatus = 1 and s.company = %s frappe.qb.from_(doctype_item)
and s.{date_field} between %s and %s .join(doctype)
""".format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), .on(doctype.name == doctype_item.parent)
(self.filters.company, self.filters.from_date, self.filters.to_date), .select(
as_dict=1, doctype_item.item_code.as_("entity"),
) doctype_item.item_name.as_("entity_name"),
doctype_item.stock_uom,
doctype_item[value_field].as_("value_field"),
doctype[self.date_field],
)
.where(
(doctype_item.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
)
).run(as_dict=True)
self.entity_names = {} self.entity_names = {}
for d in self.entries: for d in self.entries:
@@ -201,7 +241,7 @@ class Analytics:
fields=[entity_field, value_field, self.date_field], fields=[entity_field, value_field, self.date_field],
filters={ filters={
"docstatus": 1, "docstatus": 1,
"company": self.filters.company, "company": ["in", self.filters.company],
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
}, },
) )
@@ -213,16 +253,24 @@ class Analytics:
else: else:
value_field = "qty" value_field = "qty"
self.entries = frappe.db.sql( doctype = DocType(self.filters.doc_type)
f""" doctype_item = DocType(f"{self.filters.doc_type} Item")
select i.item_group as entity, i.{value_field} as value_field, s.{self.date_field}
from `tab{self.filters.doc_type} Item` i , `tab{self.filters.doc_type}` s self.entries = (
where s.name = i.parent and i.docstatus = 1 and s.company = %s frappe.qb.from_(doctype_item)
and s.{self.date_field} between %s and %s .join(doctype)
""", .on(doctype.name == doctype_item.parent)
(self.filters.company, self.filters.from_date, self.filters.to_date), .select(
as_dict=1, doctype_item.item_group.as_("entity"),
) doctype_item[value_field].as_("value_field"),
doctype[self.date_field],
)
.where(
(doctype_item.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
)
).run(as_dict=True)
self.get_groups() self.get_groups()
@@ -239,7 +287,7 @@ class Analytics:
fields=[entity, value_field, self.date_field], fields=[entity, value_field, self.date_field],
filters={ filters={
"docstatus": 1, "docstatus": 1,
"company": self.filters.company, "company": ["in", self.filters.company],
"project": ["!=", ""], "project": ["!=", ""],
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
}, },
@@ -312,7 +360,7 @@ class Analytics:
str(((posting_date.month - 1) // 3) + 1), str(posting_date.year) str(((posting_date.month - 1) // 3) + 1), str(posting_date.year)
) )
else: else:
year = get_fiscal_year(posting_date, company=self.filters.company) year = get_fiscal_year(posting_date, company=self.filters.company[0])
period = str(year[0]) period = str(year[0])
return period return period

View File

@@ -87,16 +87,19 @@
"column_break_rxvc", "column_break_rxvc",
"batch_no", "batch_no",
"available_qty_section", "available_qty_section",
"actual_batch_qty",
"actual_qty", "actual_qty",
"installed_qty", "actual_batch_qty",
"item_tax_rate",
"column_break_atna", "column_break_atna",
"company_total_stock",
"section_break_kejd",
"installed_qty",
"packed_qty", "packed_qty",
"column_break_fguf",
"received_qty", "received_qty",
"accounting_details_section", "accounting_details_section",
"expense_account", "expense_account",
"column_break_71", "column_break_71",
"item_tax_rate",
"internal_transfer_section", "internal_transfer_section",
"material_request", "material_request",
"purchase_order", "purchase_order",
@@ -519,7 +522,7 @@
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "actual_qty", "fieldname": "actual_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Available Qty at From Warehouse", "label": "Qty (Warehouse)",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "actual_qty", "oldfieldname": "actual_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
@@ -907,13 +910,30 @@
{ {
"fieldname": "column_break_rxvc", "fieldname": "column_break_rxvc",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_kejd",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_fguf",
"fieldtype": "Column Break"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-21 18:15:07.603672", "modified": "2024-11-21 17:37:37.441498",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",
@@ -923,4 +943,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -30,6 +30,7 @@ class DeliveryNoteItem(Document):
batch_no: DF.Link | None batch_no: DF.Link | None
billed_amt: DF.Currency billed_amt: DF.Currency
brand: DF.Link | None brand: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float conversion_factor: DF.Float
cost_center: DF.Link | None cost_center: DF.Link | None
customer_item_code: DF.Data | None customer_item_code: DF.Data | None

View File

@@ -663,39 +663,41 @@ $.extend(erpnext.item, {
} }
frm.doc.attributes.forEach(function (d) { frm.doc.attributes.forEach(function (d) {
let p = new Promise((resolve) => { if (!d.disabled) {
if (!d.numeric_values) { let p = new Promise((resolve) => {
frappe if (!d.numeric_values) {
.call({ frappe
method: "frappe.client.get_list", .call({
args: { method: "frappe.client.get_list",
doctype: "Item Attribute Value", args: {
filters: [["parent", "=", d.attribute]], doctype: "Item Attribute Value",
fields: ["attribute_value"], filters: [["parent", "=", d.attribute]],
limit_page_length: 0, fields: ["attribute_value"],
parent: "Item Attribute", limit_page_length: 0,
order_by: "idx", parent: "Item Attribute",
}, order_by: "idx",
}) },
.then((r) => { })
if (r.message) { .then((r) => {
attr_val_fields[d.attribute] = r.message.map(function (d) { if (r.message) {
return d.attribute_value; attr_val_fields[d.attribute] = r.message.map(function (d) {
}); return d.attribute_value;
resolve(); });
} resolve();
}); }
} else { });
let values = []; } else {
for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) { let values = [];
values.push(i); for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) {
values.push(i);
}
attr_val_fields[d.attribute] = values;
resolve();
} }
attr_val_fields[d.attribute] = values; });
resolve();
}
});
promises.push(p); promises.push(p);
}
}, this); }, this);
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
@@ -710,26 +712,29 @@ $.extend(erpnext.item, {
for (var i = 0; i < frm.doc.attributes.length; i++) { for (var i = 0; i < frm.doc.attributes.length; i++) {
var fieldtype, desc; var fieldtype, desc;
var row = frm.doc.attributes[i]; var row = frm.doc.attributes[i];
if (row.numeric_values) {
fieldtype = "Float"; if (!row.disabled) {
desc = if (row.numeric_values) {
"Min Value: " + fieldtype = "Float";
row.from_range + desc =
" , Max Value: " + "Min Value: " +
row.to_range + row.from_range +
", in Increments of: " + " , Max Value: " +
row.increment; row.to_range +
} else { ", in Increments of: " +
fieldtype = "Data"; row.increment;
desc = ""; } else {
fieldtype = "Data";
desc = "";
}
fields = fields.concat({
label: row.attribute,
fieldname: row.attribute,
fieldtype: fieldtype,
reqd: 0,
description: desc,
});
} }
fields = fields.concat({
label: row.attribute,
fieldname: row.attribute,
fieldtype: fieldtype,
reqd: 0,
description: desc,
});
} }
if (frm.doc.image) { if (frm.doc.image) {

View File

@@ -10,6 +10,8 @@
"field_order": [ "field_order": [
"attribute_name", "attribute_name",
"numeric_values", "numeric_values",
"column_break_vbik",
"disabled",
"section_break_4", "section_break_4",
"from_range", "from_range",
"increment", "increment",
@@ -70,15 +72,26 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Item Attribute Values", "label": "Item Attribute Values",
"options": "Item Attribute Value" "options": "Item Attribute Value"
},
{
"fieldname": "column_break_vbik",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
} }
], ],
"icon": "fa fa-edit", "icon": "fa fa-edit",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-10-02 12:03:02.359202", "modified": "2024-11-26 20:05:29.421714",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Attribute", "name": "Item Attribute",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -94,4 +107,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -30,6 +30,7 @@ class ItemAttribute(Document):
from erpnext.stock.doctype.item_attribute_value.item_attribute_value import ItemAttributeValue from erpnext.stock.doctype.item_attribute_value.item_attribute_value import ItemAttributeValue
attribute_name: DF.Data attribute_name: DF.Data
disabled: DF.Check
from_range: DF.Float from_range: DF.Float
increment: DF.Float increment: DF.Float
item_attribute_values: DF.Table[ItemAttributeValue] item_attribute_values: DF.Table[ItemAttributeValue]
@@ -47,6 +48,19 @@ class ItemAttribute(Document):
def on_update(self): def on_update(self):
self.validate_exising_items() self.validate_exising_items()
self.set_enabled_disabled_in_items()
def set_enabled_disabled_in_items(self):
db_value = self.get_doc_before_save()
if not db_value or db_value.disabled != self.disabled:
item_variant_table = frappe.qb.DocType("Item Variant Attribute")
query = (
frappe.qb.update(item_variant_table)
.set(item_variant_table.disabled, self.disabled)
.where(item_variant_table.attribute == self.name)
)
query.run()
def validate_exising_items(self): def validate_exising_items(self):
"""Validate that if there are existing items with attributes, they are valid""" """Validate that if there are existing items with attributes, they are valid"""

View File

@@ -11,6 +11,7 @@
"column_break_2", "column_break_2",
"attribute_value", "attribute_value",
"numeric_values", "numeric_values",
"disabled",
"section_break_4", "section_break_4",
"from_range", "from_range",
"increment", "increment",
@@ -74,11 +75,18 @@
"fieldname": "to_range", "fieldname": "to_range",
"fieldtype": "Float", "fieldtype": "Float",
"label": "To Range" "label": "To Range"
},
{
"default": "0",
"fetch_from": "attribute.disabled",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-07-14 17:15:19.112119", "modified": "2024-11-26 20:10:49.873339",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Variant Attribute", "name": "Item Variant Attribute",
@@ -87,4 +95,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -16,6 +16,7 @@ class ItemVariantAttribute(Document):
attribute: DF.Link attribute: DF.Link
attribute_value: DF.Data | None attribute_value: DF.Data | None
disabled: DF.Check
from_range: DF.Float from_range: DF.Float
increment: DF.Float increment: DF.Float
numeric_values: DF.Check numeric_values: DF.Check

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, flt, get_number_format_info from frappe.utils import cint, cstr, flt, get_link_to_form, 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,
@@ -73,6 +73,27 @@ class QualityInspection(Document):
if self.readings: if self.readings:
self.inspect_and_set_status() self.inspect_and_set_status()
self.validate_inspection_required()
def validate_inspection_required(self):
if self.reference_type in ["Purchase Receipt", "Purchase Invoice"] and not frappe.get_cached_value(
"Item", self.item_code, "inspection_required_before_purchase"
):
frappe.throw(
_(
"'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI"
).format(get_link_to_form("Item", self.item_code))
)
if self.reference_type in ["Delivery Note", "Sales Invoice"] and not frappe.get_cached_value(
"Item", self.item_code, "inspection_required_before_delivery"
):
frappe.throw(
_(
"'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI"
).format(get_link_to_form("Item", self.item_code))
)
def before_submit(self): def before_submit(self):
self.validate_readings_status_mandatory() self.validate_readings_status_mandatory()

View File

@@ -940,11 +940,12 @@ def get_batch_based_item_price(params, item_code) -> float:
params = parse_json(params) params = parse_json(params)
item_price = get_item_price(params, item_code, force_batch_no=True) item_price = get_item_price(params, item_code, force_batch_no=True)
if not item_price: if not item_price:
item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True) item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True)
if item_price and item_price[0].uom == params.get("uom"): if item_price and item_price[0][2] == params.get("uom"):
return item_price[0].price_list_rate return item_price[0][1]
return 0.0 return 0.0

View File

@@ -3,6 +3,7 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Number Card", "doctype": "Number Card",
"document_type": "Warehouse", "document_type": "Warehouse",
"dynamic_filters_json": "[[\"Warehouse\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Warehouse\",\"disabled\",\"=\",0]]", "filters_json": "[[\"Warehouse\",\"disabled\",\"=\",0]]",
"function": "Count", "function": "Count",
"idx": 0, "idx": 0,