Merge branch 'develop' into unit-price-contract-2

This commit is contained in:
Marica
2025-04-28 16:15:41 +05:30
committed by GitHub
116 changed files with 76874 additions and 92108 deletions

View File

@@ -1,5 +1,5 @@
exclude: 'node_modules|.git'
default_stages: [commit]
default_stages: [pre-commit]
fail_fast: false

View File

@@ -7,6 +7,7 @@
<p>Powerful, Intuitive and Open-Source ERP</p>
</p>
[![Learn on Frappe School](https://img.shields.io/badge/Frappe%20School-Learn%20ERPNext-blue?style=flat-square)](https://frappe.school)<br><br>
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml/badge.svg?event=schedule)](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker)
@@ -141,6 +142,7 @@ To setup the repository locally follow the steps mentioned below:
1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
1. [Report Security Vulnerabilities](https://erpnext.com/security)
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
2. [Translations](https://crowdin.com/project/frappe)
## Logo and Trademark Policy

View File

@@ -58,6 +58,8 @@
"pos_tab",
"pos_setting_section",
"post_change_gl_entries",
"column_break_xrnd",
"use_sales_invoice_in_pos",
"assets_tab",
"asset_settings_section",
"calculate_depr_using_total_days",
@@ -532,14 +534,26 @@
"fieldtype": "Select",
"label": "Posting Date Inheritance for Exchange Gain / Loss",
"options": "Invoice\nPayment\nReconciliation Date"
},
{
"fieldname": "column_break_xrnd",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, Sales Invoice will be generated instead of POS Invoice in POS Transactions for real-time update of G/L and Stock Ledger.",
"fieldname": "use_sales_invoice_in_pos",
"fieldtype": "Check",
"label": "Use Sales Invoice"
}
],
"grid_page_length": 50,
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-01-23 13:15:44.077853",
"modified": "2025-03-30 20:47:17.954736",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -564,8 +578,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -66,6 +66,7 @@ class AccountsSettings(Document):
submit_journal_entries: DF.Check
unlink_advance_payment_on_cancelation_of_order: DF.Check
unlink_payment_on_cancellation_of_invoice: DF.Check
use_sales_invoice_in_pos: DF.Check
# end: auto-generated types
def validate(self):
@@ -92,6 +93,9 @@ class AccountsSettings(Document):
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
self.validate_pending_reposts()
if old_doc.use_sales_invoice_in_pos != self.use_sales_invoice_in_pos:
self.validate_invoice_mode_switch_in_pos()
if clear_cache:
frappe.clear_cache()
@@ -135,3 +139,15 @@ class AccountsSettings(Document):
if self.has_value_changed("reconciliation_queue_size"):
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
frappe.throw(_("Queue Size should be between 5 and 100"))
def validate_invoice_mode_switch_in_pos(self):
pos_opening_entries_count = frappe.db.count(
"POS Opening Entry", filters={"docstatus": 1, "status": "Open"}
)
if pos_opening_entries_count:
frappe.throw(
_("{0} can be enabled/disabled after all the POS Opening Entries are closed.").format(
frappe.bold(_("Use Sales Invoice"))
),
title=_("Switch Invoice Mode Error"),
)

View File

@@ -1,6 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import json
import frappe
from frappe.model import mapper
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import add_days, nowdate, today
@@ -77,6 +80,36 @@ class TestDunning(IntegrationTestCase):
dunning.reload()
self.assertEqual(dunning.status, "Resolved")
def test_fetch_overdue_payments(self):
"""
Create SI with overdue payment. Check if overdue payment is fetched in Dunning.
"""
si1 = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -1 * 6),
qty=1,
rate=100,
)
si2 = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -1 * 6),
qty=1,
rate=300,
)
dunning = create_dunning_from_sales_invoice(si1.name)
dunning.overdue_payments = []
method = "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning"
updated_dunning = mapper.map_docs(method, json.dumps([si1.name, si2.name]), dunning)
self.assertEqual(len(updated_dunning.overdue_payments), 2)
self.assertEqual(updated_dunning.overdue_payments[0].sales_invoice, si1.name)
self.assertEqual(updated_dunning.overdue_payments[0].outstanding, si1.outstanding_amount)
self.assertEqual(updated_dunning.overdue_payments[1].sales_invoice, si2.name)
self.assertEqual(updated_dunning.overdue_payments[1].outstanding, si2.outstanding_amount)
def test_dunning_and_payment_against_partially_due_invoice(self):
"""
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.

View File

@@ -3025,6 +3025,8 @@ def get_payment_entry(
party_account_currency if payment_type == "Receive" else bank.account_currency
)
pe.paid_to_account_currency = party_account_currency if payment_type == "Pay" else bank.account_currency
pe.paid_from_account_type = frappe.db.get_value("Account", pe.paid_from, "account_type")
pe.paid_to_account_type = frappe.db.get_value("Account", pe.paid_to, "account_type")
pe.paid_amount = paid_amount
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
@@ -3388,26 +3390,25 @@ def set_paid_amount_and_received_amount(
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
else:
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
if payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
if bank and company_currency != bank.account_currency:
received_amount = paid_amount / doc.get("conversion_rate", 1)
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
# settings if it is for receive
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
if bank and company_currency != bank.account_currency:
# doc currency can be different from bank currency
posting_date = doc.get("posting_date") or doc.get("transaction_date")
conversion_rate = get_exchange_rate(
bank.account_currency, party_account_currency, posting_date
)
received_amount = paid_amount / conversion_rate
else:
if bank and company_currency != bank.account_currency:
paid_amount = received_amount / doc.get("conversion_rate", 1)
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get("conversion_rate", 1)
received_amount = paid_amount * doc.get("conversion_rate", 1)
# if payment type is pay, then paid amount and received amount are swapped
if payment_type == "Pay":
paid_amount, received_amount = received_amount, paid_amount
return paid_amount, received_amount

View File

@@ -2,7 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on("POS Closing Entry", {
onload: function (frm) {
onload: async function (frm) {
frm.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log"];
frm.set_query("pos_profile", function (doc) {
return {
@@ -36,6 +36,15 @@ frappe.ui.form.on("POS Closing Entry", {
}
});
const is_pos_using_sales_invoice = await frappe.db.get_single_value(
"Accounts Settings",
"use_sales_invoice_in_pos"
);
if (is_pos_using_sales_invoice) {
frm.set_df_property("pos_transactions", "hidden", 1);
}
set_html_data(frm);
if (frm.doc.docstatus == 1) {
@@ -83,6 +92,7 @@ frappe.ui.form.on("POS Closing Entry", {
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
() => frm.trigger("set_opening_amounts"),
() => frm.trigger("get_pos_invoices"),
() => frm.trigger("get_sales_invoices"),
() => frappe.dom.unfreeze(),
]);
}
@@ -113,7 +123,25 @@ frappe.ui.form.on("POS Closing Entry", {
},
callback: (r) => {
let pos_docs = r.message;
set_form_data(pos_docs, frm);
set_pos_transaction_form_data(pos_docs, frm);
refresh_fields(frm);
set_html_data(frm);
},
});
},
get_sales_invoices(frm) {
return frappe.call({
method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_sales_invoices",
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
pos_profile: frm.doc.pos_profile,
user: frm.doc.user,
},
callback: (r) => {
let sales_docs = r.message;
set_sales_invoice_transaction_form_data(sales_docs, frm);
refresh_fields(frm);
set_html_data(frm);
},
@@ -132,9 +160,40 @@ frappe.ui.form.on("POS Closing Entry", {
row.expected_amount = row.opening_amount;
}
const is_pos_using_sales_invoice = await frappe.db.get_single_value(
"Accounts Settings",
"use_sales_invoice_in_pos"
);
if (is_pos_using_sales_invoice) {
await Promise.all([
frappe.call({
method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices",
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
pos_profile: frm.doc.pos_profile,
user: frm.doc.user,
},
callback: (r) => {
let pos_invoices = r.message;
for (let doc of pos_invoices) {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm, false);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
}
},
}),
]);
}
await Promise.all([
frappe.call({
method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices",
method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_sales_invoices",
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
@@ -142,8 +201,8 @@ frappe.ui.form.on("POS Closing Entry", {
user: frm.doc.user,
},
callback: (r) => {
let pos_invoices = r.message;
for (let doc of pos_invoices) {
let sales_invoices = r.message;
for (let doc of sales_invoices) {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
@@ -155,6 +214,7 @@ frappe.ui.form.on("POS Closing Entry", {
},
}),
]);
frappe.dom.unfreeze();
},
});
@@ -166,7 +226,7 @@ frappe.ui.form.on("POS Closing Entry Detail", {
},
});
function set_form_data(data, frm) {
function set_pos_transaction_form_data(data, frm) {
data.forEach((d) => {
add_to_pos_transaction(d, frm);
frm.doc.grand_total += flt(d.grand_total);
@@ -177,6 +237,17 @@ function set_form_data(data, frm) {
});
}
function set_sales_invoice_transaction_form_data(data, frm) {
data.forEach((d) => {
add_to_sales_invoice_transaction(d, frm);
frm.doc.grand_total += flt(d.grand_total);
frm.doc.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty);
refresh_payments(d, frm, true);
refresh_taxes(d, frm);
});
}
function add_to_pos_transaction(d, frm) {
frm.add_child("pos_transactions", {
pos_invoice: d.name,
@@ -186,6 +257,15 @@ function add_to_pos_transaction(d, frm) {
});
}
function add_to_sales_invoice_transaction(d, frm) {
frm.add_child("sales_invoice_transactions", {
sales_invoice: d.name,
posting_date: d.posting_date,
grand_total: d.grand_total,
customer: d.customer,
});
}
function refresh_payments(d, frm, is_new) {
d.payments.forEach((p) => {
const payment = frm.doc.payment_reconciliation.find(
@@ -226,6 +306,7 @@ function refresh_taxes(d, frm) {
function reset_values(frm) {
frm.set_value("pos_transactions", []);
frm.set_value("sales_invoice_transactions", []);
frm.set_value("payment_reconciliation", []);
frm.set_value("taxes", []);
frm.set_value("grand_total", 0);
@@ -235,6 +316,7 @@ function reset_values(frm) {
function refresh_fields(frm) {
frm.refresh_field("pos_transactions");
frm.refresh_field("sales_invoice_transactions");
frm.refresh_field("payment_reconciliation");
frm.refresh_field("taxes");
frm.refresh_field("grand_total");

View File

@@ -21,6 +21,7 @@
"user",
"section_break_12",
"pos_transactions",
"sales_invoice_transactions",
"section_break_9",
"payment_reconciliation_details",
"section_break_11",
@@ -227,8 +228,15 @@
"label": "Posting Time",
"no_copy": 1,
"reqd": 1
},
{
"fieldname": "sales_invoice_transactions",
"fieldtype": "Table",
"label": "Sales Invoice Transactions",
"options": "Sales Invoice Reference"
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [
{
@@ -236,7 +244,7 @@
"link_fieldname": "pos_closing_entry"
}
],
"modified": "2024-03-27 13:10:14.073467",
"modified": "2025-03-19 19:49:58.845697",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",
@@ -285,8 +293,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -28,8 +28,9 @@ class POSClosingEntry(StatusUpdater):
from erpnext.accounts.doctype.pos_closing_entry_taxes.pos_closing_entry_taxes import (
POSClosingEntryTaxes,
)
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
POSInvoiceReference,
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
from erpnext.accounts.doctype.sales_invoice_reference.sales_invoice_reference import (
SalesInvoiceReference,
)
amended_from: DF.Link | None
@@ -45,6 +46,7 @@ class POSClosingEntry(StatusUpdater):
pos_transactions: DF.Table[POSInvoiceReference]
posting_date: DF.Date
posting_time: DF.Time
sales_invoice_transactions: DF.Table[SalesInvoiceReference]
status: DF.Literal["Draft", "Submitted", "Queued", "Failed", "Cancelled"]
taxes: DF.Table[POSClosingEntryTaxes]
total_quantity: DF.Float
@@ -58,8 +60,20 @@ class POSClosingEntry(StatusUpdater):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_duplicate_pos_invoices()
self.validate_pos_invoices()
self.is_pos_using_sales_invoice = frappe.db.get_single_value(
"Accounts Settings", "use_sales_invoice_in_pos"
)
if self.is_pos_using_sales_invoice == 0:
self.validate_duplicate_pos_invoices()
self.validate_pos_invoices()
if self.is_pos_using_sales_invoice == 1:
if len(self.pos_transactions) != 0:
frappe.throw(_("POS Invoices can't be added when Sales Invoice is enabled"))
self.validate_duplicate_sales_invoices()
self.validate_sales_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
@@ -114,6 +128,71 @@ class POSClosingEntry(StatusUpdater):
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
def validate_duplicate_sales_invoices(self):
sales_invoice_occurrences = {}
for idx, inv in enumerate(self.sales_invoice_transactions, 1):
sales_invoice_occurrences.setdefault(inv.sales_invoice, []).append(idx)
error_list = []
for key, value in sales_invoice_occurrences.items():
if len(value) > 1:
error_list.append(
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate Sales Invoices found"), as_list=True)
def validate_sales_invoices(self):
invalid_rows = []
for d in self.sales_invoice_transactions:
invalid_row = {"idx": d.idx}
sales_invoice = frappe.db.get_values(
"Sales Invoice",
d.sales_invoice,
[
"pos_profile",
"docstatus",
"is_pos",
"owner",
"is_created_using_pos",
"is_consolidated",
"pos_closing_entry",
],
as_dict=1,
)[0]
if sales_invoice.pos_closing_entry:
invalid_row.setdefault("msg", []).append(_("Sales Invoice is already consolidated"))
invalid_rows.append(invalid_row)
continue
if sales_invoice.is_pos == 0:
invalid_row.setdefault("msg", []).append(_("Sales Invoice does not have Payments"))
if sales_invoice.is_created_using_pos == 0:
invalid_row.setdefault("msg", []).append(_("Sales Invoice is not created using POS"))
if sales_invoice.pos_profile != self.pos_profile:
invalid_row.setdefault("msg", []).append(
_("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile))
)
if sales_invoice.docstatus != 1:
invalid_row.setdefault("msg", []).append(_("Sales Invoice is not submitted"))
if sales_invoice.owner != self.user:
invalid_row.setdefault("msg", []).append(
_("Sales Invoice isn't created by user {}").format(frappe.bold(self.owner))
)
if invalid_row.get("msg"):
invalid_rows.append(invalid_row)
if not invalid_rows:
return
error_list = []
for row in invalid_rows:
for msg in row.get("msg"):
error_list.append(_("Row #{}: {}").format(row.get("idx"), msg))
frappe.throw(error_list, title=_("Invalid Sales Invoices"), as_list=True)
@frappe.whitelist()
def get_payment_reconciliation_details(self):
currency = frappe.get_cached_value("Company", self.company, "default_currency")
@@ -130,9 +209,13 @@ class POSClosingEntry(StatusUpdater):
docname=f"POS Opening Entry/{self.pos_opening_entry}",
)
self.update_sales_invoices_closing_entry()
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)
self.update_sales_invoices_closing_entry(cancel=True)
@frappe.whitelist()
def retry(self):
consolidate_pos_invoices(closing_entry=self)
@@ -143,6 +226,12 @@ class POSClosingEntry(StatusUpdater):
opening_entry.set_status()
opening_entry.save()
def update_sales_invoices_closing_entry(self, cancel=False):
for d in self.sales_invoice_transactions:
frappe.db.set_value(
"Sales Invoice", d.sales_invoice, "pos_closing_entry", self.name if not cancel else None
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -173,6 +262,33 @@ def get_pos_invoices(start, end, pos_profile, user):
return data
@frappe.whitelist()
def get_sales_invoices(start, end, pos_profile, user):
data = frappe.db.sql(
"""
select
name, timestamp(posting_date, posting_time) as "timestamp"
from
`tabSales Invoice`
where
owner = %s
and docstatus = 1
and is_pos = 1
and pos_profile = %s
and is_created_using_pos = 1
and ifnull(pos_closing_entry,'') = ''
""",
(user, pos_profile),
as_dict=1,
)
data = [d for d in data if get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end)]
# need to get taxes and payments so can't avoid get_doc
data = [frappe.get_doc("Sales Invoice", d.name).as_dict() for d in data]
return data
def make_closing_entry_from_opening(opening_entry):
closing_entry = frappe.new_doc("POS Closing Entry")
closing_entry.pos_opening_entry = opening_entry.name
@@ -185,7 +301,20 @@ def make_closing_entry_from_opening(opening_entry):
closing_entry.net_total = 0
closing_entry.total_quantity = 0
invoices = get_pos_invoices(
is_pos_using_sales_invoice = frappe.db.get_single_value("Accounts Settings", "use_sales_invoice_in_pos")
pos_invoices = (
get_pos_invoices(
closing_entry.period_start_date,
closing_entry.period_end_date,
closing_entry.pos_profile,
closing_entry.user,
)
if is_pos_using_sales_invoice == 0
else []
)
sales_invoices = get_sales_invoices(
closing_entry.period_start_date,
closing_entry.period_end_date,
closing_entry.pos_profile,
@@ -193,6 +322,7 @@ def make_closing_entry_from_opening(opening_entry):
)
pos_transactions = []
sales_invoice_transactions = []
taxes = []
payments = []
for detail in opening_entry.balance_details:
@@ -206,7 +336,7 @@ def make_closing_entry_from_opening(opening_entry):
)
)
for d in invoices:
for d in pos_invoices:
pos_transactions.append(
frappe._dict(
{
@@ -217,6 +347,20 @@ def make_closing_entry_from_opening(opening_entry):
}
)
)
for d in sales_invoices:
sales_invoice_transactions.append(
frappe._dict(
{
"sales_invoice": d.name,
"posting_date": d.posting_date,
"grand_total": d.grand_total,
"customer": d.customer,
}
)
)
for d in [*pos_invoices, *sales_invoices]:
closing_entry.grand_total += flt(d.grand_total)
closing_entry.net_total += flt(d.net_total)
closing_entry.total_quantity += flt(d.total_qty)
@@ -246,6 +390,7 @@ def make_closing_entry_from_opening(opening_entry):
)
closing_entry.set("pos_transactions", pos_transactions)
closing_entry.set("sales_invoice_transactions", sales_invoice_transactions)
closing_entry.set("payment_reconciliation", payments)
closing_entry.set("taxes", taxes)

View File

@@ -289,6 +289,46 @@ class TestPOSClosingEntry(IntegrationTestCase):
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
self.assertEqual(batch_qty_with_pos, 10.0)
def test_closing_entries_with_sales_invoice(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
test_user, pos_profile = init_user_and_profile()
# Deleting all opening entry
frappe.db.sql("delete from `tabPOS Opening Entry`")
with self.change_settings("Accounts Settings", {"use_sales_invoice_in_pos": 1}):
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_si = create_sales_invoice(qty=10, do_not_save=1)
pos_si.is_pos = 1
pos_si.pos_profile = pos_profile.name
pos_si.is_created_using_pos = 1
pos_si.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
pos_si.save()
pos_si.submit()
pos_si2 = create_sales_invoice(qty=5, do_not_save=1)
pos_si2.is_pos = 1
pos_si2.pos_profile = pos_profile.name
pos_si2.is_created_using_pos = 1
pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
pos_si2.save()
pos_si2.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry)
payment = pcv_doc.payment_reconciliation[0]
self.assertEqual(payment.mode_of_payment, "Cash")
for d in pcv_doc.payment_reconciliation:
if d.mode_of_payment == "Cash":
d.closing_amount = 1500
pcv_doc.submit()
self.assertEqual(pcv_doc.total_quantity, 15)
self.assertEqual(pcv_doc.net_total, 1500)
def init_user_and_profile(**args):
user = "test@example.com"

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _, bold
from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
from frappe.utils.nestedset import get_descendants_of
@@ -17,13 +18,10 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.controllers.queries import item_query as _item_query
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class PartialPaymentValidationError(frappe.ValidationError):
pass
class POSInvoice(SalesInvoice):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -197,6 +195,7 @@ class POSInvoice(SalesInvoice):
# run on validate method of selling controller
super(SalesInvoice, self).validate()
self.validate_pos_opening_entry()
self.validate_is_pos_using_sales_invoice()
self.validate_auto_set_posting_time()
self.validate_mode_of_payment()
self.validate_uom_is_integer("stock_uom", "stock_qty")
@@ -244,6 +243,9 @@ class POSInvoice(SalesInvoice):
update_coupon_code_count(self.coupon_code, "used")
self.clear_unallocated_mode_of_payments()
if self.is_return and self.is_pos_using_sales_invoice:
self.create_and_add_consolidated_sales_invoice()
def before_cancel(self):
if (
self.consolidated_invoice
@@ -287,6 +289,47 @@ class POSInvoice(SalesInvoice):
sip = frappe.qb.DocType("Sales Invoice Payment")
frappe.qb.from_(sip).delete().where(sip.parent == self.name).where(sip.amount == 0).run()
def create_and_add_consolidated_sales_invoice(self):
sales_inv = self.create_return_sales_invoice()
self.db_set("consolidated_invoice", sales_inv.name)
self.set_status(update=True)
def create_return_sales_invoice(self):
return_sales_invoice = frappe.new_doc("Sales Invoice")
return_sales_invoice.is_pos = 1
return_sales_invoice.is_return = 1
map_doc(self, return_sales_invoice, table_map={"doctype": return_sales_invoice.doctype})
return_sales_invoice.is_created_using_pos = 1
return_sales_invoice.is_consolidated = 1
return_sales_invoice.return_against = frappe.db.get_value(
"POS Invoice", self.return_against, "consolidated_invoice"
)
items, taxes, payments = [], [], []
for d in self.items:
si_item = map_child_doc(d, return_sales_invoice, {"doctype": "Sales Invoice Item"})
si_item.pos_invoice = self.name
si_item.pos_invoice_item = d.name
si_item.sales_invoice_item = get_sales_invoice_item_from_consolidated_invoice(
self.return_against, d.pos_invoice_item
)
items.append(si_item)
for d in self.get("taxes"):
tax = map_child_doc(d, return_sales_invoice, {"doctype": "Sales Taxes and Charges"})
taxes.append(tax)
for d in self.get("payments"):
payment = map_child_doc(d, return_sales_invoice, {"doctype": "Sales Invoice Payment"})
payments.append(payment)
return_sales_invoice.set("items", items)
return_sales_invoice.set("taxes", taxes)
return_sales_invoice.set("payments", payments)
return_sales_invoice.save()
return_sales_invoice.submit()
return return_sales_invoice
def delink_serial_and_batch_bundle(self):
for row in self.items:
if row.serial_and_batch_bundle:
@@ -378,6 +421,13 @@ class POSInvoice(SalesInvoice):
title=_("Item Unavailable"),
)
def validate_is_pos_using_sales_invoice(self):
self.is_pos_using_sales_invoice = frappe.db.get_single_value(
"Accounts Settings", "use_sales_invoice_in_pos"
)
if self.is_pos_using_sales_invoice and not self.is_return:
frappe.throw(_("Sales Invoice mode is activated in POS. Please create Sales Invoice instead."))
def validate_serialised_or_batched_item(self):
error_msg = []
for d in self.get("items"):
@@ -502,20 +552,6 @@ class POSInvoice(SalesInvoice):
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
validate_loyalty_points(self, self.loyalty_points)
def validate_full_payment(self):
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
if self.docstatus == 1:
if self.is_return and self.paid_amount != invoice_total:
frappe.throw(
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
)
if self.paid_amount < invoice_total:
frappe.throw(
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
)
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get("amended_from"):

View File

@@ -7,8 +7,9 @@ import frappe
from frappe import _
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.sales_invoice.sales_invoice import PartialPaymentValidationError
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt

View File

@@ -2,13 +2,13 @@
# For license information, please see license.txt
import hashlib
import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder import DocType
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
from frappe.utils.background_jobs import enqueue, is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive
@@ -16,6 +16,7 @@ from frappe.utils.scheduler import is_scheduler_inactive
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
from erpnext.controllers.taxes_and_totals import ItemWiseTaxDetail
@@ -238,7 +239,7 @@ class POSInvoiceMergeLog(Document):
si_item.pos_invoice = doc.name
si_item.pos_invoice_item = item.name
if doc.is_return:
si_item.sales_invoice_item = get_sales_invoice_item(
si_item.sales_invoice_item = get_sales_invoice_item_from_consolidated_invoice(
doc.return_against, item.pos_invoice_item
)
if item.serial_and_batch_bundle:
@@ -303,10 +304,17 @@ class POSInvoiceMergeLog(Document):
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions]
dimension_values = frappe.db.get_value(
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1
"POS Profile",
{"name": invoice.pos_profile},
[*accounting_dimensions_fields, "cost_center", "project"],
as_dict=1,
)
for dimension in accounting_dimensions:
dimension_value = dimension_values.get(dimension.fieldname)
dimension_value = (
data[0].get(dimension.fieldname)
if data[0].get(dimension.fieldname)
else dimension_values.get(dimension.fieldname)
)
if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs):
frappe.throw(
@@ -318,6 +326,14 @@ class POSInvoiceMergeLog(Document):
invoice.set(dimension.fieldname, dimension_value)
invoice.set(
"cost_center",
data[0].get("cost_center") if data[0].get("cost_center") else dimension_values.get("cost_center"),
)
invoice.set(
"project", data[0].get("project") if data[0].get("project") else dimension_values.get("project")
)
if self.merge_invoices_based_on == "Customer Group":
invoice.flags.ignore_pos_profile = True
invoice.pos_profile = ""
@@ -337,7 +353,7 @@ class POSInvoiceMergeLog(Document):
for doc in invoice_docs:
doc.load_from_db()
inv = sales_invoice
if doc.is_return:
if doc.is_return and credit_notes:
for key, value in credit_notes.items():
if doc.name in value:
inv = key
@@ -446,9 +462,34 @@ def get_invoice_customer_map(pos_invoices):
pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice)
for customer, invoices in pos_invoice_customer_map.items():
pos_invoice_customer_map[customer] = split_invoices_by_accounting_dimension(invoices)
return pos_invoice_customer_map
def split_invoices_by_accounting_dimension(pos_invoices):
# pos_invoices = {
# {'dim_field1': 'dim_field1_value1', 'dim_field2': 'dim_field2_value1'}: [],
# {'dim_field1': 'dim_field1_value2', 'dim_field2': 'dim_field2_value1'}: []
# }
pos_invoice_accounting_dimensions_map = {}
for invoice in pos_invoices:
dimension_fields = [d.fieldname for d in get_checks_for_pl_and_bs_accounts()]
accounting_dimensions = frappe.db.get_value(
"POS Invoice", invoice.pos_invoice, [*dimension_fields, "cost_center", "project"], as_dict=1
)
accounting_dimensions_dic_hash = hashlib.sha256(
json.dumps(accounting_dimensions).encode()
).hexdigest()
pos_invoice_accounting_dimensions_map.setdefault(accounting_dimensions_dic_hash, [])
pos_invoice_accounting_dimensions_map[accounting_dimensions_dic_hash].append(invoice)
return pos_invoice_accounting_dimensions_map
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions"))
if frappe.flags.in_test and not invoices:
@@ -532,20 +573,21 @@ def split_invoices(invoices):
def create_merge_logs(invoice_by_customer, closing_entry=None):
try:
for customer, invoices in invoice_by_customer.items():
for _invoices in split_invoices(invoices):
merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = (
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
)
merge_log.posting_time = (
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
)
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
merge_log.set("pos_invoices", _invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
for customer, invoices_acc_dim in invoice_by_customer.items():
for invoices in invoices_acc_dim.values():
for _invoices in split_invoices(invoices):
merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = (
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
)
merge_log.posting_time = (
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
)
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
merge_log.set("pos_invoices", _invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
if closing_entry:
closing_entry.set_status(update=True, status="Submitted")
closing_entry.db_set("error_message", "")
@@ -633,26 +675,3 @@ def get_error_message(message) -> str:
return message["message"]
except Exception:
return str(message)
def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
try:
SalesInvoice = DocType("Sales Invoice")
SalesInvoiceItem = DocType("Sales Invoice Item")
query = (
frappe.qb.from_(SalesInvoice)
.from_(SalesInvoiceItem)
.select(SalesInvoiceItem.name)
.where(
(SalesInvoice.name == SalesInvoiceItem.parent)
& (SalesInvoice.is_return == 0)
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
)
)
result = query.run(as_dict=True)
return result[0].name if result else None
except Exception:
return None

View File

@@ -472,3 +472,58 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_separate_consolidated_invoice_for_different_accounting_dimensions(self):
"""
Creating 3 POS Invoices where first POS Invoice has different Cost Center than the other two.
Consolidate the Invoices.
Check whether the first POS Invoice is consolidated with a separate Sales Invoice than the other two.
Check whether the second and third POS Invoice are consolidated with the same Sales Invoice.
"""
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
frappe.db.sql("delete from `tabPOS Invoice`")
create_cost_center(cost_center_name="_Test POS Cost Center 1", is_group=0)
create_cost_center(cost_center_name="_Test POS Cost Center 2", is_group=0)
try:
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.cost_center = "_Test POS Cost Center 1 - _TC"
pos_inv.save()
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv.cost_center = "_Test POS Cost Center 2 - _TC"
pos_inv2.save()
pos_inv2.submit()
pos_inv3 = create_pos_invoice(rate=2300, do_not_submit=1)
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
pos_inv.cost_center = "_Test POS Cost Center 2 - _TC"
pos_inv3.save()
pos_inv3.submit()
consolidate_pos_invoices()
pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
pos_inv2.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv2.consolidated_invoice))
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
pos_inv3.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@@ -417,6 +417,7 @@
"options": "Project"
}
],
"grid_page_length": 50,
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,

View File

@@ -144,8 +144,10 @@
"contact_mobile",
"contact_email",
"company_shipping_address_section",
"shipping_address",
"dispatch_address",
"dispatch_address_display",
"column_break_126",
"shipping_address",
"shipping_address_display",
"company_billing_address_section",
"billing_address",
@@ -1548,7 +1550,7 @@
{
"fieldname": "company_shipping_address_section",
"fieldtype": "Section Break",
"label": "Company Shipping Address"
"label": "Shipping Address"
},
{
"fieldname": "column_break_126",
@@ -1635,13 +1637,28 @@
"fieldtype": "Data",
"label": "Sender",
"options": "Email"
},
{
"fieldname": "dispatch_address_display",
"fieldtype": "Text Editor",
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "dispatch_address",
"fieldtype": "Link",
"label": "Select Dispatch Address ",
"options": "Address",
"print_hide": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2025-01-14 11:39:04.564610",
"modified": "2025-04-09 16:49:22.175081",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1696,6 +1713,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "posting_date, supplier, bill_no, base_grand_total, outstanding_amount",
"sender_field": "sender",
"show_name_in_global_search": 1,

View File

@@ -117,6 +117,8 @@ class PurchaseInvoice(BuyingController):
currency: DF.Link | None
disable_rounded_total: DF.Check
discount_amount: DF.Currency
dispatch_address: DF.Link | None
dispatch_address_display: DF.TextEditor | None
due_date: DF.Date | None
from_date: DF.Date | None
grand_total: DF.Currency

View File

@@ -2705,13 +2705,13 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
To test if after applying discount on grand total,
the grand total is calculated correctly without any rounding errors
"""
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
invoice = make_purchase_invoice(qty=3, rate=100, do_not_save=True, do_not_submit=True)
invoice.append(
"items",
{
"item_code": "_Test Item",
"qty": 1,
"rate": 21.39,
"qty": 3,
"rate": 50.3,
},
)
invoice.append(
@@ -2720,18 +2720,19 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"rate": 15.5,
"rate": 15,
},
)
# the grand total here will be 255.71
# the grand total here will be 518.54
invoice.disable_rounded_total = 1
# apply discount on grand total to adjust the grand total to 255
invoice.discount_amount = 0.71
# apply discount on grand total to adjust the grand total to 518
invoice.discount_amount = 0.54
invoice.save()
# check if grand total is 496 and not something like 254.99 due to rounding errors
self.assertEqual(invoice.grand_total, 255)
# check if grand total is 518 and not something like 517.99 due to rounding errors
self.assertEqual(invoice.grand_total, 518)
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
"""

View File

@@ -199,7 +199,7 @@
"fieldname": "item_wise_tax_detail",
"fieldtype": "Code",
"hidden": 1,
"label": "Item Wise Tax Detail ",
"label": "Item Wise Tax Detail",
"oldfieldname": "item_wise_tax_detail",
"oldfieldtype": "Small Text",
"print_hide": 1,
@@ -267,18 +267,20 @@
"report_hide": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-11-22 19:17:02.377473",
"modified": "2025-04-15 13:14:48.936047",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -180,6 +180,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
if (this.frm.doc.is_created_using_pos && !this.frm.doc.is_return) {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
}
}
make_invoice_discounting() {
@@ -778,24 +782,6 @@ frappe.ui.form.on("Sales Invoice", {
};
});
},
// When multiple companies are set up. in case company name is changed set default company address
company: function (frm) {
if (frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
debounce: 2000,
callback: function (r) {
if (r.message) {
frm.set_value("company_address", r.message);
} else {
frm.set_value("company_address", "");
}
},
});
}
},
onload: function (frm) {
frm.redemption_conversion_factor = null;
},

View File

@@ -29,6 +29,8 @@
"update_billed_amount_in_delivery_note",
"is_debit_note",
"amended_from",
"is_created_using_pos",
"pos_closing_entry",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -2199,6 +2201,23 @@
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"default": "0",
"fieldname": "is_created_using_pos",
"fieldtype": "Check",
"hidden": 1,
"label": "Is created using POS",
"print_hide": 1
},
{
"depends_on": "is_created_using_pos",
"fieldname": "pos_closing_entry",
"fieldtype": "Link",
"hidden": 1,
"label": "POS Closing Entry",
"options": "POS Closing Entry",
"print_hide": 1
}
],
"grid_page_length": 50,

View File

@@ -51,6 +51,10 @@ from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amou
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
class PartialPaymentValidationError(frappe.ValidationError):
pass
class SalesInvoice(SellingController):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -133,6 +137,7 @@ class SalesInvoice(SellingController):
inter_company_invoice_reference: DF.Link | None
is_cash_or_non_trade_discount: DF.Check
is_consolidated: DF.Check
is_created_using_pos: DF.Check
is_debit_note: DF.Check
is_discounted: DF.Check
is_internal_customer: DF.Check
@@ -162,6 +167,7 @@ class SalesInvoice(SellingController):
plc_conversion_rate: DF.Float
po_date: DF.Date | None
po_no: DF.Data | None
pos_closing_entry: DF.Link | None
pos_profile: DF.Link | None
posting_date: DF.Date
posting_time: DF.Time | None
@@ -306,6 +312,10 @@ class SalesInvoice(SellingController):
if cint(self.is_pos):
self.validate_pos()
if cint(self.is_created_using_pos):
self.validate_created_using_pos()
self.validate_full_payment()
self.validate_dropship_item()
if cint(self.update_stock):
@@ -528,7 +538,22 @@ class SalesInvoice(SellingController):
)
frappe.throw(msg, title=_("Not Allowed"))
def check_if_created_using_pos_and_pos_closing_entry_generated(self):
if self.doctype == "Sales Invoice" and self.is_created_using_pos and self.pos_closing_entry:
pos_closing_entry_docstatus = frappe.db.get_value(
"POS Closing Entry", self.pos_closing_entry, "docstatus"
)
if pos_closing_entry_docstatus == 1:
frappe.throw(
msg=_("To cancel this Sales Invoice you need to cancel the POS Closing Entry {}.").format(
get_link_to_form("POS Closing Entry", self.pos_closing_entry)
),
title=_("Not Allowed"),
)
def before_cancel(self):
# check if generated via POS and already included in POS Closing Entry
self.check_if_created_using_pos_and_pos_closing_entry_generated()
self.check_if_consolidated_invoice()
super().before_cancel()
@@ -598,6 +623,15 @@ class SalesInvoice(SellingController):
self.delete_auto_created_batches()
if (
self.doctype == "Sales Invoice"
and self.is_pos
and self.is_return
and self.is_created_using_pos
and not self.pos_closing_entry
):
self.cancel_pos_invoice_credit_note_generated_during_sales_invoice_mode()
def update_status_updater_args(self):
if not cint(self.update_stock):
return
@@ -669,6 +703,15 @@ class SalesInvoice(SellingController):
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.db_update_all()
def cancel_pos_invoice_credit_note_generated_during_sales_invoice_mode(self):
pos_invoices = frappe.get_all(
"POS Invoice", filters={"consolidated_invoice": self.name}, pluck="name"
)
if pos_invoices:
for pos_invoice in pos_invoices:
pos_invoice_doc = frappe.get_doc("POS Invoice", pos_invoice)
pos_invoice_doc.cancel()
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
@@ -704,6 +747,13 @@ class SalesInvoice(SellingController):
"allow_print_before_pay": pos.get("allow_print_before_pay"),
}
@frappe.whitelist()
def reset_mode_of_payments(self):
if self.pos_profile:
pos_profile = frappe.get_cached_doc("POS Profile", self.pos_profile)
update_multi_mode_option(self, pos_profile)
self.paid_amount = 0
def update_time_sheet(self, sales_invoice):
for d in self.timesheets:
if d.time_sheet:
@@ -1025,6 +1075,32 @@ class SalesInvoice(SellingController):
) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
def validate_created_using_pos(self):
if self.is_created_using_pos and not self.pos_profile:
frappe.throw(_("POS Profile is mandatory to mark this invoice as POS Transaction."))
self.is_pos_using_sales_invoice = frappe.db.get_single_value(
"Accounts Settings", "use_sales_invoice_in_pos"
)
if not self.is_pos_using_sales_invoice and not self.is_return:
frappe.throw(_("Transactions using Sales Invoice in POS are disabled."))
def validate_full_payment(self):
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
if self.docstatus == 1:
if self.is_return and self.paid_amount != invoice_total:
frappe.throw(
msg=_("Partial Payment in POS Transactions are not allowed."),
exc=PartialPaymentValidationError,
)
if self.paid_amount < invoice_total:
frappe.throw(
msg=_("Partial Payment in POS Transactions are not allowed."),
exc=PartialPaymentValidationError,
)
def validate_warehouse(self):
super().validate_warehouse()
@@ -2295,7 +2371,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
# Invert Addresses
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name
)
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name
)
update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
@@ -2717,9 +2796,11 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language")
# update outstanding
# update outstanding from doc
if source.payment_schedule and len(source.payment_schedule) == 1:
target.overdue_payments[0].outstanding = source.get("outstanding_amount")
for row in target.overdue_payments:
if row.payment_schedule == source.payment_schedule[0].name:
row.outstanding = source.get("outstanding_amount")
target.validate()

View File

@@ -4386,6 +4386,27 @@ class TestSalesInvoice(IntegrationTestCase):
self.assertRaises(StockOverReturnError, return_doc.save)
def test_pos_sales_invoice_creation_during_pos_invoice_mode(self):
# Deleting all opening entry
frappe.db.sql("delete from `tabPOS Opening Entry`")
with self.change_settings("Accounts Settings", {"use_sales_invoice_in_pos": 0}):
pos_profile = make_pos_profile()
pos_profile.payments = []
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
pos_profile.save()
pos = create_sales_invoice(qty=10, do_not_save=True)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
pos.is_created_using_pos = 1
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
self.assertRaises(frappe.ValidationError, pos.insert)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -0,0 +1,85 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-03-19 15:01:28.834774",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sales_invoice",
"posting_date",
"column_break_fear",
"customer",
"grand_total",
"is_return",
"return_against"
],
"fields": [
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"reqd": 1
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "column_break_fear",
"fieldtype": "Column Break"
},
{
"fetch_from": "sales_invoice.customer",
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer",
"read_only": 1,
"reqd": 1
},
{
"default": "0",
"fetch_from": "sales_invoice.is_return",
"fieldname": "is_return",
"fieldtype": "Check",
"label": "Is Return",
"read_only": 1
},
{
"fetch_from": "sales_invoice.return_against",
"fieldname": "return_against",
"fieldtype": "Link",
"label": "Return Against",
"options": "Sales Invoice",
"read_only": 1
},
{
"fetch_from": "sales_invoice.grand_total",
"fieldname": "grand_total",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-20 01:14:57.890299",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Reference",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,28 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SalesInvoiceReference(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
customer: DF.Link
grand_total: DF.Currency
is_return: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
posting_date: DF.Date
return_against: DF.Link | None
sales_invoice: DF.Link
# end: auto-generated types
pass

View File

@@ -71,6 +71,7 @@ def get_party_details(
party_address=None,
company_address=None,
shipping_address=None,
dispatch_address=None,
pos_profile=None,
):
if not party:
@@ -92,6 +93,7 @@ def get_party_details(
party_address,
company_address,
shipping_address,
dispatch_address,
pos_profile,
)
@@ -111,6 +113,7 @@ def _get_party_details(
party_address=None,
company_address=None,
shipping_address=None,
dispatch_address=None,
pos_profile=None,
):
party_details = frappe._dict(
@@ -134,6 +137,7 @@ def _get_party_details(
party_address,
company_address,
shipping_address,
dispatch_address,
ignore_permissions=ignore_permissions,
)
set_contact_details(party_details, party, party_type)
@@ -191,34 +195,51 @@ def set_address_details(
party_address=None,
company_address=None,
shipping_address=None,
dispatch_address=None,
*,
ignore_permissions=False,
):
billing_address_field = (
# party_billing
party_billing_field = (
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address"
)
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
party_details[party_billing_field] = party_address or get_default_address(party_type, party.name)
if doctype:
party_details.update(
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
get_fetch_values(doctype, party_billing_field, party_details[party_billing_field])
)
# address display
party_details.address_display = render_address(
party_details[billing_address_field], check_permissions=not ignore_permissions
)
# shipping address
if party_type in ["Customer", "Lead"]:
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
party_type, party.name
)
party_details.shipping_address = render_address(
party_details["shipping_address_name"], check_permissions=not ignore_permissions
)
if doctype:
party_details.update(
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
)
party_details.address_display = render_address(
party_details[party_billing_field], check_permissions=not ignore_permissions
)
# party_shipping
if party_type in ["Customer", "Lead"]:
party_shipping_field = "shipping_address_name"
party_shipping_display = "shipping_address"
default_shipping = shipping_address
else:
# Supplier
party_shipping_field = "dispatch_address"
party_shipping_display = "dispatch_address_display"
default_shipping = dispatch_address
party_details[party_shipping_field] = default_shipping or get_party_shipping_address(
party_type, party.name
)
party_details[party_shipping_display] = render_address(
party_details[party_shipping_field], check_permissions=not ignore_permissions
)
if doctype:
party_details.update(
get_fetch_values(doctype, party_shipping_field, party_details[party_shipping_field])
)
# company_address
if company_address:
party_details.company_address = company_address
else:
@@ -256,22 +277,20 @@ def set_address_details(
**get_fetch_values(doctype, "shipping_address", party_details.billing_address),
)
party_address, shipping_address = (
party_details.get(billing_address_field),
party_details.shipping_address_name,
party_billing, party_shipping = (
party_details.get(party_billing_field),
party_details.get(party_shipping_field),
)
party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"),
party_address,
shipping_address if party_type != "Supplier" else party_address,
party.get("tax_category"), party_billing, party_shipping
)
if doctype in TRANSACTION_TYPES:
with temporary_flag("company", company):
get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address
return party_billing, party_shipping
@erpnext.allow_regional

View File

@@ -100,7 +100,7 @@ class PartyLedgerSummaryReport:
conditions.append(doctype.territory.isin(self.filters.territory))
if self.filters.get(group_field):
conditions.append(doctype.get(group_field).isin(self.filters.get(group_field)))
conditions.append(doctype[group_field].isin(self.filters.get(group_field)))
if self.filters.payment_terms_template:
conditions.append(doctype.payment_terms == self.filters.payment_terms_template)

View File

@@ -519,9 +519,6 @@ def get_accounting_entries(
.where(gl_entry.company == filters.company)
)
if group_by_account:
query = query.groupby(gl_entry.account)
ignore_is_opening = frappe.db.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
@@ -551,6 +548,9 @@ def get_accounting_entries(
if match_conditions:
query += "and" + match_conditions
if group_by_account:
query += " GROUP BY `account`"
return frappe.db.sql(query, params, as_dict=True)

View File

@@ -59,3 +59,33 @@ class TestSupplierLedgerSummary(AccountsTestMixin, IntegrationTestCase):
for field in expected:
with self.subTest(field=field):
self.assertEqual(report_output[0].get(field), expected.get(field))
def test_supplier_ledger_summary_with_filters(self):
self.create_purchase_invoice()
supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group")
filters = {
"company": self.company,
"from_date": today(),
"to_date": today(),
"supplier_group": supplier_group,
}
expected = {
"party": "_Test Supplier",
"party_name": "_Test Supplier",
"opening_balance": 0,
"invoiced_amount": 300.0,
"paid_amount": 0,
"return_amount": 0,
"closing_balance": 300.0,
"currency": "INR",
"supplier_name": "_Test Supplier",
}
report_output = execute(filters)[1]
self.assertEqual(len(report_output), 1)
for field in expected:
with self.subTest(field=field):
self.assertEqual(report_output[0].get(field), expected.get(field))

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.utils import getdate
def execute(filters=None):
@@ -33,6 +34,7 @@ def execute(filters=None):
def validate_filters(filters):
"""Validate if dates are properly set"""
filters = frappe._dict(filters or {})
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
@@ -68,7 +70,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
if not tax_withholding_category:
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category)
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
if net_total_map.get((voucher_type, name)):
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
@@ -439,12 +441,22 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
def get_tax_rate_map(filters):
rate_map = frappe.get_all(
"Tax Withholding Rate",
filters={
"from_date": ("<=", filters.get("from_date")),
"to_date": (">=", filters.get("to_date")),
},
fields=["parent", "tax_withholding_rate"],
as_list=1,
filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)},
fields=["parent", "tax_withholding_rate", "from_date", "to_date"],
)
return frappe._dict(rate_map)
rate_list = frappe._dict()
for rate in rate_map:
rate_list.setdefault(rate.parent, []).append(frappe._dict(rate))
return rate_list
def get_tax_withholding_rates(tax_withholding, posting_date):
# returns the row that matches with the fiscal year from posting date
for rate in tax_withholding:
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
return rate.tax_withholding_rate
return 0

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import today
from frappe.utils import add_to_date, today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -60,6 +60,56 @@ class TestTaxWithholdingDetails(AccountsTestMixin, IntegrationTestCase):
]
self.check_expected_values(result, expected_values)
def test_date_filters_in_multiple_tax_withholding_rules(self):
create_tax_category("TDS - 3", rate=10, account="TDS - _TC", cumulative_threshold=1)
# insert new rate in same fiscal year
fiscal_year = get_fiscal_year(today(), company="_Test Company")
mid_year = add_to_date(fiscal_year[1], months=6)
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
tds_doc.rates[0].to_date = mid_year
tds_doc.append(
"rates",
{
"tax_withholding_rate": 20,
"from_date": add_to_date(mid_year, days=1),
"to_date": fiscal_year[2],
"single_threshold": 1,
"cumulative_threshold": 1,
},
)
tds_doc.save()
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_1.apply_tds = 1
inv_1.tax_withholding_category = "TDS - 3"
inv_1.submit()
inv_2 = make_purchase_invoice(
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
)
inv_2.set_posting_time = 1
inv_1.apply_tds = 1
inv_2.tax_withholding_category = "TDS - 3"
inv_2.save()
inv_2.submit()
result = execute(
frappe._dict(
company="_Test Company",
party_type="Supplier",
from_date=fiscal_year[1],
to_date=fiscal_year[2],
)
)[1]
expected_values = [
[inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500],
[inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000],
]
self.check_expected_values(result, expected_values)
def check_expected_values(self, result, expected_values):
for i in range(len(result)):
voucher = frappe._dict(result[i])

View File

@@ -661,10 +661,6 @@ frappe.ui.form.on("Asset", {
} else {
frm.set_value("purchase_invoice_item", data.purchase_invoice_item);
}
let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only
frm.set_df_property("gross_purchase_amount", "read_only", is_editable);
frm.set_df_property("asset_quantity", "read_only", is_editable);
}
},
});

View File

@@ -87,6 +87,7 @@
"options": "ACC-ASS-.YYYY.-"
},
{
"allow_on_submit": 1,
"depends_on": "item_code",
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
@@ -477,6 +478,7 @@
"fieldname": "total_asset_cost",
"fieldtype": "Currency",
"label": "Total Asset Cost",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -485,6 +487,7 @@
"fieldname": "additional_asset_cost",
"fieldtype": "Currency",
"label": "Additional Asset Cost",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -588,7 +591,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-02-20 14:09:05.421913",
"modified": "2025-04-24 15:31:47.373274",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -626,10 +629,11 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "asset_name",
"track_changes": 1
}
}

View File

@@ -42,14 +42,15 @@ from erpnext.controllers.accounts_controller import AccountsController
class Asset(AccountsController):
# begin: auto-generated types
# ruff: noqa
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
from frappe.types import DF
additional_asset_cost: DF.Currency
amended_from: DF.Link | None
@@ -117,6 +118,7 @@ class Asset(AccountsController):
total_asset_cost: DF.Currency
total_number_of_depreciations: DF.Int
value_after_depreciation: DF.Currency
# ruff: noqa
# end: auto-generated types
def validate(self):
@@ -1168,7 +1170,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}"))
first_item = matching_items[0]
is_multiple_items = len(matching_items) > 1
return {
"company": purchase_doc.company,
@@ -1177,7 +1178,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
"asset_quantity": first_item.qty,
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
"asset_location": first_item.get("asset_location"),
"is_multiple_items": is_multiple_items,
"purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
}

View File

@@ -135,9 +135,11 @@ class AssetRepair(AccountsController):
self.increase_asset_value()
total_repair_cost = self.get_total_value_of_stock_consumed()
if self.capitalize_repair_cost:
self.asset_doc.total_asset_cost += self.repair_cost
self.asset_doc.additional_asset_cost += self.repair_cost
total_repair_cost += self.repair_cost
self.asset_doc.total_asset_cost += total_repair_cost
self.asset_doc.additional_asset_cost += total_repair_cost
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
@@ -176,9 +178,11 @@ class AssetRepair(AccountsController):
self.decrease_asset_value()
total_repair_cost = self.get_total_value_of_stock_consumed()
if self.capitalize_repair_cost:
self.asset_doc.total_asset_cost -= self.repair_cost
self.asset_doc.additional_asset_cost -= self.repair_cost
total_repair_cost += self.repair_cost
self.asset_doc.total_asset_cost -= total_repair_cost
self.asset_doc.additional_asset_cost -= total_repair_cost
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")

View File

@@ -110,8 +110,10 @@
"contact_mobile",
"contact_email",
"shipping_address_section",
"shipping_address",
"dispatch_address",
"dispatch_address_display",
"column_break_99",
"shipping_address",
"shipping_address_display",
"company_billing_address_section",
"billing_address",
@@ -1291,6 +1293,20 @@
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
},
{
"fieldname": "dispatch_address",
"fieldtype": "Link",
"label": "Dispatch Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "dispatch_address_display",
"fieldtype": "Text Editor",
"label": "Dispatch Address Details",
"print_hide": 1,
"read_only": 1
}
],
"grid_page_length": 50,
@@ -1298,7 +1314,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2025-03-03 16:48:08.697520",
"modified": "2025-04-09 16:54:08.836106",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -92,6 +92,8 @@ class PurchaseOrder(BuyingController):
customer_name: DF.Data | None
disable_rounded_total: DF.Check
discount_amount: DF.Currency
dispatch_address: DF.Link | None
dispatch_address_display: DF.TextEditor | None
from_date: DF.Date | None
grand_total: DF.Currency
group_same_items: DF.Check

View File

@@ -158,6 +158,14 @@ frappe.ui.form.on("Request for Quotation", {
);
frm.page.set_inner_btn_group_as_primary(__("Create"));
frm.add_custom_button(
__("Supplier Quotation Comparison"),
function () {
frm.trigger("show_supplier_quotation_comparison");
},
__("View")
);
}
if (frm.doc.docstatus === 0) {
@@ -165,6 +173,20 @@ frappe.ui.form.on("Request for Quotation", {
}
},
show_supplier_quotation_comparison(frm) {
const today = new Date();
const oneMonthAgo = new Date(today);
oneMonthAgo.setMonth(today.getMonth() - 1);
frappe.route_options = {
company: frm.doc.company,
from_date: moment(oneMonthAgo).format("YYYY-MM-DD"),
to_date: moment(today).format("YYYY-MM-DD"),
request_for_quotation: frm.doc.name,
};
frappe.set_route("query-report", "Supplier Quotation Comparison");
},
make_supplier_quotation: function (frm) {
var doc = frm.doc;
var dialog = new frappe.ui.Dialog({

View File

@@ -2415,13 +2415,12 @@ class AccountsController(TransactionBase):
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
)
d.outstanding = d.payment_amount
d.base_outstanding = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding")
)
d.base_outstanding = d.base_payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
)
d.base_outstanding = d.base_payment_amount
else:
self.fetch_payment_terms_from_order(
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms

View File

@@ -141,6 +141,7 @@ class BuyingController(SubcontractingController):
company=self.company,
party_address=self.get("supplier_address"),
shipping_address=self.get("shipping_address"),
dispatch_address=self.get("dispatch_address"),
company_address=self.get("billing_address"),
fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"),
ignore_permissions=self.flags.ignore_permissions,
@@ -242,6 +243,7 @@ class BuyingController(SubcontractingController):
address_dict = {
"supplier_address": "address_display",
"shipping_address": "shipping_address_display",
"dispatch_address": "dispatch_address_display",
"billing_address": "billing_address_display",
}

View File

@@ -8,9 +8,10 @@ from collections import OrderedDict, defaultdict
import frappe
from frappe import qb, scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.permissions import has_permission
from frappe.query_builder import Criterion, CustomFunction
from frappe.query_builder.functions import Concat, Locate, Sum
from frappe.utils import nowdate, today, unique
from frappe.utils import cint, nowdate, today, unique
from pypika import Order
import erpnext
@@ -20,10 +21,28 @@ from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_templat
# searches for active employees
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def employee_query(doctype, txt, searchfield, start, page_len, filters):
def employee_query(
doctype,
txt,
searchfield,
start,
page_len,
filters,
reference_doctype: str | None = None,
ignore_user_permissions: bool = False,
):
doctype = "Employee"
conditions = []
fields = get_fields(doctype, ["name", "employee_name"])
ignore_permissions = False
if reference_doctype and ignore_user_permissions:
ignore_permissions = has_ignored_field(reference_doctype, doctype) and has_permission(
doctype,
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
)
mcond = "" if ignore_permissions else get_match_cond(doctype)
return frappe.db.sql(
"""select {fields} from `tabEmployee`
@@ -42,13 +61,32 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
"fields": ", ".join(fields),
"key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions),
"mcond": get_match_cond(doctype),
"mcond": mcond,
}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
)
def has_ignored_field(reference_doctype, doctype):
meta = frappe.get_meta(reference_doctype)
for field in meta.fields:
if not field.ignore_user_permissions:
continue
if field.fieldtype == "Link" and field.options == doctype:
return True
elif field.fieldtype == "Dynamic Link":
options = meta.get_link_doctype(field.fieldname)
if not options:
continue
if isinstance(options, str):
options = options.split("\n")
if doctype in options or "DocType" in options:
return True
return False
# searches for leads which are not converted
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -928,7 +966,7 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.get_all(
"UOM",
filters={"name": ["like", f"%{txt}%"]},
filters={"name": ["like", f"%{txt}%"], "enabled": 1},
fields=["name"],
limit_start=start,
limit_page_length=page_len,

View File

@@ -6,6 +6,7 @@ from collections import defaultdict
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.query_builder import DocType
from frappe.utils import cint, flt, format_datetime, get_datetime
import erpnext
@@ -347,6 +348,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
"Company", company, "default_warehouse_for_sales_return"
)
if doctype == "Sales Invoice":
inv_is_consolidated, inv_is_pos = frappe.db.get_value(
"Sales Invoice", source_name, ["is_consolidated", "is_pos"]
)
if inv_is_consolidated and inv_is_pos:
frappe.throw(
_("Cannot create return for consolidated invoice {0}.").format(source_name),
title=_("Cannot Create Return"),
)
def set_missing_values(source, target):
doc = frappe.get_doc(target)
doc.is_return = 1
@@ -377,6 +388,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if doc.get("is_return"):
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
doc.consolidated_invoice = ""
if doc.doctype == "Sales Invoice":
doc.pos_closing_entry = ""
# no copy enabled for party_account_currency
doc.party_account_currency = source.party_account_currency
doc.set("payments", [])
@@ -1169,26 +1182,49 @@ def get_payment_data(invoice):
@frappe.whitelist()
def get_pos_invoice_item_returned_qty(pos_invoice, customer, item_row_name):
is_return, docstatus = frappe.db.get_value("POS Invoice", pos_invoice, ["is_return", "docstatus"])
def get_invoice_item_returned_qty(doctype, invoice, customer, item_row_name):
is_return, docstatus = frappe.db.get_value(doctype, invoice, ["is_return", "docstatus"])
if not is_return and docstatus == 1:
return get_returned_qty_map_for_row(pos_invoice, customer, item_row_name, "POS Invoice")
return get_returned_qty_map_for_row(invoice, customer, item_row_name, doctype)
@frappe.whitelist()
def is_pos_invoice_returnable(pos_invoice):
def is_invoice_returnable(doctype, invoice):
is_return, docstatus, customer = frappe.db.get_value(
"POS Invoice", pos_invoice, ["is_return", "docstatus", "customer"]
doctype, invoice, ["is_return", "docstatus", "customer"]
)
if is_return or docstatus == 0:
return False
invoice_item_qty = frappe.db.get_all("POS Invoice Item", {"parent": pos_invoice}, ["name", "qty"])
invoice_item_qty = frappe.db.get_all(f"{doctype} Item", {"parent": invoice}, ["name", "qty"])
already_full_returned = 0
for d in invoice_item_qty:
returned_qty = get_returned_qty_map_for_row(pos_invoice, customer, d.name, "POS Invoice")
returned_qty = get_returned_qty_map_for_row(invoice, customer, d.name, doctype)
if returned_qty.qty == d.qty:
already_full_returned += 1
return len(invoice_item_qty) != already_full_returned
def get_sales_invoice_item_from_consolidated_invoice(return_against_pos_invoice, pos_invoice_item):
try:
SalesInvoice = DocType("Sales Invoice")
SalesInvoiceItem = DocType("Sales Invoice Item")
query = (
frappe.qb.from_(SalesInvoice)
.from_(SalesInvoiceItem)
.select(SalesInvoiceItem.name)
.where(
(SalesInvoice.name == SalesInvoiceItem.parent)
& (SalesInvoice.is_return == 0)
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
)
)
result = query.run(as_dict=True)
return result[0].name if result else None
except Exception:
return None

View File

@@ -92,8 +92,8 @@ status_map = {
"Delivery Note": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],

View File

@@ -991,7 +991,13 @@ class StockController(AccountsController):
def update_billing_percentage(self, update_modified=True):
target_ref_field = "amount"
if self.doctype == "Delivery Note":
target_ref_field = "amount - (returned_qty * rate)"
total_amount = total_returned = 0
for item in self.items:
total_amount += flt(item.amount)
total_returned += flt(item.returned_qty * item.rate)
if total_returned < total_amount:
target_ref_field = "(amount - (returned_qty * rate))"
self._update_percent_field(
{
@@ -1044,6 +1050,16 @@ class StockController(AccountsController):
def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
if self.doctype in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
] and frappe.db.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
):
return
if not row.quality_inspection:
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code)
@@ -1156,6 +1172,12 @@ class StockController(AccountsController):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
return
self.__inter_company_reference = (
self.get("inter_company_reference")
if self.doctype == "Purchase Invoice"
else self.get("inter_company_invoice_reference")
)
item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty()
if not item_wise_transfer_qty:
return
@@ -1185,15 +1207,11 @@ class StockController(AccountsController):
bold(key[1]),
bold(flt(transferred_qty, precision)),
bold(parent_doctype),
get_link_to_form(parent_doctype, self.get("inter_company_reference")),
get_link_to_form(parent_doctype, self.__inter_company_reference),
)
)
def get_item_wise_inter_transfer_qty(self):
reference_field = "inter_company_reference"
if self.doctype == "Purchase Invoice":
reference_field = "inter_company_invoice_reference"
parent_doctype = {
"Purchase Receipt": "Delivery Note",
"Purchase Invoice": "Sales Invoice",
@@ -1213,7 +1231,7 @@ class StockController(AccountsController):
child_tab.item_code,
child_tab.qty,
)
.where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1))
.where((parent_tab.name == self.__inter_company_reference) & (parent_tab.docstatus == 1))
)
data = query.run(as_dict=True)

View File

@@ -21,8 +21,6 @@ from erpnext.deprecation_dumpster import deprecated
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template, get_item_tax_map
from erpnext.utilities.regional import temporary_flag
logger = frappe.logger(__name__)
ItemWiseTaxDetail = frappe._dict
@@ -384,22 +382,22 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
self.grand_total_diff = 0
doc = self.doc
if not doc.get("taxes"):
return
# maintain actual tax rate based on idx
actual_tax_dict = dict(
[
[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
for tax in self.doc.get("taxes")
for tax in doc.taxes
if tax.charge_type == "Actual"
]
)
logger.debug(f"{self.doc} ...")
for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
logger.debug(f" Item {n}: {item.item_code}" + (f" - {item_tax_map}" if item_tax_map else ""))
for i, tax in enumerate(self.doc.get("taxes")):
for i, tax in enumerate(doc.taxes):
# tax_amount represents the amount of tax for the current step
current_net_amount, current_tax_amount = self.get_current_tax_and_net_amount(
item, tax, item_tax_map
@@ -438,37 +436,42 @@ class calculate_taxes_and_totals:
tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount)
else:
tax.grand_total_for_current_item = flt(
self.doc.get("taxes")[i - 1].grand_total_for_current_item + current_tax_amount
doc.taxes[i - 1].grand_total_for_current_item + current_tax_amount
)
# set precision in the last item iteration
if n == len(self._items) - 1:
self.round_off_totals(tax)
self._set_in_company_currency(
tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]
discount_amount_applied = self.discount_amount_applied
if doc.apply_discount_on == "Grand Total" and (
discount_amount_applied or doc.discount_amount or doc.additional_discount_percentage
):
tax_amount_precision = doc.taxes[0].precision("tax_amount")
for i, tax in enumerate(doc.taxes):
if discount_amount_applied:
tax.tax_amount_after_discount_amount = flt(
tax.tax_amount_after_discount_amount, tax_amount_precision
)
self.round_off_base_values(tax)
self.set_cumulative_total(i, tax)
self.set_cumulative_total(i, tax)
self._set_in_company_currency(tax, ["total"])
# adjust Discount Amount loss in last tax iteration
if (
i == (len(self.doc.get("taxes")) - 1)
and self.discount_amount_applied
and self.doc.discount_amount
and self.doc.apply_discount_on == "Grand Total"
):
self.grand_total_diff = flt(
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"),
)
logger.debug(
f" net_amount: {current_net_amount:<20} tax_amount: {current_tax_amount:<20} - {tax.description}"
if not discount_amount_applied:
self.grand_total_for_distributing_discount = doc.taxes[-1].total
else:
self.grand_total_diff = flt(
self.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[-1].total,
doc.precision("grand_total"),
)
for i, tax in enumerate(doc.taxes):
self.round_off_totals(tax)
self._set_in_company_currency(
tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]
)
self.round_off_base_values(tax)
self.set_cumulative_total(i, tax)
self._set_in_company_currency(tax, ["total"])
def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax):
# if just for valuation, do not add the tax amount in total
# if tax/charges is for deduction, multiply by -1
@@ -612,16 +615,20 @@ class calculate_taxes_and_totals:
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.grand_total_diff = diff
else:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
else:
self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt(
self.doc.grand_total - self.doc.net_total - self.grand_total_diff,
self.doc.grand_total - self.doc.net_total - grand_total_diff,
self.doc.precision("total_taxes_and_charges"),
)
else:
@@ -766,7 +773,8 @@ class calculate_taxes_and_totals:
self.doc.base_discount_amount = 0
def get_total_for_discount_amount(self):
if self.doc.apply_discount_on == "Net Total":
doc = self.doc
if doc.apply_discount_on == "Net Total" or not doc.get("taxes"):
return self.doc.net_total
total_actual_tax = 0
@@ -786,7 +794,7 @@ class calculate_taxes_and_totals:
"cumulative_tax_amount": total_actual_tax,
}
for tax in self.doc.get("taxes"):
for tax in doc.taxes:
if tax.charge_type in ["Actual", "On Item Quantity"]:
update_actual_tax_dict(tax, tax.tax_amount)
continue
@@ -805,7 +813,7 @@ class calculate_taxes_and_totals:
)
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
return self.doc.grand_total - total_actual_tax
return getattr(self, "grand_total_for_distributing_discount", doc.grand_total) - total_actual_tax
def calculate_total_advance(self):
if not self.doc.docstatus.is_cancelled():

View File

@@ -2,6 +2,9 @@ import unittest
from functools import partial
import frappe
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.core.doctype.user_permission.user_permission import add_user_permissions
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.tests import IntegrationTestCase
from erpnext.controllers import queries
@@ -85,3 +88,54 @@ class TestQueries(IntegrationTestCase):
def test_default_uoms(self):
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
def test_employee_query_with_user_permissions(self):
# party field is a dynamic link field in Payment Entry doctype with ignore_user_permissions=0
ps = make_property_setter(
doctype="Payment Entry",
fieldname="party",
property="ignore_user_permissions",
value=1,
property_type="Check",
)
ps.save()
user = create_user("test_employee_query@example.com", ("Accounts User", "HR User"))
add_user_permissions(
{
"user": user.name,
"doctype": "Employee",
"docname": "_T-Employee-00001",
"is_default": 1,
"apply_to_all_doctypes": 1,
"applicable_doctypes": [],
"hide_descendants": 0,
}
)
frappe.reload_doc("accounts", "doctype", "payment entry")
frappe.set_user(user.name)
params = {
"doctype": "Employee",
"txt": "",
"searchfield": "name",
"start": 0,
"page_len": 20,
"filters": None,
"reference_doctype": "Payment Entry",
"ignore_user_permissions": 1,
}
result = queries.employee_query(**params)
self.assertGreater(len(result), 1)
ps.delete(ignore_permissions=1, force=1, delete_permanently=1)
frappe.reload_doc("accounts", "doctype", "payment entry")
frappe.clear_cache()
# only one employee should be returned even though ignore_user_permissions is passed as 1
result = queries.employee_query(**params)
self.assertEqual(len(result), 1)
frappe.set_user("Administrator")

View File

@@ -147,14 +147,37 @@ def link_open_events(ref_doctype, ref_docname, doc):
def get_open_activities(ref_doctype, ref_docname):
tasks = get_open_todos(ref_doctype, ref_docname)
events = get_open_events(ref_doctype, ref_docname)
tasks_history = get_closed_todos(ref_doctype, ref_docname)
events_history = get_closed_events(ref_doctype, ref_docname)
return {"tasks": tasks, "events": events}
return {
"tasks": tasks,
"events": events,
"tasks_history": tasks_history,
"events_history": events_history,
}
def get_closed_todos(ref_doctype, ref_docname):
return get_filtered_todos(ref_doctype, ref_docname, status=("!=", "Open"))
def get_open_todos(ref_doctype, ref_docname):
return get_filtered_todos(ref_doctype, ref_docname, status="Open")
def get_open_events(ref_doctype, ref_docname):
return get_filtered_events(ref_doctype, ref_docname, open=True)
def get_closed_events(ref_doctype, ref_docname):
return get_filtered_events(ref_doctype, ref_docname, open=False)
def get_filtered_todos(ref_doctype, ref_docname, status: str | tuple[str, str]):
return frappe.get_all(
"ToDo",
filters={"reference_type": ref_doctype, "reference_name": ref_docname, "status": "Open"},
filters={"reference_type": ref_doctype, "reference_name": ref_docname, "status": status},
fields=[
"name",
"description",
@@ -164,10 +187,15 @@ def get_open_todos(ref_doctype, ref_docname):
)
def get_open_events(ref_doctype, ref_docname):
def get_filtered_events(ref_doctype, ref_docname, open: bool):
event = frappe.qb.DocType("Event")
event_link = frappe.qb.DocType("Event Participants")
if open:
event_status_filter = event.status == "Open"
else:
event_status_filter = event.status != "Open"
query = (
frappe.qb.from_(event)
.join(event_link)
@@ -183,7 +211,7 @@ def get_open_events(ref_doctype, ref_docname):
.where(
(event_link.reference_doctype == ref_doctype)
& (event_link.reference_docname == ref_docname)
& (event.status == "Open")
& (event_status_filter)
)
)
data = query.run(as_dict=True)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -334,7 +334,7 @@ frappe.ui.form.on("Production Plan", {
frm.set_value("consider_minimum_order_qty", 0);
if (frm.doc.ignore_existing_ordered_qty) {
if (!frm.doc.ignore_existing_ordered_qty) {
frm.events.get_items_for_material_requests(frm);
} else {
const title = __("Transfer Materials For Warehouse {0}", [frm.doc.for_warehouse]);

View File

@@ -230,11 +230,11 @@
"label": "Include Subcontracted Items"
},
{
"default": "0",
"description": "If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.",
"default": "1",
"description": "If enabled, the system will consider items with a shortfall in quantity. \n<br>\nQty = Reqd Qty (BOM) - <a href=\"https://docs.frappe.io/erpnext/user/manual/en/projected-quantity\">Projected Qty</a>",
"fieldname": "ignore_existing_ordered_qty",
"fieldtype": "Check",
"label": "Ignore Available Stock"
"label": "Skip Available Raw Materials"
},
{
"fieldname": "column_break_25",
@@ -249,7 +249,7 @@
{
"fieldname": "get_items_for_mr",
"fieldtype": "Button",
"label": "Get Raw Materials for Purchase"
"label": "Get Items for Purchase Only"
},
{
"fieldname": "section_break_27",
@@ -391,9 +391,10 @@
"label": "Consolidate Sub Assembly Items"
},
{
"description": "If items in stock, proceed with Material Transfer or Purchase.",
"fieldname": "transfer_materials",
"fieldtype": "Button",
"label": "Get Raw Materials for Transfer"
"label": "Get Items for Purchase / Transfer"
},
{
"collapsible": 1,
@@ -402,8 +403,8 @@
"label": "Preview Required Materials"
},
{
"default": "0",
"description": "If this checkbox is enabled, then the system won\u2019t run the MRP for the available sub-assembly items.",
"default": "1",
"description": "If enabled, the system will consider items with a shortfall in quantity. \n<br>\nQty = Reqd Qty (BOM) - <a href=\"https://docs.frappe.io/erpnext/user/manual/en/projected-quantity\">Projected Qty</a>",
"fieldname": "skip_available_sub_assembly_item",
"fieldtype": "Check",
"label": "Skip Available Sub Assembly Items"
@@ -436,11 +437,12 @@
"label": "Consider Minimum Order Qty"
}
],
"grid_page_length": 50,
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-01-10 17:47:52.207209",
"modified": "2025-04-08 17:24:09.394056",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
@@ -461,7 +463,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

View File

@@ -950,8 +950,7 @@ class ProductionPlan(Document):
if material_request_list:
material_request_list = [
f"""<a href="/app/Form/Material Request/{m.name}">{m.name}</a>"""
for m in material_request_list
get_link_to_form("Material Request", m.name) for m in material_request_list
]
msgprint(_("{0} created").format(comma_and(material_request_list)))
else:
@@ -1329,7 +1328,7 @@ def get_material_request_items(
total_qty = row["qty"]
required_qty = 0
if ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
required_qty = total_qty
elif total_qty > bin_dict.get("projected_qty", 0):
required_qty = total_qty - bin_dict.get("projected_qty", 0)
@@ -1688,7 +1687,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
if items:
mr_items.append(items)
if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
if (ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
new_mr_items = []
for item in mr_items:
get_materials_from_other_locations(item, warehouses, new_mr_items, company)

View File

@@ -180,7 +180,7 @@ class TestProductionPlan(IntegrationTestCase):
)
pln = create_production_plan(
item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=0
item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=1
)
self.assertFalse(len(pln.mr_items))
@@ -725,6 +725,7 @@ class TestProductionPlan(IntegrationTestCase):
},
)
pln.skip_available_sub_assembly_item = 0
pln.get_sub_assembly_items("In House")
pln.submit()
pln.make_work_order()
@@ -1454,6 +1455,7 @@ class TestProductionPlan(IntegrationTestCase):
)
plan.for_warehouse = mrp_warhouse
plan.ignore_existing_ordered_qty = 1
items = get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": wh1}, {"warehouse": wh2}]
@@ -1690,6 +1692,7 @@ class TestProductionPlan(IntegrationTestCase):
)
pln.for_warehouse = rm_warehouse
pln.ignore_existing_ordered_qty = 1
items = get_items_for_material_requests(pln.as_dict(), warehouses=[{"warehouse": store_warehouse}])
for row in items:
@@ -1891,6 +1894,7 @@ class TestProductionPlan(IntegrationTestCase):
},
)
plan.save()
plan.ignore_existing_ordered_qty = 1
plan.get_sub_assembly_items()

View File

@@ -107,7 +107,6 @@ erpnext.patches.v12_0.remove_bank_remittance_custom_fields
erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit
erpnext.patches.v12_0.add_variant_of_in_item_attribute_table
erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account
erpnext.patches.v12_0.create_default_energy_point_rules
erpnext.patches.v12_0.set_produced_qty_field_in_sales_order_for_work_order
erpnext.patches.v12_0.set_cwip_and_delete_asset_settings
erpnext.patches.v12_0.set_expense_account_in_landed_cost_voucher_taxes

View File

@@ -1,8 +0,0 @@
import frappe
from erpnext.setup.install import create_default_energy_point_rules
def execute():
frappe.reload_doc("social", "doctype", "energy_point_rule")
create_default_energy_point_rules()

View File

@@ -54,6 +54,12 @@ erpnext.buying = {
return erpnext.queries.company_address_query(this.frm.doc)
});
}
if(this.frm.get_field('dispatch_address')) {
this.frm.set_query("dispatch_address", () => {
return erpnext.queries.address_query(this.frm.doc);
});
}
}
setup_queries(doc, cdt, cdn) {
@@ -295,6 +301,12 @@ erpnext.buying = {
"shipping_address_display", true);
}
dispatch_address(){
var me = this;
erpnext.utils.get_address_display(this.frm, "dispatch_address",
"dispatch_address_display", true);
}
billing_address() {
erpnext.utils.get_address_display(this.frm, "billing_address",
"billing_address_display", true);

View File

@@ -343,12 +343,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
calculate_taxes() {
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
var me = this;
this.grand_total_diff = 0;
var actual_tax_dict = {};
// maintain actual tax rate based on idx
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
$.each(doc.taxes, function(i, tax) {
if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax));
}
@@ -356,7 +358,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
$.each(this.frm._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
$.each(doc.taxes, function(i, tax) {
// tax_amount represents the amount of tax for the current step
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
if (frappe.flags.round_row_wise_tax) {
@@ -401,29 +403,40 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
tax.grand_total_for_current_item =
flt(me.frm.doc["taxes"][i-1].grand_total_for_current_item + current_tax_amount);
}
// set precision in the last item iteration
if (n == me.frm._items.length - 1) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
me.round_off_base_values(tax);
// in tax.total, accumulate grand total for each item
me.set_cumulative_total(i, tax);
me.set_in_company_currency(tax, ["total"]);
// adjust Discount Amount loss in last tax iteration
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
me.grand_total_diff = flt(me.frm.doc.grand_total -
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
}
}
});
});
const discount_amount_applied = this.discount_amount_applied;
if (doc.apply_discount_on === "Grand Total" && (discount_amount_applied || doc.discount_amount || doc.additional_discount_percentage)) {
const tax_amount_precision = precision("tax_amount", doc.taxes[0]);
for (const [i, tax] of doc.taxes.entries()) {
if (discount_amount_applied)
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, tax_amount_precision);
this.set_cumulative_total(i, tax);
}
if (!this.discount_amount_applied) {
this.grand_total_for_distributing_discount = doc.taxes[doc.taxes.length - 1].total;
} else {
this.grand_total_diff = flt(
this.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[doc.taxes.length - 1].total, precision("grand_total"));
}
}
for (const [i, tax] of doc.taxes.entries()) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
me.round_off_base_values(tax);
// in tax.total, accumulate grand total for each tax
me.set_cumulative_total(i, tax);
me.set_in_company_currency(tax, ["total"]);
}
}
set_cumulative_total(row_idx, tax) {
@@ -586,10 +599,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_totals() {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
var me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
const me = this;
const tax_count = this.frm.doc.taxes?.length;
const grand_total_diff = this.grand_total_diff || 0;
this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff
? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff
: this.frm.doc.net_total);
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
@@ -621,7 +636,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
- this.grand_total_diff, precision("total_taxes_and_charges"));
- grand_total_diff, precision("total_taxes_and_charges"));
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
@@ -744,8 +759,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
get_total_for_discount_amount() {
if(this.frm.doc.apply_discount_on == "Net Total")
return this.frm.doc.net_total;
const doc = this.frm.doc;
if (doc.apply_discount_on == "Net Total" || !doc.taxes?.length)
return doc.net_total;
let total_actual_tax = 0.0;
let actual_taxes_dict = {};
@@ -760,7 +777,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
};
}
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
doc.taxes.forEach(tax => {
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
update_actual_taxes_dict(tax, tax.tax_amount);
return;
@@ -775,7 +792,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
});
return this.frm.doc.grand_total - total_actual_tax;
return (this.grand_total_for_distributing_discount || doc.grand_total) - total_actual_tax;
}
calculate_total_advance(update_paid_amount) {

View File

@@ -795,6 +795,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return;
}
if (item.serial_no) {
item.use_serial_batch_fields = 1
}
if (item && item.serial_no) {
if (!item.item_code) {
this.frm.trigger("item_code", cdt, cdn);
@@ -1130,13 +1134,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
currency() {
// The transaction date be either transaction_date (from orders) or posting_date (from invoices)
let transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
let inter_company_reference = this.frm.doc.inter_company_order_reference || this.frm.doc.inter_company_invoice_reference;
let me = this;
this.set_dynamic_labels();
let company_currency = this.get_company_currency();
// Added `load_after_mapping` to determine if document is loading after mapping from another doc
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
&& !this.frm.doc.__onload?.load_after_mapping) {
&& (!this.frm.doc.__onload?.load_after_mapping || inter_company_reference)) {
this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency,
function(exchange_rate) {
@@ -1357,13 +1362,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
batch_no(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
if (!this.is_a_mapped_document(item)) {
this.apply_price_list(item, true);
}
}
toggle_conversion_factor(item) {
// toggle read only property for conversion factor field if the uom and stock uom are same
if(this.frm.get_field('items').grid.fields_map.conversion_factor) {
@@ -1589,7 +1587,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
batch_no(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.use_serial_batch_fields && row.batch_no) {
if (row.batch_no) {
row.use_serial_batch_fields = 1
}
if (row.batch_no) {
var params = this._get_args(row);
params.batch_no = row.batch_no;
params.uom = row.uom;

View File

@@ -239,13 +239,16 @@ erpnext.setup.fiscal_years = {
Afghanistan: ["12-21", "12-20"],
Australia: ["07-01", "06-30"],
Bangladesh: ["07-01", "06-30"],
Canada: ["04-01", "03-31"],
"Costa Rica": ["10-01", "09-30"],
Egypt: ["07-01", "06-30"],
Ethiopia: ["07-08", "07-07"],
"Hong Kong": ["04-01", "03-31"],
India: ["04-01", "03-31"],
Iran: ["06-23", "06-22"],
Kenya: ["07-01", "06-30"],
Malaysia: ["07-01", "06-30"],
Myanmar: ["04-01", "03-31"],
Nepal: ["07-16", "07-15"],
"New Zealand": ["04-01", "03-31"],
Pakistan: ["07-01", "06-30"],
Singapore: ["04-01", "03-31"],

View File

@@ -57,6 +57,47 @@
{{ __("No open task") }}
</div>
{% } %}
{% if (typeof tasks_history == "object" && tasks_history?.length) { %}
<div class="mt-4" style="opacity: 0.8;">
<div class="open-section-head">
<span>{{ __("Completed Tasks") }}</span>
</div>
{% for (const t of tasks_history) { %}
<div class="single-activity">
{% if(t.date || t.allocated_to) { %}
<div class="text-muted small flex align-items-center">
<div>
{% if(t.allocated_to) { %}
{%= frappe.avatar(t.allocated_to) %}
{% } %}
</div>
<div class="ml-2">
<div class="indicator-pill no-indicator-dot whitespace-nowrap red">
{{ __("Done") }}
</div>
</div>
{% if (t.date) { %}
<div class="ml-auto">
{%= frappe.datetime.global_date_format(t.date) %}
</div>
{% } %}
<div class="checkbox {% if(!t.date) { %}ml-auto{% } else { %}ml-2{% } %}">
<input type="checkbox" class="completion-checkbox" checked disabled>
</div>
</div>
{% } %}
<div class="pl-2 mb-2 mt-2">
<div class="label-area font-md">
<a href="/app/todo/{{ t.name }}" title="{{ __('Open Task') }}">
{%= t.description %}
</a>
</div>
</div>
</div>
{% } %}
</div>
{% } %}
</div>
<div class="open-events pl-1">
<div class="open-section-head">
@@ -104,6 +145,49 @@
{{ __("No open event") }}
</div>
{% } %}
{% if (typeof events_history == "object" && events_history?.length) { %}
<div class="mt-4" style="opacity: 0.8;">
<div class="open-section-head">
<span>{{ __("Past Events") }}</span>
</div>
{% let icon_set = {"Sent/Received Email": "mail", "Call": "call", "Meeting": "share-people"}; %}
{% for(const event of events_history) { %}
<div class="single-activity">
<div class="flex justify-between mb-2">
<div class="row label-area font-md ml-1 title">
<span class="mr-2">
<svg class="icon icon-sm">
<use href="#icon-{{ icon_set[event.event_category] || 'calendar' }}"></use>
</svg>
</span>
<a href="/app/event/{{ event.name }}" title="{{ __('Open Event') }}">
{%= event.subject %}
</a>
</div>
<div class="checkbox">
<input type="checkbox" class="completion-checkbox" checked disabled>
</div>
</div>
<div class="text-muted ml-1">
{%= frappe.datetime.global_date_format(event.starts_on) %}
{% if (event.ends_on) { %}
{% if (frappe.datetime.obj_to_user(event.starts_on) != frappe.datetime.obj_to_user(event.ends_on)) %}
-
{%= frappe.datetime.global_date_format(frappe.datetime.obj_to_user(event.ends_on)) %}
{%= frappe.datetime.get_time(event.ends_on) %}
{% } else if (event.ends_on) { %}
-
{%= frappe.datetime.get_time(event.ends_on) %}
{% } %}
{% } %}
</div>
</div>
{% } %}
</div>
{% } %}
</div>
</div>
</div>

View File

@@ -35,6 +35,8 @@ erpnext.utils.CRMActivities = class CRMActivities {
var activities_html = frappe.render_template("crm_activities", {
tasks: r.message.tasks,
events: r.message.events,
tasks_history: r.message.tasks_history,
events_history: r.message.events_history,
});
$(activities_html).appendTo(me.open_activities_wrapper);

View File

@@ -78,35 +78,34 @@ erpnext.accounts.dimensions = {
},
update_dimension(frm, doctype) {
if (this.accounting_dimensions) {
this.accounting_dimensions.forEach((dimension) => {
if (frm.is_new()) {
if (
frm.doc.company &&
Object.keys(this.default_dimensions || {}).length > 0 &&
this.default_dimensions[frm.doc.company]
) {
let default_dimension =
this.default_dimensions[frm.doc.company][dimension["fieldname"]];
if (
!this.accounting_dimensions ||
!frm.is_new() ||
!frm.doc.company ||
!this.default_dimensions?.[frm.doc.company]
)
return;
if (default_dimension) {
if (frappe.meta.has_field(doctype, dimension["fieldname"])) {
frm.set_value(dimension["fieldname"], default_dimension);
}
$.each(frm.doc.items || frm.doc.accounts || [], function (i, row) {
frappe.model.set_value(
row.doctype,
row.name,
dimension["fieldname"],
default_dimension
);
});
}
}
}
});
// don't set default dimensions if any of the dimension is already set due to mapping
if (frm.doc.__onload?.load_after_mapping) {
for (const dimension of this.accounting_dimensions) {
if (frm.doc[dimension["fieldname"]]) return;
}
}
this.accounting_dimensions.forEach((dimension) => {
const default_dimension = this.default_dimensions[frm.doc.company][dimension["fieldname"]];
if (!default_dimension) return;
if (frappe.meta.has_field(doctype, dimension["fieldname"])) {
frm.set_value(dimension["fieldname"], default_dimension);
}
(frm.doc.items || frm.doc.accounts || []).forEach((row) => {
frappe.model.set_value(row.doctype, row.name, dimension["fieldname"], default_dimension);
});
});
},
copy_dimension_from_first_row(frm, cdt, cdn, fieldname) {

View File

@@ -71,6 +71,10 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) {
if (!args.shipping_address && frm.doc.shipping_address) {
args.shipping_address = frm.doc.shipping_address;
}
if (!args.dispatch_address && frm.doc.dispatch_address) {
args.dispatch_address = frm.doc.dispatch_address;
}
}
if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {

View File

@@ -115,6 +115,33 @@ erpnext.sales_common = {
this.toggle_editable_price_list_rate();
}
company() {
super.company();
this.set_default_company_address();
}
set_default_company_address() {
if (!frappe.meta.has_field(this.frm.doc.doctype, "company_address")) return;
var me = this;
if (this.frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: {
name: this.frm.doc.company,
existing_address: this.frm.doc.company_address || "",
},
debounce: 2000,
callback: function (r) {
if (r.message) {
me.frm.set_value("company_address", r.message);
} else {
me.frm.set_value("company_address", "");
}
},
});
}
}
customer() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function () {

View File

@@ -610,6 +610,7 @@ body[data-route="pos"] {
.frappe-control[data-fieldname="address_display"] .ql-editor,
.frappe-control[data-fieldname="shipping_address_display"] .ql-editor,
.frappe-control[data-fieldname="shipping_address"] .ql-editor,
.frappe-control[data-fieldname="dispatch_address_display"] .ql-editor,
.frappe-control[data-fieldname="dispatch_address"] .ql-editor,
.frappe-control[data-fieldname="source_address_display"] .ql-editor,
.frappe-control[data-fieldname="target_address_display"] .ql-editor,

View File

@@ -153,9 +153,11 @@
margin-bottom: 0px;
min-height: 8rem;
height: 8rem;
overflow: hidden;
> img {
@extend .image;
width: 100%;
}
}
@@ -395,9 +397,11 @@
border-radius: var(--border-radius-md);
color: var(--text-light);
margin-right: var(--margin-md);
overflow: hidden;
> img {
@extend .image;
width: 100%;
}
}
@@ -699,14 +703,16 @@
display: flex;
align-items: center;
justify-content: center;
width: 11rem;
height: 11rem;
max-width: 11rem;
max-height: 11rem;
border-radius: var(--border-radius-md);
margin-left: var(--margin-md);
color: var(--gray-500);
overflow: hidden;
> img {
@extend .image;
width: 100%;
}
> .item-abbr {
@@ -716,8 +722,8 @@
justify-content: center;
border-radius: var(--border-radius-md);
font-size: var(--text-3xl);
width: 100%;
height: 100%;
width: 11rem;
height: 11rem;
}
}
}
@@ -747,6 +753,7 @@
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: var(--padding-md);
row-gap: var(--padding-md);
> .auto-fetch-btn {
@extend .pointer-no-select;
@@ -1167,6 +1174,16 @@
}
}
@media screen and (max-width: 1200px) {
.point-of-sale-app {
> .items-selector {
> .items-container {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
}
}
@media screen and (max-width: 620px) {
.point-of-sale-app {
grid-template-columns: repeat(1, minmax(0, 1fr));

View File

@@ -378,6 +378,7 @@ def make_custom_fields(update=True):
),
],
"Purchase Invoice Item": invoice_item_fields,
"POS Invoice Item": invoice_item_fields,
"Sales Order Item": invoice_item_fields,
"Delivery Note Item": invoice_item_fields,
"Sales Invoice Item": invoice_item_fields + customer_po_fields,

View File

@@ -78,7 +78,8 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
onload(doc, dt, dn) {
super.onload(doc, dt, dn);
this.frm.trigger("disable_customer_if_creating_from_opportunity");
// TODO: think of better way to do this
// this.frm.trigger("disable_customer_if_creating_from_opportunity");
}
party_name() {
var me = this;

View File

@@ -175,27 +175,6 @@ frappe.ui.form.on("Sales Order", {
);
},
// When multiple companies are set up. in case company name is changed set default company address
company: function (frm) {
if (frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: {
name: frm.doc.company,
existing_address: frm.doc.company_address || "",
},
debounce: 2000,
callback: function (r) {
if (r.message) {
frm.set_value("company_address", r.message);
} else {
frm.set_value("company_address", "");
}
},
});
}
},
onload: function (frm) {
if (!frm.doc.transaction_date) {
frm.set_value("transaction_date", frappe.datetime.get_today());

View File

@@ -5,7 +5,7 @@
import json
import frappe
from frappe.utils import cint
from frappe.utils import cint, get_datetime
from frappe.utils.nestedset import get_root_of
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
@@ -328,25 +328,59 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = []
if search_term and status:
invoices_by_customer = frappe.db.get_list(
pos_invoices_by_customer = frappe.db.get_list(
"POS Invoice",
filters={"customer": ["like", f"%{search_term}%"], "status": status},
filters=get_invoice_filters("POS Invoice", status, customer=search_term),
fields=fields,
page_length=limit,
)
invoices_by_name = frappe.db.get_list(
pos_invoices_by_name = frappe.db.get_list(
"POS Invoice",
filters={"name": ["like", f"%{search_term}%"], "status": status},
filters=get_invoice_filters("POS Invoice", status, name=search_term),
fields=fields,
page_length=limit,
)
invoice_list = invoices_by_customer + invoices_by_name
elif status:
invoice_list = frappe.db.get_list(
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
pos_invoice_list = add_doctype_to_results(
"POS Invoice", pos_invoices_by_customer + pos_invoices_by_name
)
sales_invoices_by_customer = frappe.db.get_list(
"Sales Invoice",
filters=get_invoice_filters("Sales Invoice", status, customer=search_term),
fields=fields,
page_length=limit,
)
sales_invoices_by_name = frappe.db.get_list(
"Sales Invoice",
filters=get_invoice_filters("Sales Invoice", status, name=search_term),
fields=fields,
page_length=limit,
)
sales_invoice_list = add_doctype_to_results(
"Sales Invoice", sales_invoices_by_customer + sales_invoices_by_name
)
elif status:
pos_invoice_list = frappe.db.get_list(
"POS Invoice",
filters=get_invoice_filters("POS Invoice", status),
fields=fields,
page_length=limit,
)
pos_invoice_list = add_doctype_to_results("POS Invoice", pos_invoice_list)
sales_invoice_list = frappe.db.get_list(
"Sales Invoice",
filters=get_invoice_filters("Sales Invoice", status),
fields=fields,
page_length=limit,
)
sales_invoice_list = add_doctype_to_results("Sales Invoice", sales_invoice_list)
invoice_list = order_results_by_posting_date([*pos_invoice_list, *sales_invoice_list])
return invoice_list
@@ -402,3 +436,68 @@ def get_pos_profile_data(pos_profile):
pos_profile.customer_groups = _customer_groups_with_children
return pos_profile
def add_doctype_to_results(doctype, results):
for result in results:
result["doctype"] = doctype
return results
def order_results_by_posting_date(results):
return sorted(
results,
key=lambda x: get_datetime(f"{x.get('posting_date')} {x.get('posting_time')}"),
reverse=True,
)
def get_invoice_filters(doctype, status, name=None, customer=None):
filters = {}
if name:
filters["name"] = ["like", f"%{name}%"]
if customer:
filters["customer"] = ["like", f"%{customer}%"]
if doctype == "POS Invoice":
filters["status"] = status
return filters
if doctype == "Sales Invoice":
filters["is_created_using_pos"] = 1
filters["is_consolidated"] = 0
if status == "Draft":
filters["docstatus"] = 0
else:
filters["docstatus"] = 1
if status == "Paid":
filters["is_return"] = 0
if status == "Return":
filters["is_return"] = 1
filters["pos_closing_entry"] = ["is", "set"] if status == "Consolidated" else ["is", "not set"]
return filters
@frappe.whitelist()
def get_customer_recent_transactions(customer):
sales_invoices = frappe.db.get_list(
"Sales Invoice",
filters={"customer": customer, "docstatus": 1, "is_pos": 1, "is_consolidated": 0},
fields=["name", "grand_total", "status", "posting_date", "posting_time", "currency"],
page_length=20,
)
pos_invoices = frappe.db.get_list(
"POS Invoice",
filters={"customer": customer, "docstatus": 1},
fields=["name", "grand_total", "status", "posting_date", "posting_time", "currency"],
page_length=20,
)
invoices = order_results_by_posting_date(sales_invoices + pos_invoices)
return invoices

View File

@@ -139,6 +139,11 @@ erpnext.PointOfSale.Controller = class {
this.allow_negative_stock = flt(message.allow_negative_stock) || false;
});
const use_sales_invoice_in_pos = await frappe.db.get_single_value(
"Accounts Settings",
"use_sales_invoice_in_pos"
);
frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data",
args: { pos_profile: this.pos_profile },
@@ -146,6 +151,7 @@ erpnext.PointOfSale.Controller = class {
const profile = res.message;
Object.assign(this.settings, profile);
this.settings.customer_groups = profile.customer_groups.map((group) => group.name);
this.settings.frm_doctype = use_sales_invoice_in_pos ? "Sales Invoice" : "POS Invoice";
this.make_app();
},
});
@@ -376,7 +382,6 @@ erpnext.PointOfSale.Controller = class {
highlight_cart_item: (item) => {
const cart_item = this.cart.get_cart_item(item);
this.cart.toggle_item_highlight(cart_item);
},
item_field_focused: (fieldname) => {
@@ -456,8 +461,9 @@ erpnext.PointOfSale.Controller = class {
this.recent_order_list = new erpnext.PointOfSale.PastOrderList({
wrapper: this.$components_wrapper,
events: {
open_invoice_data: (name) => {
frappe.db.get_doc("POS Invoice", name).then((doc) => {
open_invoice_data: (doctype, name) => {
if (!["POS Invoice", "Sales Invoice"].includes(doctype)) return;
frappe.db.get_doc(doctype, name).then((doc) => {
this.order_summary.load_summary_of(doc);
});
},
@@ -473,21 +479,26 @@ erpnext.PointOfSale.Controller = class {
events: {
get_frm: () => this.frm,
process_return: (name) => {
process_return: (doctype, name) => {
this.recent_order_list.toggle_component(false);
frappe.db.get_doc("POS Invoice", name).then((doc) => {
frappe.db.get_doc(doctype, name).then((doc) => {
frappe.run_serially([
() => frappe.dom.freeze(),
() => this.make_invoice_frm(doc.doctype),
() => this.make_return_invoice(doc),
() => this.cart.load_invoice(),
() => this.item_selector.toggle_component(true),
() => this.item_selector.resize_selector(false),
() => this.item_details.toggle_component(false),
() => frappe.dom.unfreeze(),
]);
});
},
edit_order: (name) => {
edit_order: (doctype, name) => {
this.recent_order_list.toggle_component(false);
frappe.run_serially([
() => this.make_invoice_frm(doctype),
() => this.sync_draft_invoice_to_frm(doctype, name),
() => this.frm.refresh(name),
() => this.frm.call("reset_mode_of_payments"),
() => this.cart.load_invoice(),
@@ -496,9 +507,11 @@ erpnext.PointOfSale.Controller = class {
() => this.item_details.toggle_component(false),
]);
},
delete_order: (name) => {
frappe.model.delete_doc(this.frm.doc.doctype, name, () => {
this.recent_order_list.refresh_list();
delete_order: (doctype, name) => {
frappe.model.with_doctype(doctype, () => {
frappe.model.delete_doc(doctype, name, () => {
this.recent_order_list.refresh_list();
});
});
},
new_order: () => {
@@ -530,7 +543,7 @@ erpnext.PointOfSale.Controller = class {
make_new_invoice() {
return frappe.run_serially([
() => frappe.dom.freeze(),
() => this.make_sales_invoice_frm(),
() => this.make_invoice_frm(this.settings.frm_doctype),
() => this.set_pos_profile_data(),
() => this.set_pos_profile_status(),
() => this.cart.load_invoice(),
@@ -538,27 +551,27 @@ erpnext.PointOfSale.Controller = class {
]);
}
make_sales_invoice_frm() {
const doctype = "POS Invoice";
make_invoice_frm(doctype) {
return new Promise((resolve) => {
if (this.frm) {
this.frm = this.get_new_frm(this.frm);
if (this.frm && this.frm.doctype == doctype) {
this.frm = this.get_new_frm(this.frm, doctype);
this.frm.doc.items = [];
this.frm.doc.is_pos = 1;
if (doctype == "Sales Invoice") this.frm.doc.is_created_using_pos = 1;
resolve();
} else {
frappe.model.with_doctype(doctype, () => {
this.frm = this.get_new_frm();
this.frm = this.get_new_frm(undefined, doctype);
this.frm.doc.items = [];
this.frm.doc.is_pos = 1;
if (doctype == "Sales Invoice") this.frm.doc.is_created_using_pos = 1;
resolve();
});
}
});
}
get_new_frm(_frm) {
const doctype = "POS Invoice";
get_new_frm(_frm, doctype = this.settings.frm_doctype) {
const page = $("<div>");
const frm = _frm || new frappe.ui.form.Form(doctype, page, false);
const name = frappe.model.make_new_doc_and_get_name(doctype, true);
@@ -567,12 +580,18 @@ erpnext.PointOfSale.Controller = class {
return frm;
}
sync_draft_invoice_to_frm(doctype, invoice) {
return frappe.db.get_doc(doctype, invoice).then((doc) => {
frappe.model.sync(doc);
});
}
async make_return_invoice(doc) {
frappe.dom.freeze();
this.frm = this.get_new_frm(this.frm);
this.frm.doc.items = [];
return frappe.call({
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
method:
doc.doctype == "POS Invoice"
? "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return"
: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_sales_return",
args: {
source_name: doc.name,
target_doc: this.frm.doc,
@@ -580,9 +599,7 @@ erpnext.PointOfSale.Controller = class {
callback: (r) => {
frappe.model.sync(r.message);
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
this.set_pos_profile_data().then(() => {
frappe.dom.unfreeze();
});
this.set_pos_profile_data();
},
});
}

View File

@@ -188,6 +188,7 @@ erpnext.PointOfSale.ItemCart = class {
await me.events.checkout();
me.toggle_checkout_btn(false);
me.disable_customer_selection();
me.allow_discount_change && me.$add_discount_elem.removeClass("d-none");
});
@@ -195,6 +196,7 @@ erpnext.PointOfSale.ItemCart = class {
this.$totals_section.on("click", ".edit-cart-btn", () => {
this.events.edit_cart();
this.toggle_checkout_btn(true);
me.enable_customer_selection();
});
this.$component.on("click", ".add-discount-wrapper", () => {
@@ -207,6 +209,11 @@ erpnext.PointOfSale.ItemCart = class {
// called when discount is applied
this.update_totals_section(frm);
});
frappe.ui.form.on("Sales Invoice", "paid_amount", (frm) => {
// called when discount is applied
this.update_totals_section(frm);
});
}
attach_shortcuts() {
@@ -278,7 +285,7 @@ erpnext.PointOfSale.ItemCart = class {
toggle_item_highlight(item) {
const $cart_item = $(item);
const item_is_highlighted = $cart_item.attr("style") == "background-color:var(--gray-50);";
const item_is_highlighted = $cart_item.attr("style") == "background-color: var(--control-bg);";
if (!item || item_is_highlighted) {
this.item_is_selected = false;
@@ -698,6 +705,25 @@ erpnext.PointOfSale.ItemCart = class {
}
}
disable_customer_selection() {
this.$customer_section.find(".reset-customer-btn").css("visibility", "hidden");
this.$customer_section.off("click", ".customer-display");
this.$customer_section.off("click", ".reset-customer-btn");
}
enable_customer_selection() {
this.$customer_section.find(".reset-customer-btn").css("visibility", "visible");
this.$customer_section.on("click", ".customer-display", (e) => {
if ($(e.target).closest(".reset-customer-btn").length) return;
const show = this.$cart_container.is(":visible");
this.toggle_customer_info(show);
});
this.$customer_section.on("click", ".reset-customer-btn", () => {
this.reset_customer_selector();
});
}
highlight_checkout_btn(toggle) {
if (toggle) {
this.$add_discount_elem.css("display", "flex");
@@ -968,13 +994,13 @@ erpnext.PointOfSale.ItemCart = class {
}
fetch_customer_transactions() {
frappe.db
.get_list("POS Invoice", {
filters: { customer: this.customer_info.customer, docstatus: 1 },
fields: ["name", "grand_total", "status", "posting_date", "posting_time", "currency"],
limit: 20,
frappe
.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_customer_recent_transactions",
args: { customer: this.customer_info.customer },
})
.then((res) => {
res = res.message;
const transaction_container = this.$customer_section.find(".customer-transactions");
if (!res.length) {
@@ -998,6 +1024,7 @@ erpnext.PointOfSale.ItemCart = class {
Draft: "red",
Return: "gray",
Consolidated: "blue",
"Credit Note Issued": "gray",
};
transaction_container.append(

View File

@@ -6,6 +6,7 @@ erpnext.PointOfSale.ItemDetails = class {
this.allow_rate_change = settings.allow_rate_change;
this.allow_discount_change = settings.allow_discount_change;
this.current_item = {};
this.frm_doctype = settings.frm_doctype;
this.init_component();
}
@@ -323,7 +324,9 @@ erpnext.PointOfSale.ItemDetails = class {
};
}
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
const frm_doctype = this.events.get_frm().doc.doctype;
frappe.model.on(`${frm_doctype} Item`, "*", (fieldname, value, item_row) => {
const field_control = this[`${fieldname}_control`];
const item_row_is_being_edited = this.compare_with_current_item(item_row);
if (
@@ -423,7 +426,7 @@ erpnext.PointOfSale.ItemDetails = class {
warehouse: this.warehouse_control.get_value() || "",
batch_nos: this.current_item.batch_no || "",
posting_date: expiry_date,
for_doctype: "POS Invoice",
for_doctype: this.frm_doctype,
},
});

View File

@@ -104,11 +104,11 @@ erpnext.PointOfSale.ItemSelector = class {
return `<div class="item-qty-pill">
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
</div>
<div class="flex items-center justify-center border-b-grey text-6xl text-grey-100" style="height:8rem; min-height:8rem">
<div class="item-display">
<img
onerror="cur_pos.item_selector.handle_broken_image(this)"
class="h-full item-img" src="${item_image}"
alt="${frappe.get_abbr(item.item_name)}"
class="item-img" src="${item_image}"
alt="${item.item_name}"
>
</div>`;
} else {

View File

@@ -38,9 +38,10 @@ erpnext.PointOfSale.PastOrderList = class {
});
const me = this;
this.$invoices_container.on("click", ".invoice-wrapper", function () {
const invoice_doctype = $(this).attr("data-invoice-doctype");
const invoice_name = unescape($(this).attr("data-invoice-name"));
me.events.open_invoice_data(invoice_name);
me.events.open_invoice_data(invoice_doctype, invoice_name);
});
}
@@ -99,7 +100,9 @@ erpnext.PointOfSale.PastOrderList = class {
const posting_datetime = frappe.datetime.str_to_user(
invoice.posting_date + " " + invoice.posting_time
);
return `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
return `<div class="invoice-wrapper" data-invoice-doctype="${
invoice.doctype
}" data-invoice-name="${escape(invoice.name)}">
<div class="invoice-name-date">
<div class="invoice-name">${invoice.name}</div>
<div class="invoice-date">

View File

@@ -117,9 +117,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
async function get_returned_qty() {
const r = await frappe.call({
method: "erpnext.controllers.sales_and_purchase_return.get_pos_invoice_item_returned_qty",
method: "erpnext.controllers.sales_and_purchase_return.get_invoice_item_returned_qty",
args: {
pos_invoice: doc.name,
doctype: doc.doctype,
invoice: doc.name,
customer: doc.customer,
item_row_name: item_data.name,
},
@@ -192,7 +193,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
bind_events() {
this.$summary_container.on("click", ".return-btn", async () => {
const r = await this.is_pos_invoice_returnable(this.doc.name);
const r = await this.is_invoice_returnable(this.doc.doctype, this.doc.name);
if (!r) {
frappe.msgprint({
title: __("Invalid Return"),
@@ -201,21 +202,21 @@ erpnext.PointOfSale.PastOrderSummary = class {
});
return;
}
this.events.process_return(this.doc.name);
this.events.process_return(this.doc.doctype, this.doc.name);
this.toggle_component(false);
this.$component.find(".no-summary-placeholder").css("display", "flex");
this.$summary_wrapper.css("display", "none");
});
this.$summary_container.on("click", ".edit-btn", () => {
this.events.edit_order(this.doc.name);
this.events.edit_order(this.doc.doctype, this.doc.name);
this.toggle_component(false);
this.$component.find(".no-summary-placeholder").css("display", "flex");
this.$summary_wrapper.css("display", "none");
});
this.$summary_container.on("click", ".delete-btn", () => {
this.events.delete_order(this.doc.name);
this.events.delete_order(this.doc.doctype, this.doc.name);
this.show_summary_placeholder();
});
@@ -461,11 +462,12 @@ erpnext.PointOfSale.PastOrderSummary = class {
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
}
async is_pos_invoice_returnable(invoice) {
async is_invoice_returnable(doctype, invoice) {
const r = await frappe.call({
method: "erpnext.controllers.sales_and_purchase_return.is_pos_invoice_returnable",
method: "erpnext.controllers.sales_and_purchase_return.is_invoice_returnable",
args: {
pos_invoice: invoice,
doctype: doctype,
invoice: invoice,
},
});
return r.message;

View File

@@ -164,36 +164,12 @@ erpnext.PointOfSale.Payment = class {
}
});
frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => {
const contact = frm.doc.contact_mobile;
const request_button = $(this.request_for_payment_field?.$input[0]);
if (contact) {
request_button.removeClass("btn-default").addClass("btn-primary");
} else {
request_button.removeClass("btn-primary").addClass("btn-default");
}
frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => {
this.bind_coupon_code_event(frm);
});
frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => {
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
if (!frm.doc.ignore_pricing_rule) {
frm.applying_pos_coupon_code = true;
frappe.run_serially([
() => (frm.doc.ignore_pricing_rule = 1),
() => frm.trigger("ignore_pricing_rule"),
() => (frm.doc.ignore_pricing_rule = 0),
() => frm.trigger("apply_pricing_rule"),
() => frm.save(),
() => this.update_totals_section(frm.doc),
() => (frm.applying_pos_coupon_code = false),
]);
} else if (frm.doc.ignore_pricing_rule) {
frappe.show_alert({
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
indicator: "orange",
});
}
}
frappe.ui.form.on("Sales Invoice", "coupon_code", (frm) => {
this.bind_coupon_code_event(frm);
});
this.setup_listener_for_payments();
@@ -225,19 +201,19 @@ erpnext.PointOfSale.Payment = class {
});
frappe.ui.form.on("POS Invoice", "paid_amount", (frm) => {
this.update_totals_section(frm.doc);
// need to re calculate cash shortcuts after discount is applied
const is_cash_shortcuts_invisible = !this.$payment_modes.find(".cash-shortcuts").is(":visible");
this.attach_cash_shortcuts(frm.doc);
!is_cash_shortcuts_invisible &&
this.$payment_modes.find(".cash-shortcuts").css("display", "grid");
this.render_payment_mode_dom();
this.bind_paid_amount_event(frm);
});
frappe.ui.form.on("POS Invoice", "loyalty_amount", (frm) => {
const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency);
this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency);
this.bind_loyalty_amount_event(frm);
});
frappe.ui.form.on("Sales Invoice", "paid_amount", (frm) => {
this.bind_paid_amount_event(frm);
});
frappe.ui.form.on("Sales Invoice", "loyalty_amount", (frm) => {
this.bind_loyalty_amount_event(frm);
});
frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
@@ -250,6 +226,43 @@ erpnext.PointOfSale.Payment = class {
});
}
bind_coupon_code_event(frm) {
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
if (!frm.doc.ignore_pricing_rule) {
frm.applying_pos_coupon_code = true;
frappe.run_serially([
() => (frm.doc.ignore_pricing_rule = 1),
() => frm.trigger("ignore_pricing_rule"),
() => (frm.doc.ignore_pricing_rule = 0),
() => frm.trigger("apply_pricing_rule"),
() => frm.save(),
() => this.update_totals_section(frm.doc),
() => (frm.applying_pos_coupon_code = false),
]);
} else if (frm.doc.ignore_pricing_rule) {
frappe.show_alert({
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
indicator: "orange",
});
}
}
}
bind_paid_amount_event(frm) {
this.update_totals_section(frm.doc);
// need to re calculate cash shortcuts after discount is applied
const is_cash_shortcuts_invisible = !this.$payment_modes.find(".cash-shortcuts").is(":visible");
this.attach_cash_shortcuts(frm.doc);
!is_cash_shortcuts_invisible && this.$payment_modes.find(".cash-shortcuts").css("display", "grid");
this.render_payment_mode_dom();
}
bind_loyalty_amount_event(frm) {
const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency);
this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency);
}
setup_listener_for_payments() {
frappe.realtime.on("process_phone_payment", (data) => {
const doc = this.events.get_frm().doc;

View File

@@ -3,15 +3,17 @@
import frappe
from frappe import _
field_map = {
"Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"],
"Contact": ["name", "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"],
"Address": [
"name",
"address_line1",
"address_line2",
"pincode",
"city",
"state",
"pincode",
"country",
"is_primary_address",
],
@@ -29,6 +31,12 @@ def get_columns(filters):
columns = [
f"{party_type}:Link/{party_type}",
f"{frappe.unscrub(str(party_type_value))}::150",
{
"label": _("Address"),
"fieldtype": "Link",
"options": "Address",
"hidden": 1,
},
"Address Line 1",
"Address Line 2",
"Postal Code",
@@ -36,6 +44,7 @@ def get_columns(filters):
"State",
"Country",
"Is Primary Address:Check",
{"label": _("Contact"), "fieldtype": "Link", "options": "Contact", "hidden": 1},
"First Name",
"Last Name",
"Phone",
@@ -43,14 +52,10 @@ def get_columns(filters):
"Email Id",
"Is Primary Contact:Check",
]
if filters.get("party_type") == "Supplier" and frappe.db.get_single_value(
"Buying Settings", "supp_master_name"
) == ["Naming Series", "Auto Name"]:
columns.insert(1, "Supplier Name:Data:150")
if filters.get("party_type") == "Customer" and frappe.db.get_single_value(
"Selling Settings", "cust_master_name"
) == ["Naming Series", "Auto Name"]:
columns.insert(1, "Customer Name:Data:150")
if should_add_party_name(party_type):
columns.insert(2, f"{party_type} Name:Data:150")
return columns
@@ -72,6 +77,7 @@ def get_party_addresses_and_contact(party_type, party, party_group, filters):
if party:
query_filters = {"name": party}
if filters.get("party_type") in ["Customer", "Supplier"]:
field = filters.get("party_type").lower() + "_name"
else:
@@ -93,14 +99,18 @@ def get_party_addresses_and_contact(party_type, party, party_group, filters):
party_details = get_party_details(party_type, party_list, "Address", party_details)
party_details = get_party_details(party_type, party_list, "Contact", party_details)
add_party_name = should_add_party_name(party_type)
for party, details in party_details.items():
addresses = details.get("address", [])
contacts = details.get("contact", [])
if not any([addresses, contacts]):
result = [party]
result.append(party_groups[party])
if filters.get("party_type") in ["Customer", "Supplier"]:
if add_party_name:
result.append(party_name_map[party])
result.extend(add_blank_columns_for("Contact"))
result.extend(add_blank_columns_for("Address"))
data.append(result)
@@ -112,8 +122,10 @@ def get_party_addresses_and_contact(party_type, party, party_group, filters):
for idx in range(0, max_length):
result = [party]
result.append(party_groups[party])
if filters.get("party_type") in ["Customer", "Supplier"]:
if add_party_name:
result.append(party_name_map[party])
address = addresses[idx] if idx < len(addresses) else add_blank_columns_for("Address")
contact = contacts[idx] if idx < len(contacts) else add_blank_columns_for("Contact")
result.extend(address)
@@ -130,9 +142,11 @@ def get_party_details(party_type, party_list, doctype, party_details):
fields = ["`tabDynamic Link`.link_name", *field_map.get(doctype, [])]
records = frappe.get_list(doctype, filters=filters, fields=fields, as_list=True)
for d in records:
details = party_details.get(d[0])
details.setdefault(frappe.scrub(doctype), []).append(d[1:])
return party_details
@@ -151,3 +165,16 @@ def get_party_group(party_type):
}
return group[party_type]
def should_add_party_name(party_type):
settings_map = {
"Supplier": ("Buying Settings", "supp_master_name"),
"Customer": ("Selling Settings", "cust_master_name"),
}
if party_type in settings_map:
doctype, fieldname = settings_map.get(party_type)
return frappe.db.get_single_value(doctype, fieldname) in ["Naming Series", "Auto Name"]
return False

View File

@@ -1,48 +0,0 @@
from frappe import _
doctype_rule_map = {
"Item": {"points": 5, "for_doc_event": "New"},
"Customer": {"points": 5, "for_doc_event": "New"},
"Supplier": {"points": 5, "for_doc_event": "New"},
"Lead": {"points": 2, "for_doc_event": "New"},
"Opportunity": {
"points": 10,
"for_doc_event": "Custom",
"condition": 'doc.status=="Converted"',
"rule_name": _("On Converting Opportunity"),
"user_field": "converted_by",
},
"Sales Order": {
"points": 10,
"for_doc_event": "Submit",
"rule_name": _("On Sales Order Submission"),
"user_field": "modified_by",
},
"Purchase Order": {
"points": 10,
"for_doc_event": "Submit",
"rule_name": _("On Purchase Order Submission"),
"user_field": "modified_by",
},
"Task": {
"points": 5,
"condition": 'doc.status == "Completed"',
"rule_name": _("On Task Completion"),
"user_field": "completed_by",
},
}
def get_default_energy_point_rules():
return [
{
"doctype": "Energy Point Rule",
"reference_doctype": doctype,
"for_doc_event": rule.get("for_doc_event") or "Custom",
"condition": rule.get("condition"),
"rule_name": rule.get("rule_name") or _("On {0} Creation").format(doctype),
"points": rule.get("points"),
"user_field": rule.get("user_field") or "owner",
}
for doctype, rule in doctype_rule_map.items()
]

View File

@@ -281,6 +281,7 @@ class Company(NestedSet):
frappe.clear_cache()
def create_default_warehouses(self):
parent_warehouse = None
for wh_detail in [
{"warehouse_name": _("All Warehouses"), "is_group": 1},
{"warehouse_name": _("Stores"), "is_group": 0},
@@ -288,22 +289,31 @@ class Company(NestedSet):
{"warehouse_name": _("Finished Goods"), "is_group": 0},
{"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"},
]:
if not frappe.db.exists("Warehouse", "{} - {}".format(wh_detail["warehouse_name"], self.abbr)):
warehouse = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": wh_detail["warehouse_name"],
"is_group": wh_detail["is_group"],
"company": self.name,
"parent_warehouse": "{} - {}".format(_("All Warehouses"), self.abbr)
if not wh_detail["is_group"]
else "",
"warehouse_type": wh_detail.get("warehouse_type"),
}
)
warehouse.flags.ignore_permissions = True
warehouse.flags.ignore_mandatory = True
warehouse.insert()
if frappe.db.exists(
"Warehouse",
{
"warehouse_name": wh_detail["warehouse_name"],
"company": self.name,
},
):
continue
warehouse = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": wh_detail["warehouse_name"],
"is_group": wh_detail["is_group"],
"company": self.name,
"parent_warehouse": parent_warehouse,
"warehouse_type": wh_detail.get("warehouse_type"),
}
)
warehouse.flags.ignore_permissions = True
warehouse.flags.ignore_mandatory = True
warehouse.insert()
if wh_detail["is_group"]:
parent_warehouse = warehouse.name
def create_default_accounts(self):
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts

View File

@@ -8,7 +8,6 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.utils import cint
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from .default_success_action import get_default_success_action
@@ -26,7 +25,6 @@ def after_install():
create_marketgin_campagin_custom_fields()
add_all_roles_to("Administrator")
create_default_success_action()
create_default_energy_point_rules()
create_incoterms()
create_default_role_profiles()
add_company_to_session_defaults()
@@ -147,18 +145,6 @@ def create_default_success_action():
doc.insert(ignore_permissions=True)
def create_default_energy_point_rules():
for rule in get_default_energy_point_rules():
# check if any rule for ref. doctype exists
rule_exists = frappe.db.exists(
"Energy Point Rule", {"reference_doctype": rule.get("reference_doctype")}
)
if rule_exists:
continue
doc = frappe.get_doc(rule)
doc.insert(ignore_permissions=True)
def add_company_to_session_defaults():
settings = frappe.get_single("Session Default Settings")
settings.append("session_defaults", {"ref_doctype": "Company"})

View File

@@ -2,5 +2,20 @@
// For license information, please see license.txt
frappe.ui.form.on("Bin", {
refresh: function (frm) {},
refresh(frm) {
frm.trigger("recalculate_bin_quantity");
},
recalculate_bin_quantity(frm) {
frm.add_custom_button(__("Recalculate Bin Qty"), () => {
frappe.call({
method: "recalculate_qty",
freeze: true,
doc: frm.doc,
callback: function (r) {
frappe.show_alert(__("Bin Qty Recalculated"), 2);
},
});
});
},
});

Some files were not shown because too many files have changed in this diff Show More