mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-18 04:12:37 +00:00
Compare commits
184 Commits
v15.46.2
...
automatch-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e5cf18066 | ||
|
|
01254da4e0 | ||
|
|
dadc8266dc | ||
|
|
100b4e9274 | ||
|
|
59af144e29 | ||
|
|
452dffab48 | ||
|
|
eff12cbfbe | ||
|
|
89155f529e | ||
|
|
642b89782d | ||
|
|
3194807a41 | ||
|
|
2333d33362 | ||
|
|
622bfa6633 | ||
|
|
e22771c729 | ||
|
|
5b066f4a59 | ||
|
|
9ecafdc680 | ||
|
|
05763d226a | ||
|
|
0f1c6ff1c9 | ||
|
|
8874f4a9e4 | ||
|
|
21a83c508a | ||
|
|
90b8860a40 | ||
|
|
9daabfca8a | ||
|
|
10a4b54a67 | ||
|
|
c924feb0d0 | ||
|
|
cfa062df86 | ||
|
|
2e67a33412 | ||
|
|
03b06fc3ff | ||
|
|
500deff3e9 | ||
|
|
2c487af2df | ||
|
|
4dfc5a664a | ||
|
|
d3ea8b8e77 | ||
|
|
6247d5aadb | ||
|
|
85167bf934 | ||
|
|
b5f6926140 | ||
|
|
c615df5ac4 | ||
|
|
d26d0c6282 | ||
|
|
d31b0a507f | ||
|
|
bd12c1475a | ||
|
|
f9d038ee4a | ||
|
|
00102a15e3 | ||
|
|
3049027f43 | ||
|
|
ab87265395 | ||
|
|
163af91c37 | ||
|
|
b3b808335f | ||
|
|
66544bfa10 | ||
|
|
85ba96e0f3 | ||
|
|
1a1476afa4 | ||
|
|
e7f4a9bf77 | ||
|
|
f09acc784f | ||
|
|
0a2cc6bcd7 | ||
|
|
52bdf5b170 | ||
|
|
8d650e56ba | ||
|
|
e09fb87597 | ||
|
|
a11aee3ab3 | ||
|
|
92ad2ce554 | ||
|
|
3533c25969 | ||
|
|
aff83051a6 | ||
|
|
b135a684a5 | ||
|
|
885dd31c5c | ||
|
|
6703a457fe | ||
|
|
8885b07114 | ||
|
|
6f7138996a | ||
|
|
8d5fe20c7c | ||
|
|
b8922823a3 | ||
|
|
20efe7bb80 | ||
|
|
ea4b6e8dd7 | ||
|
|
00ae829d89 | ||
|
|
839ffb3f2a | ||
|
|
d6903fbc8d | ||
|
|
9853bd9ba1 | ||
|
|
c33e07550c | ||
|
|
772b7b95ac | ||
|
|
2e9c507dfa | ||
|
|
e3dc5d0de4 | ||
|
|
2efc701e4e | ||
|
|
1353a14a6b | ||
|
|
8b4f5261b4 | ||
|
|
74220430e5 | ||
|
|
d550b433c1 | ||
|
|
8e55d5cc39 | ||
|
|
4a74ee7b6e | ||
|
|
c19725ca74 | ||
|
|
7b90742409 | ||
|
|
583182180a | ||
|
|
28442f3414 | ||
|
|
cc827c8077 | ||
|
|
40f46b76fa | ||
|
|
7dd2b0c189 | ||
|
|
3a361eac4e | ||
|
|
61ee292957 | ||
|
|
c2d9ac11f0 | ||
|
|
2feeebb5fb | ||
|
|
f6a9051291 | ||
|
|
e582ff862e | ||
|
|
98631eb266 | ||
|
|
cfa432dbca | ||
|
|
a11f7d5a82 | ||
|
|
f83112520d | ||
|
|
2f279a6eb4 | ||
|
|
6c206c1cb3 | ||
|
|
6bb2b76040 | ||
|
|
508435ac9f | ||
|
|
6cc70605fa | ||
|
|
488d8080c8 | ||
|
|
5cc9e10923 | ||
|
|
b45b77df93 | ||
|
|
991a3366a8 | ||
|
|
042d12b2c1 | ||
|
|
c241262266 | ||
|
|
9060e4ce57 | ||
|
|
61367ee1ed | ||
|
|
944dc966bc | ||
|
|
9c6832a622 | ||
|
|
771632a5e2 | ||
|
|
7a81c0f10f | ||
|
|
69ed2a9dfe | ||
|
|
57e6ed4645 | ||
|
|
b48f46ed5a | ||
|
|
72b720477c | ||
|
|
5ca60f3e0a | ||
|
|
ddc58f0146 | ||
|
|
96cc9e29a1 | ||
|
|
ca7c229e86 | ||
|
|
90aadcdcbc | ||
|
|
7a5c30fe9b | ||
|
|
f0671d45de | ||
|
|
ddfead2cde | ||
|
|
516a325a31 | ||
|
|
d6001e5ef9 | ||
|
|
f32cf84413 | ||
|
|
8092d58d9c | ||
|
|
3f3df7ef2a | ||
|
|
0969877cd7 | ||
|
|
9543a4c66f | ||
|
|
3eb56cbbfb | ||
|
|
dc1ed406a1 | ||
|
|
2edb6f3224 | ||
|
|
4e23e3191d | ||
|
|
55470fefdb | ||
|
|
33fc987d95 | ||
|
|
01c1ed98ac | ||
|
|
8f8dd1c088 | ||
|
|
f005bef218 | ||
|
|
19a8ea217e | ||
|
|
4c5540aef9 | ||
|
|
cf4068d1a3 | ||
|
|
3ab4acfafa | ||
|
|
13123a0412 | ||
|
|
4884849f23 | ||
|
|
7498cdf644 | ||
|
|
bec1f972b3 | ||
|
|
497029f958 | ||
|
|
b0a10c6b2a | ||
|
|
875797e655 | ||
|
|
1a9dfec115 | ||
|
|
59841408ac | ||
|
|
a55aaea5a1 | ||
|
|
041d94f3cf | ||
|
|
58e846709e | ||
|
|
7f5c19a81e | ||
|
|
3d4a4e661c | ||
|
|
e6390bfba1 | ||
|
|
13a3c816d7 | ||
|
|
4869847bc7 | ||
|
|
b5596d98e3 | ||
|
|
d137f780bd | ||
|
|
16b013fab2 | ||
|
|
ad57e33cd7 | ||
|
|
58c0e24f2d | ||
|
|
162380d9da | ||
|
|
cc1834b0cc | ||
|
|
9449055b1e | ||
|
|
8f811728d9 | ||
|
|
98cc79d942 | ||
|
|
db9a319104 | ||
|
|
cdeec8d24c | ||
|
|
b706a8274f | ||
|
|
20324224d3 | ||
|
|
418ef81dbc | ||
|
|
0fd7792f49 | ||
|
|
7350aa299e | ||
|
|
8b810f5fb8 | ||
|
|
3ed7f761ba | ||
|
|
75aee42635 | ||
|
|
218e777423 |
1
.github/helper/documentation.py
vendored
1
.github/helper/documentation.py
vendored
@@ -10,6 +10,7 @@ WEBSITE_REPOS = [
|
||||
|
||||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"docs.frappe.io",
|
||||
"frappeframework.com",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.46.2"
|
||||
__version__ = "15.45.4"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"description": "Rate at which this tax is applied",
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"oldfieldname": "tax_rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
|
||||
@@ -237,19 +237,22 @@ frappe.treeview_settings["Account"] = {
|
||||
},
|
||||
post_render: function (treeview) {
|
||||
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
|
||||
treeview.page.set_primary_action(
|
||||
__("New"),
|
||||
function () {
|
||||
let root_company = treeview.page.fields_dict.root_company.get_value();
|
||||
|
||||
if (root_company) {
|
||||
frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]);
|
||||
} else {
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
"add"
|
||||
);
|
||||
if (treeview.can_create) {
|
||||
treeview.page.set_primary_action(
|
||||
__("New"),
|
||||
function () {
|
||||
let root_company = treeview.page.fields_dict.root_company.get_value();
|
||||
if (root_company) {
|
||||
frappe.throw(__("Please add the account to root level Company - {0}"), [
|
||||
root_company,
|
||||
]);
|
||||
} else {
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
"add"
|
||||
);
|
||||
}
|
||||
},
|
||||
toolbar: [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -101,7 +101,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"oldfieldname": "rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ class BankAccount(Document):
|
||||
self.name = self.account_name + " - " + self.bank
|
||||
|
||||
def on_trash(self):
|
||||
delete_contact_and_address("BankAccount", self.name)
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
|
||||
@@ -117,9 +117,9 @@ class BankClearance(Document):
|
||||
)
|
||||
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||
)
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||
get_amounts_not_reflected_in_system,
|
||||
get_entries,
|
||||
@@ -304,54 +305,56 @@ def create_payment_entry_bts(
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
paid_amount = bank_transaction.unallocated_amount
|
||||
|
||||
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
|
||||
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
payment_entry_dict = {
|
||||
"company": company,
|
||||
"payment_type": payment_type,
|
||||
"reference_no": reference_number,
|
||||
"reference_date": reference_date,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"posting_date": posting_date,
|
||||
"paid_amount": paid_amount,
|
||||
"received_amount": paid_amount,
|
||||
}
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_cached_value("Account", bank_account, "company")
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
|
||||
payment_entry.update(payment_entry_dict)
|
||||
bank_currency = bank_transaction.currency
|
||||
party_currency = frappe.get_cached_value("Account", party_account, "account_currency")
|
||||
|
||||
if mode_of_payment:
|
||||
payment_entry.mode_of_payment = mode_of_payment
|
||||
if project:
|
||||
payment_entry.project = project
|
||||
if cost_center:
|
||||
payment_entry.cost_center = cost_center
|
||||
if payment_type == "Receive":
|
||||
payment_entry.paid_to = company_account
|
||||
else:
|
||||
payment_entry.paid_from = company_account
|
||||
exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date)
|
||||
|
||||
payment_entry.validate()
|
||||
amt_in_bank_acc_currency = bank_transaction.unallocated_amount
|
||||
amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = payment_type
|
||||
pe.company = company
|
||||
pe.reference_no = reference_number
|
||||
pe.reference_date = reference_date
|
||||
pe.party_type = party_type
|
||||
pe.party = party
|
||||
pe.posting_date = posting_date
|
||||
pe.paid_from = party_account if payment_type == "Receive" else bank_account
|
||||
pe.paid_to = party_account if payment_type == "Pay" else bank_account
|
||||
pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency
|
||||
pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency
|
||||
pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency
|
||||
pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency
|
||||
pe.mode_of_payment = mode_of_payment
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
return payment_entry
|
||||
return pe
|
||||
|
||||
payment_entry.insert()
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
payment_entry.submit()
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment_entry.name,
|
||||
"amount": paid_amount,
|
||||
"payment_name": pe.name,
|
||||
"amount": amt_in_bank_acc_currency,
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -480,8 +483,12 @@ def get_linked_payments(
|
||||
def subtract_allocations(gl_account, vouchers):
|
||||
"Look up & subtract any existing Bank Transaction allocations"
|
||||
copied = []
|
||||
|
||||
voucher_docs = [(voucher.get("doctype"), voucher.get("name")) for voucher in vouchers]
|
||||
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
|
||||
|
||||
for voucher in vouchers:
|
||||
rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
|
||||
rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or []
|
||||
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
|
||||
|
||||
if amount := None if not filtered_row else filtered_row[0]["total"]:
|
||||
@@ -719,7 +726,7 @@ def get_pe_matching_query(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.base_paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
|
||||
@@ -49,41 +49,39 @@ class AutoMatchbyAccountIBAN:
|
||||
return result
|
||||
|
||||
def match_account_in_party(self) -> tuple | None:
|
||||
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
or_filters = self.get_or_filters()
|
||||
"""
|
||||
Returns (Party Type, Party) if a matching account is found in Bank Account or Employee:
|
||||
1. Get party from a matching (iban/account no) Bank Account
|
||||
2. If not found, get party from Employee with matching bank account details (iban/account no)
|
||||
"""
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
# Nothing to match
|
||||
return None
|
||||
|
||||
for party in parties:
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
|
||||
)
|
||||
# Search for a matching Bank Account that has party set
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account",
|
||||
or_filters=self.get_or_filters(),
|
||||
filters={"party_type": ("is", "set"), "party": ("is", "set")},
|
||||
fields=["party", "party_type"],
|
||||
limit_page_length=1,
|
||||
)
|
||||
if result := party_result[0] if party_result else None:
|
||||
return (result["party_type"], result["party"])
|
||||
|
||||
if party == "Employee" and not party_result:
|
||||
# Search in Bank Accounts first for Employee, and then Employee record
|
||||
if "bank_account_no" in or_filters:
|
||||
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
|
||||
# If no party is found, search in Employee (since it has bank account details)
|
||||
employee_result = frappe.db.get_all(
|
||||
"Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1
|
||||
)
|
||||
if employee_result:
|
||||
return ("Employee", employee_result[0])
|
||||
|
||||
party_result = frappe.db.get_all(
|
||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||
)
|
||||
|
||||
if "bank_ac_no" in or_filters:
|
||||
or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
|
||||
|
||||
if party_result:
|
||||
result = (
|
||||
party,
|
||||
party_result[0],
|
||||
)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def get_or_filters(self) -> dict:
|
||||
def get_or_filters(self, party: str | None = None) -> dict:
|
||||
"""Return OR filters for Bank Account and IBAN"""
|
||||
or_filters = {}
|
||||
if self.bank_party_account_number:
|
||||
or_filters["bank_account_no"] = self.bank_party_account_number
|
||||
bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no"
|
||||
or_filters[bank_ac_field] = self.bank_party_account_number
|
||||
|
||||
if self.bank_party_iban:
|
||||
or_filters["iban"] = self.bank_party_iban
|
||||
|
||||
@@ -154,10 +154,16 @@ class BankTransaction(Document):
|
||||
"""
|
||||
remaining_amount = self.unallocated_amount
|
||||
to_remove = []
|
||||
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
|
||||
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
self, payment_entry
|
||||
self,
|
||||
payment_entry,
|
||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
|
||||
or [],
|
||||
)
|
||||
|
||||
if 0.0 == unallocated_amount:
|
||||
@@ -232,7 +238,7 @@ def get_doctypes_for_bank_reconciliation():
|
||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||
|
||||
|
||||
def get_clearance_details(transaction, payment_entry):
|
||||
def get_clearance_details(transaction, payment_entry, bt_allocations):
|
||||
"""
|
||||
There should only be one bank gle for a voucher.
|
||||
Could be none for a Bank Transaction.
|
||||
@@ -241,7 +247,6 @@ def get_clearance_details(transaction, payment_entry):
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
@@ -294,44 +299,52 @@ def get_related_bank_gl_entries(doctype, docname):
|
||||
)
|
||||
|
||||
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
def get_total_allocated_amount(docs):
|
||||
"""
|
||||
Gets the sum of allocations for a voucher on each bank GL account
|
||||
along with the latest bank transaction name & date
|
||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||
"""
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT total, latest_name, latest_date, gl_account FROM (
|
||||
SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER w AS rownum,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
||||
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||
ba.account AS gl_account
|
||||
ba.account AS gl_account,
|
||||
btp.payment_document,
|
||||
btp.payment_entry
|
||||
FROM
|
||||
`tabBank Transaction Payments` btp
|
||||
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
|
||||
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
|
||||
WHERE
|
||||
btp.payment_document = %(doctype)s
|
||||
AND btp.payment_entry = %(docname)s
|
||||
(btp.payment_document, btp.payment_entry) IN %(docs)s
|
||||
AND bt.docstatus = 1
|
||||
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
|
||||
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
|
||||
) temp
|
||||
WHERE
|
||||
rownum = 1
|
||||
""",
|
||||
dict(doctype=doctype, docname=docname),
|
||||
dict(docs=docs),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
payment_allocation_details = {}
|
||||
for row in result:
|
||||
# Why is this *sometimes* a byte string?
|
||||
if isinstance(row["latest_name"], bytes):
|
||||
row["latest_name"] = row["latest_name"].decode()
|
||||
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||
return result
|
||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
|
||||
|
||||
return payment_allocation_details
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
|
||||
@@ -490,13 +490,20 @@ def get_actual_expense(args):
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
for d in frappe.db.sql(
|
||||
"""select mdp.month, mdp.percentage_allocation
|
||||
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
|
||||
where mdp.parent=md.name and md.fiscal_year=%s""",
|
||||
fiscal_year,
|
||||
as_dict=1,
|
||||
):
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(mdp)
|
||||
.join(md)
|
||||
.on(mdp.parent == md.name)
|
||||
.select(mdp.month, mdp.percentage_allocation)
|
||||
.where(md.fiscal_year == fiscal_year)
|
||||
.where(md.name == monthly_distribution)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for d in res:
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
|
||||
@@ -13,7 +13,11 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
|
||||
from erpnext.accounts.party import (
|
||||
validate_account_party_type,
|
||||
validate_party_frozen_disabled,
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
@@ -268,8 +272,12 @@ class GLEntry(Document):
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
if self.is_cancelled:
|
||||
return
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
account_currency = get_account_currency(self.account)
|
||||
|
||||
|
||||
@@ -79,3 +79,48 @@ class TestGLEntry(unittest.TestCase):
|
||||
"SELECT current from tabSeries where name = %s", naming_series
|
||||
)[0][0]
|
||||
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
|
||||
|
||||
def test_validate_account_party_type(self):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Supplier"
|
||||
break
|
||||
|
||||
jv.save()
|
||||
try:
|
||||
jv.submit()
|
||||
except Exception as e:
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Customer"
|
||||
break
|
||||
|
||||
jv1.save()
|
||||
try:
|
||||
jv1.submit()
|
||||
except Exception as e:
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
@@ -27,6 +27,18 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
// project excluded in setup_dimension_filters
|
||||
frm.set_query("project", function (doc) {
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (doc.party_type == "Customer") filters.customer = doc.party;
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
}
|
||||
|
||||
@@ -1479,7 +1479,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Paid",
|
||||
company=company,
|
||||
account_type="Liability",
|
||||
account_type="Payable",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -58,7 +58,9 @@
|
||||
"payment_account",
|
||||
"payment_channel",
|
||||
"payment_order",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"column_break_iiuv",
|
||||
"phone_number"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -376,6 +378,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel==\"Phone\"",
|
||||
"fetch_from": "payment_gateway_account.payment_channel",
|
||||
"fieldname": "payment_channel",
|
||||
"fieldtype": "Select",
|
||||
@@ -429,13 +432,22 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iiuv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Number"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-10-23 12:23:40.117336",
|
||||
"modified": "2024-12-27 21:29:10.361894",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
||||
@@ -224,6 +224,7 @@ class PaymentRequest(Document):
|
||||
sender=self.email_to,
|
||||
currency=self.currency,
|
||||
payment_gateway=self.payment_gateway,
|
||||
phone_number=self.phone_number,
|
||||
)
|
||||
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
@@ -635,6 +636,7 @@ def make_payment_request(**args):
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
"party_name": args.get("party_name") or ref_doc.get("customer_name"),
|
||||
"phone_number": args.get("phone_number") if args.get("phone_number") else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Grand Total') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Grand Total") }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Net Total') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Net Total") }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Total Quantity') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Total Quantity") }}</td>
|
||||
<td class='text-right'>{{ data.total_quantity or '' }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<tbody>
|
||||
{% for d in data.payment_reconciliation %}
|
||||
<tr>
|
||||
<td class="text-left">{{ d.mode_of_payment }}</td>
|
||||
<td class="text-left">{{ _(d.mode_of_payment) }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -63,7 +63,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ _("Account") }}</th>
|
||||
<th class="text-left">{{ _("Rate") }}</th>
|
||||
<th class="text-left">{{ _("Tax Rate") }}</th>
|
||||
<th class="text-right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -117,11 +117,13 @@ class POSInvoiceMergeLog(Document):
|
||||
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
|
||||
|
||||
sales_invoice, credit_note = "", ""
|
||||
sales_invoice_doc = None
|
||||
if sales:
|
||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||
sales_invoice_doc = self.process_merging_into_sales_invoice(sales)
|
||||
sales_invoice = sales_invoice_doc.name
|
||||
|
||||
if returns:
|
||||
credit_note = self.process_merging_into_credit_note(returns, sales_invoice)
|
||||
credit_note = self.process_merging_into_credit_note(returns, sales_invoice_doc)
|
||||
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
@@ -152,15 +154,23 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice.name
|
||||
return sales_invoice
|
||||
|
||||
def process_merging_into_credit_note(self, data, sales_invoice):
|
||||
def process_merging_into_credit_note(self, data, sales_invoice_doc=None):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||
referenes = {}
|
||||
|
||||
credit_note.return_against = sales_invoice
|
||||
if sales_invoice_doc:
|
||||
credit_note.return_against = sales_invoice_doc.name
|
||||
|
||||
for d in sales_invoice_doc.items:
|
||||
referenes[d.item_code] = d.name
|
||||
|
||||
for d in credit_note.items:
|
||||
d.sales_invoice_item = referenes.get(d.item_code)
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
@@ -366,7 +376,12 @@ class POSInvoiceMergeLog(Document):
|
||||
return []
|
||||
|
||||
def cancel_linked_invoices(self):
|
||||
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
|
||||
invoices = [self.consolidated_invoice, self.consolidated_credit_note]
|
||||
if not invoices:
|
||||
return
|
||||
|
||||
invoices.reverse()
|
||||
for si_name in invoices:
|
||||
if not si_name:
|
||||
continue
|
||||
si = frappe.get_doc("Sales Invoice", si_name)
|
||||
|
||||
@@ -399,6 +399,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
hide_fields(this.frm.doc);
|
||||
if (cint(this.frm.doc.is_paid)) {
|
||||
this.frm.set_value("allocate_advances_automatically", 0);
|
||||
this.frm.set_value("payment_terms_template", "");
|
||||
this.frm.set_value("payment_schedule", []);
|
||||
if (!this.frm.doc.company) {
|
||||
this.frm.set_value("is_paid", 0);
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
|
||||
@@ -1677,7 +1677,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if pi:
|
||||
pi = pi[0][0]
|
||||
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
|
||||
|
||||
frappe.throw(
|
||||
_("Supplier Invoice No exists in Purchase Invoice {0}").format(
|
||||
get_link_to_form("Purchase Invoice", pi)
|
||||
)
|
||||
)
|
||||
|
||||
def update_billing_status_in_pr(self, update_modified=True):
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
|
||||
|
||||
@@ -1838,6 +1838,52 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_adjust_incoming_rate_for_rejected_item(self):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rejected_qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "warehouse": pi.items[0].warehouse},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": pr.name,
|
||||
"warehouse": pi.items[0].rejected_warehouse,
|
||||
},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertFalse(stock_value_difference)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_item_less_defaults(self):
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
pi.supplier = "_Test Supplier"
|
||||
@@ -2448,6 +2494,34 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(len(actual), 3)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_invoice_against_returned_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_purchase_invoice_from_pr,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_return_against_rejected_warehouse,
|
||||
)
|
||||
|
||||
item = make_item("_Test Item For Invoice Against Returned PR", properties={"is_stock_item": 1}).name
|
||||
|
||||
original_value = frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
|
||||
|
||||
pr = make_purchase_receipt(item_code=item, qty=5, rejected_qty=5, rate=100)
|
||||
pr_return = make_purchase_return_against_rejected_warehouse(pr.name)
|
||||
pr_return.submit()
|
||||
|
||||
pi = make_purchase_invoice_from_pr(pr.name)
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].qty, 5.0)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", original_value
|
||||
)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "180px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -40,7 +41,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -111,13 +112,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-23 21:13:18.013816",
|
||||
"modified": "2024-12-20 12:04:46.729972",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
|
||||
@@ -16,6 +16,7 @@ class PurchaseInvoiceAdvance(Document):
|
||||
|
||||
advance_amount: DF.Currency
|
||||
allocated_amount: DF.Currency
|
||||
difference_posting_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -507,7 +507,7 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
def validate_pos_paid_amount(self):
|
||||
if len(self.payments) == 0 and self.is_pos:
|
||||
if len(self.payments) == 0 and self.is_pos and flt(self.grand_total) > 0:
|
||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||
|
||||
def check_if_consolidated_invoice(self):
|
||||
@@ -1002,9 +1002,9 @@ class SalesInvoice(SellingController):
|
||||
def validate_pos(self):
|
||||
if self.is_return:
|
||||
invoice_total = self.rounded_total or self.grand_total
|
||||
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > 1.0 / (
|
||||
10.0 ** (self.precision("grand_total") + 1.0)
|
||||
):
|
||||
if abs(flt(self.paid_amount)) + abs(flt(self.write_off_amount)) - abs(
|
||||
flt(invoice_total)
|
||||
) > 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_warehouse(self):
|
||||
|
||||
@@ -43,6 +43,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.get_item_details import get_item_tax_map
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
@@ -2873,13 +2874,26 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
item.save()
|
||||
|
||||
sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True)
|
||||
item_tax_map = get_item_tax_map(
|
||||
company=sales_invoice.company,
|
||||
item_tax_template=sales_invoice.items[0].item_tax_template,
|
||||
)
|
||||
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
# Apply discount
|
||||
sales_invoice.apply_discount_on = "Net Total"
|
||||
sales_invoice.discount_amount = 300
|
||||
sales_invoice.save()
|
||||
|
||||
item_tax_map = get_item_tax_map(
|
||||
company=sales_invoice.company,
|
||||
item_tax_template=sales_invoice.items[0].item_tax_template,
|
||||
)
|
||||
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_sales_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "250px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -41,7 +42,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -112,13 +113,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-23 21:12:57.557731",
|
||||
"modified": "2024-12-20 11:58:28.962370",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
|
||||
@@ -16,6 +16,7 @@ class SalesInvoiceAdvance(Document):
|
||||
|
||||
advance_amount: DF.Currency
|
||||
allocated_amount: DF.Currency
|
||||
difference_posting_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -247,14 +247,14 @@ def get_tax_row_for_tds(tax_details, tax_amount):
|
||||
}
|
||||
|
||||
|
||||
def get_lower_deduction_certificate(company, tax_details, pan_no):
|
||||
def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
||||
ldc_name = frappe.db.get_value(
|
||||
"Lower Deduction Certificate",
|
||||
{
|
||||
"pan_no": pan_no,
|
||||
"tax_withholding_category": tax_details.tax_withholding_category,
|
||||
"valid_from": (">=", tax_details.from_date),
|
||||
"valid_upto": ("<=", tax_details.to_date),
|
||||
"valid_from": ("<=", posting_date),
|
||||
"valid_upto": (">=", posting_date),
|
||||
"company": company,
|
||||
},
|
||||
"name",
|
||||
@@ -302,7 +302,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
|
||||
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
if ldc:
|
||||
@@ -539,7 +539,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
)
|
||||
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.tax_withholding_net_total
|
||||
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
if type.payment_type == "Pay":
|
||||
@@ -551,9 +551,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.base_tax_withholding_net_total
|
||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
||||
else:
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
|
||||
@@ -759,6 +759,20 @@ def validate_party_frozen_disabled(party_type, party_name):
|
||||
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
|
||||
|
||||
|
||||
def validate_account_party_type(self):
|
||||
if self.is_cancelled:
|
||||
return
|
||||
|
||||
if self.party_type and self.party:
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type and (account_type not in ["Receivable", "Payable"]):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
|
||||
).format(self.account)
|
||||
)
|
||||
|
||||
|
||||
def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
|
||||
|
||||
|
||||
@@ -134,7 +134,6 @@ class ReceivablePayableReport:
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
@@ -150,6 +149,9 @@ class ReceivablePayableReport:
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -275,9 +277,6 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
|
||||
@@ -142,7 +142,8 @@ def get_journal_entries(filters):
|
||||
where jvd.parent = jv.name and jv.docstatus=1
|
||||
and jvd.account = %(account)s and jv.posting_date <= %(report_date)s
|
||||
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
|
||||
and ifnull(jv.is_opening, 'No') = 'No'""",
|
||||
and ifnull(jv.is_opening, 'No') = 'No'
|
||||
and jv.company = %(company)s """,
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -163,6 +164,7 @@ def get_payment_entries(filters):
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date <= %(report_date)s
|
||||
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
|
||||
and company = %(company)s
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
@@ -181,6 +183,7 @@ def get_pos_entries(filters):
|
||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
||||
and account.name = sip.account and si.posting_date <= %(report_date)s and
|
||||
ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s
|
||||
and si.company = %(company)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""",
|
||||
|
||||
@@ -350,7 +350,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
|
||||
gl_entries_by_account,
|
||||
accounts_by_name,
|
||||
accounts,
|
||||
ignore_closing_entries=False,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
root_type=root_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -637,6 +637,7 @@ class GrossProfitGenerator:
|
||||
packed_item_row = row.copy()
|
||||
packed_item_row.warehouse = packed_item.warehouse
|
||||
packed_item_row.qty = packed_item.total_qty * -1
|
||||
packed_item_row.serial_and_batch_bundle = packed_item.serial_and_batch_bundle
|
||||
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
|
||||
|
||||
return flt(buying_amount, self.currency_precision)
|
||||
@@ -728,6 +729,7 @@ class GrossProfitGenerator:
|
||||
"voucher_no": row.parent,
|
||||
"allow_zero_valuation": True,
|
||||
"company": self.filters.company,
|
||||
"item_code": item_code,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -748,12 +750,13 @@ class GrossProfitGenerator:
|
||||
.inner_join(purchase_invoice)
|
||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||
.select(
|
||||
purchase_invoice.name,
|
||||
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||
)
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||
.where(purchase_invoice_item.item_code == item_code)
|
||||
.where(purchase_invoice.is_return == 0)
|
||||
.where(purchase_invoice_item.parenttype == "Purchase Invoice")
|
||||
)
|
||||
|
||||
if row.project:
|
||||
@@ -996,6 +999,7 @@ class GrossProfitGenerator:
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"invoice": row.parent,
|
||||
"serial_and_batch_bundle": row.serial_and_batch_bundle,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1047,6 +1051,7 @@ class GrossProfitGenerator:
|
||||
pki.rate,
|
||||
(pki.rate * pki.qty).as_("base_amount"),
|
||||
pki.parent_detail_docname,
|
||||
pki.serial_and_batch_bundle,
|
||||
)
|
||||
.where(pki.docstatus == 1)
|
||||
)
|
||||
|
||||
@@ -318,7 +318,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Rate"),
|
||||
"label": _("Tax Rate"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"options": "currency",
|
||||
|
||||
@@ -70,10 +70,10 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry":
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
if rate:
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
|
||||
total_amount = grand_total = base_total
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
|
||||
(voucher_type, name)
|
||||
@@ -405,7 +405,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
"paid_amount_after_tax",
|
||||
"base_paid_amount",
|
||||
],
|
||||
"Journal Entry": ["total_amount"],
|
||||
"Journal Entry": ["tax_withholding_category", "total_debit"],
|
||||
}
|
||||
|
||||
entries = frappe.get_all(
|
||||
@@ -427,7 +427,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
elif doctype == "Payment Entry":
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
else:
|
||||
value = [entry.total_amount] * 3
|
||||
value = [entry.total_debit] * 3
|
||||
|
||||
net_total_map[(doctype, entry.name)] = value
|
||||
|
||||
|
||||
@@ -130,7 +130,9 @@ def get_fiscal_years(
|
||||
else:
|
||||
return ((fy.name, fy.year_start_date, fy.year_end_date),)
|
||||
|
||||
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
|
||||
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(
|
||||
_(label), formatdate(transaction_date)
|
||||
)
|
||||
if company:
|
||||
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from frappe.utils import (
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_comma_separated_links,
|
||||
@@ -308,12 +309,14 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
def validate_precision(self):
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(self.gross_purchase_amount, float_precision)
|
||||
self.gross_purchase_amount = flt(
|
||||
self.gross_purchase_amount, self.precision("gross_purchase_amount")
|
||||
)
|
||||
|
||||
if self.opening_accumulated_depreciation:
|
||||
self.opening_accumulated_depreciation = flt(
|
||||
self.opening_accumulated_depreciation, float_precision
|
||||
self.opening_accumulated_depreciation, self.precision("opening_accumulated_depreciation")
|
||||
)
|
||||
|
||||
def validate_asset_values(self):
|
||||
@@ -487,11 +490,7 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_expected_value_after_useful_life(self):
|
||||
for row in self.get("finance_books"):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
)
|
||||
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
||||
|
||||
if not depr_schedule:
|
||||
continue
|
||||
|
||||
@@ -889,6 +888,7 @@ def get_asset_naming_series():
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(asset, item_code, company, serial_no=None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
si.company = company
|
||||
si.currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
@@ -905,6 +905,16 @@ def make_sales_invoice(asset, item_code, company, serial_no=None):
|
||||
"qty": 1,
|
||||
},
|
||||
)
|
||||
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
si.update(
|
||||
{
|
||||
dimension["fieldname"]: asset_doc.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
si.set_missing_values()
|
||||
return si
|
||||
|
||||
|
||||
@@ -889,7 +889,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
["2030-12-31", 28630.14, 28630.14],
|
||||
["2031-12-31", 35684.93, 64315.07],
|
||||
["2032-12-31", 17842.46, 82157.53],
|
||||
["2033-06-06", 5342.46, 87499.99],
|
||||
["2033-06-06", 5342.47, 87500.00],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
|
||||
@@ -344,7 +344,7 @@ class AssetDepreciationSchedule(Document):
|
||||
date_of_disposal,
|
||||
original_schedule_date=schedule_date,
|
||||
)
|
||||
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
if depreciation_amount > 0:
|
||||
self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
|
||||
|
||||
@@ -430,6 +430,7 @@ class AssetDepreciationSchedule(Document):
|
||||
|
||||
if not depreciation_amount:
|
||||
continue
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
value_after_depreciation = flt(
|
||||
value_after_depreciation - flt(depreciation_amount),
|
||||
asset_doc.precision("gross_purchase_amount"),
|
||||
@@ -443,6 +444,7 @@ class AssetDepreciationSchedule(Document):
|
||||
depreciation_amount += flt(value_after_depreciation) - flt(
|
||||
row.expected_value_after_useful_life
|
||||
)
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
skip_row = True
|
||||
|
||||
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
|
||||
@@ -517,10 +519,13 @@ class AssetDepreciationSchedule(Document):
|
||||
i - 1
|
||||
].accumulated_depreciation_amount
|
||||
else:
|
||||
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||
accumulated_depreciation = flt(
|
||||
self.opening_accumulated_depreciation,
|
||||
asset_doc.precision("opening_accumulated_depreciation"),
|
||||
)
|
||||
|
||||
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
value_after_depreciation -= flt(d.depreciation_amount)
|
||||
value_after_depreciation = flt(value_after_depreciation, d.precision("depreciation_amount"))
|
||||
|
||||
# for the last row, if depreciation method = Straight Line
|
||||
if (
|
||||
@@ -530,12 +535,11 @@ class AssetDepreciationSchedule(Document):
|
||||
and not date_of_return
|
||||
and not row.shift_based
|
||||
):
|
||||
depreciation_amount += flt(
|
||||
d.depreciation_amount += flt(
|
||||
value_after_depreciation - flt(row.expected_value_after_useful_life),
|
||||
d.precision("depreciation_amount"),
|
||||
)
|
||||
|
||||
d.depreciation_amount = depreciation_amount
|
||||
accumulated_depreciation += d.depreciation_amount
|
||||
d.accumulated_depreciation_amount = flt(
|
||||
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
|
||||
|
||||
@@ -400,11 +400,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cur_frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
this.make_subcontracting_order,
|
||||
__("Create")
|
||||
);
|
||||
if (!doc.items.every((item) => item.qty == item.sco_qty)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
me.make_subcontracting_order();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,27 +867,40 @@ def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_order(source_name, target_doc=None, save=False, submit=False, notify=False):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
if not is_po_fully_subcontracted(source_name):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
return target_doc
|
||||
else:
|
||||
frappe.throw(_("This PO has been fully subcontracted."))
|
||||
|
||||
|
||||
def is_po_fully_subcontracted(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.sco_qty))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
@@ -931,7 +944,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": [],
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.sco_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
@@ -939,12 +953,3 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_subcontracting_order_created(po_name) -> bool:
|
||||
return (
|
||||
True
|
||||
if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]})
|
||||
else False
|
||||
)
|
||||
|
||||
@@ -1004,7 +1004,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
)
|
||||
|
||||
def update_items(po, qty):
|
||||
trans_items = [po.items[0].as_dict()]
|
||||
trans_items = [po.items[0].as_dict().update({"docname": po.items[0].name})]
|
||||
trans_items[0]["qty"] = qty
|
||||
trans_items[0]["fg_item_qty"] = qty
|
||||
trans_items = json.dumps(trans_items, default=str)
|
||||
@@ -1059,6 +1059,73 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertEqual(po.items[0].qty, 30)
|
||||
self.assertEqual(po.items[0].fg_item_qty, 30)
|
||||
|
||||
def test_new_sc_flow(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order
|
||||
|
||||
po = create_po_for_sc_testing()
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
sco.items[0].qty = 5
|
||||
sco.items.pop(1)
|
||||
sco.items[1].qty = 25
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 1: Quantity of Service Items should change based on change in Quantity of its corresponding Finished Goods Item
|
||||
self.assertEqual(sco.service_items[0].qty, 5)
|
||||
|
||||
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].sco_qty, 5)
|
||||
self.assertEqual(po.items[1].sco_qty, 0)
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
|
||||
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
|
||||
self.assertEqual(sco.items[0].amount, 2000)
|
||||
self.assertEqual(sco.service_items[0].amount, 500)
|
||||
|
||||
# Test - 4: Service Items should be removed if its corresponding Finished Good line item is deleted
|
||||
self.assertEqual(len(sco.service_items), 2)
|
||||
|
||||
# Test - 5: Service Item quantity calculation should be based upon conversion factor calculated from its corresponding PO Item
|
||||
self.assertEqual(sco.service_items[1].qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
sco.items[0].qty = 6
|
||||
|
||||
# Test - 6: Saving document should not be allowed if Quantity exceeds available Subcontracting Quantity of any Purchase Order Item
|
||||
self.assertRaises(frappe.ValidationError, sco.save)
|
||||
|
||||
sco.items[0].qty = 5
|
||||
sco.items.pop()
|
||||
sco.items.pop()
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
# Test - 7: Since line item 1 is now fully subcontracted, new SCO should by default only have the remaining 2 line items
|
||||
self.assertEqual(len(sco.items), 2)
|
||||
|
||||
sco.items.pop(0)
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 25)
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 8: Since this PO is now fully subcontracted, creating a new SCO from it should throw error
|
||||
self.assertRaises(frappe.ValidationError, make_subcontracting_order, po.name)
|
||||
|
||||
@change_settings("Buying Settings", {"auto_create_subcontracting_order": 1})
|
||||
def test_auto_create_subcontracting_order(self):
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
@@ -1124,6 +1191,53 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertEqual(po.per_billed, 100)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
make_bom_for_subcontracted_items,
|
||||
make_raw_materials,
|
||||
make_service_items,
|
||||
make_subcontracted_items,
|
||||
)
|
||||
|
||||
make_subcontracted_items()
|
||||
make_raw_materials()
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA1",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 2",
|
||||
"qty": 20,
|
||||
"rate": 25,
|
||||
"fg_item": "Subcontracted Item SA2",
|
||||
"fg_item_qty": 15,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 3",
|
||||
"qty": 25,
|
||||
"rate": 10,
|
||||
"fg_item": "Subcontracted Item SA3",
|
||||
"fg_item_qty": 50,
|
||||
},
|
||||
]
|
||||
|
||||
return create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-05-24 19:29:06",
|
||||
"creation": "2024-12-09 12:54:24.652161",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
@@ -26,6 +26,7 @@
|
||||
"quantity_and_rate",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"sco_qty",
|
||||
"col_break2",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
@@ -909,13 +910,21 @@
|
||||
{
|
||||
"fieldname": "column_break_fyqr",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "sco_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-05 11:23:24.859435",
|
||||
"modified": "2024-12-10 12:11:18.536089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -80,6 +80,7 @@ class PurchaseOrderItem(Document):
|
||||
sales_order_item: DF.Data | None
|
||||
sales_order_packed_item: DF.Data | None
|
||||
schedule_date: DF.Date
|
||||
sco_qty: DF.Float
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
stock_uom_rate: DF.Currency
|
||||
|
||||
@@ -18,11 +18,12 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
update_received_amount(data)
|
||||
|
||||
if not data:
|
||||
return [], [], None, []
|
||||
|
||||
update_received_amount(data)
|
||||
|
||||
data, chart_data = prepare_data(data, filters)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
@@ -103,6 +104,11 @@ def get_received_amount_data(data):
|
||||
pr = frappe.qb.DocType("Purchase Receipt")
|
||||
pr_item = frappe.qb.DocType("Purchase Receipt Item")
|
||||
|
||||
po_items = [row.name for row in data]
|
||||
|
||||
if not po_items:
|
||||
return frappe._dict()
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pr)
|
||||
.inner_join(pr_item)
|
||||
@@ -111,12 +117,10 @@ def get_received_amount_data(data):
|
||||
pr_item.purchase_order_item,
|
||||
Sum(pr_item.base_amount).as_("received_qty_amount"),
|
||||
)
|
||||
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
|
||||
.where((pr.docstatus == 1) & (pr_item.purchase_order_item.isin(po_items)))
|
||||
.groupby(pr_item.purchase_order_item)
|
||||
)
|
||||
|
||||
query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
|
||||
|
||||
data = query.run()
|
||||
|
||||
if not data:
|
||||
|
||||
@@ -379,13 +379,14 @@ class AccountsController(TransactionBase):
|
||||
== 1
|
||||
)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
|
||||
)
|
||||
frappe.db.sql(
|
||||
"delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s",
|
||||
(self.doctype, self.name),
|
||||
)
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
frappe.qb.from_(gle).delete().where(
|
||||
(gle.voucher_type == self.doctype) & (gle.voucher_no == self.name)
|
||||
).run()
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
frappe.qb.from_(sle).delete().where(
|
||||
(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
|
||||
).run()
|
||||
|
||||
def remove_serial_and_batch_bundle(self):
|
||||
bundles = frappe.get_all(
|
||||
@@ -463,9 +464,16 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_invoice_documents_schedule(self):
|
||||
if self.is_return:
|
||||
if (
|
||||
self.is_return
|
||||
or (self.doctype == "Purchase Invoice" and self.is_paid)
|
||||
or (self.doctype == "Sales Invoice" and self.is_pos)
|
||||
or self.get("is_opening") == "Yes"
|
||||
):
|
||||
self.payment_terms_template = ""
|
||||
self.payment_schedule = []
|
||||
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
self.validate_payment_schedule_dates()
|
||||
@@ -1150,11 +1158,12 @@ class AccountsController(TransactionBase):
|
||||
def clear_unallocated_advances(self, childtype, parentfield):
|
||||
self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]}))
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tab{}` where parentfield={} and parent = {}
|
||||
and allocated_amount = 0""".format(childtype, "%s", "%s"),
|
||||
(parentfield, self.name),
|
||||
)
|
||||
doctype = frappe.qb.DocType(childtype)
|
||||
frappe.qb.from_(doctype).delete().where(
|
||||
(doctype.parentfield == parentfield)
|
||||
& (doctype.parent == self.name)
|
||||
& (doctype.allocated_amount == 0)
|
||||
).run()
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_shipping_rule(self):
|
||||
@@ -1204,6 +1213,7 @@ class AccountsController(TransactionBase):
|
||||
"advance_amount": flt(d.amount),
|
||||
"allocated_amount": allocated_amount,
|
||||
"ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry
|
||||
"difference_posting_date": self.posting_date,
|
||||
}
|
||||
if d.get("paid_from"):
|
||||
advance_row["account"] = d.paid_from
|
||||
@@ -1214,7 +1224,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def get_advance_entries(self, include_unallocated=True):
|
||||
party_account = []
|
||||
if self.doctype == "Sales Invoice":
|
||||
default_advance_account = None
|
||||
|
||||
if self.doctype in ["Sales Invoice", "POS Invoice"]:
|
||||
party_type = "Customer"
|
||||
party = self.customer
|
||||
amount_field = "credit_in_account_currency"
|
||||
@@ -1229,10 +1241,14 @@ class AccountsController(TransactionBase):
|
||||
order_doctype = "Purchase Order"
|
||||
party_account.append(self.credit_to)
|
||||
|
||||
party_account.extend(
|
||||
get_party_account(party_type, party=party, company=self.company, include_advance=True)
|
||||
party_accounts = get_party_account(
|
||||
party_type, party=party, company=self.company, include_advance=True
|
||||
)
|
||||
|
||||
if party_accounts:
|
||||
party_account.append(party_accounts[0])
|
||||
default_advance_account = party_accounts[1] if len(party_accounts) == 2 else None
|
||||
|
||||
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
journal_entries = get_advance_journal_entries(
|
||||
@@ -1240,7 +1256,13 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
party_type, party, party_account, order_doctype, order_list, include_unallocated
|
||||
party_type,
|
||||
party,
|
||||
party_account,
|
||||
order_doctype,
|
||||
order_list,
|
||||
default_advance_account,
|
||||
include_unallocated,
|
||||
)
|
||||
|
||||
res = journal_entries + payment_entries
|
||||
@@ -1497,7 +1519,6 @@ class AccountsController(TransactionBase):
|
||||
gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
)
|
||||
|
||||
je = create_gain_loss_journal(
|
||||
self.company,
|
||||
args.get("difference_posting_date") if args else self.posting_date,
|
||||
@@ -1583,6 +1604,7 @@ class AccountsController(TransactionBase):
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
),
|
||||
"exchange_gain_loss": flt(d.get("exchange_gain_loss")),
|
||||
"difference_posting_date": d.get("difference_posting_date"),
|
||||
}
|
||||
)
|
||||
lst.append(args)
|
||||
@@ -2116,11 +2138,9 @@ class AccountsController(TransactionBase):
|
||||
for adv in self.advances:
|
||||
consider_for_total_advance = True
|
||||
if adv.reference_name == linked_doc_name:
|
||||
frappe.db.sql(
|
||||
f"""delete from `tab{self.doctype} Advance`
|
||||
where name = %s""",
|
||||
adv.name,
|
||||
)
|
||||
doctype = frappe.qb.DocType(self.doctype + " Advance")
|
||||
frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run()
|
||||
|
||||
consider_for_total_advance = False
|
||||
|
||||
if consider_for_total_advance:
|
||||
@@ -2333,6 +2353,7 @@ class AccountsController(TransactionBase):
|
||||
return
|
||||
|
||||
for d in self.get("payment_schedule"):
|
||||
d.validate_from_to_dates("discount_date", "due_date")
|
||||
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
|
||||
frappe.throw(
|
||||
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
@@ -744,6 +745,7 @@ class BuyingController(SubcontractingController):
|
||||
def auto_make_assets(self, asset_items):
|
||||
items_data = get_asset_item_details(asset_items)
|
||||
messages = []
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
|
||||
|
||||
for d in self.items:
|
||||
if d.is_fixed_asset:
|
||||
@@ -755,11 +757,11 @@ class BuyingController(SubcontractingController):
|
||||
if item_data.get("asset_naming_series"):
|
||||
created_assets = []
|
||||
if item_data.get("is_grouped_asset"):
|
||||
asset = self.make_asset(d, is_grouped_asset=True)
|
||||
asset = self.make_asset(d, accounting_dimensions, is_grouped_asset=True)
|
||||
created_assets.append(asset)
|
||||
else:
|
||||
for _qty in range(cint(d.qty)):
|
||||
asset = self.make_asset(d)
|
||||
asset = self.make_asset(d, accounting_dimensions)
|
||||
created_assets.append(asset)
|
||||
|
||||
if len(created_assets) > 5:
|
||||
@@ -797,7 +799,7 @@ class BuyingController(SubcontractingController):
|
||||
for message in messages:
|
||||
frappe.msgprint(message, title="Success", indicator="green")
|
||||
|
||||
def make_asset(self, row, is_grouped_asset=False):
|
||||
def make_asset(self, row, accounting_dimensions, is_grouped_asset=False):
|
||||
if not row.asset_location:
|
||||
frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code))
|
||||
|
||||
@@ -828,6 +830,13 @@ class BuyingController(SubcontractingController):
|
||||
"purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
asset.update(
|
||||
{
|
||||
dimension["fieldname"]: self.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
asset.flags.ignore_validate = True
|
||||
asset.flags.ignore_mandatory = True
|
||||
|
||||
@@ -271,10 +271,14 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
qb_filter_or_conditions = []
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
if filters and filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
)
|
||||
if filters:
|
||||
if filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
qb_filter_and_conditions.append(proj.company == filters.get("company"))
|
||||
|
||||
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))
|
||||
|
||||
|
||||
@@ -126,9 +126,13 @@ status_map = {
|
||||
"Partially Received",
|
||||
"eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'",
|
||||
],
|
||||
[
|
||||
"Partially Received",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'",
|
||||
],
|
||||
[
|
||||
"Partially Ordered",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'",
|
||||
],
|
||||
[
|
||||
"Manufactured",
|
||||
|
||||
@@ -156,7 +156,7 @@ class StockController(AccountsController):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
is_material_issue = False
|
||||
if self.doctype == "Stock Entry" and self.purpose == "Material Issue":
|
||||
if self.doctype == "Stock Entry" and self.purpose in ["Material Issue", "Material Transfer"]:
|
||||
is_material_issue = True
|
||||
|
||||
for d in self.get("items"):
|
||||
@@ -530,7 +530,7 @@ class StockController(AccountsController):
|
||||
"account": warehouse_account[sle.warehouse]["account"],
|
||||
"against": expense_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.project or self.get("project"),
|
||||
"project": sle.get("project") or item_row.project or self.get("project"),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": flt(sle.stock_value_difference, precision),
|
||||
"is_opening": item_row.get("is_opening")
|
||||
@@ -550,7 +550,9 @@ class StockController(AccountsController):
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(sle.stock_value_difference, precision),
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"project": sle.get("project")
|
||||
or item_row.get("project")
|
||||
or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening")
|
||||
or self.get("is_opening")
|
||||
or "No",
|
||||
@@ -678,23 +680,34 @@ class StockController(AccountsController):
|
||||
|
||||
def get_stock_ledger_details(self):
|
||||
stock_ledger = {}
|
||||
stock_ledger_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, warehouse, stock_value_difference, valuation_rate,
|
||||
voucher_detail_no, item_code, posting_date, posting_time,
|
||||
actual_qty, qty_after_transaction
|
||||
from
|
||||
`tabStock Ledger Entry`
|
||||
where
|
||||
voucher_type=%s and voucher_no=%s and is_cancelled = 0
|
||||
""",
|
||||
(self.doctype, self.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
stock_ledger_entries = (
|
||||
frappe.qb.from_(table)
|
||||
.select(
|
||||
table.name,
|
||||
table.warehouse,
|
||||
table.stock_value_difference,
|
||||
table.valuation_rate,
|
||||
table.voucher_detail_no,
|
||||
table.item_code,
|
||||
table.posting_date,
|
||||
table.posting_time,
|
||||
table.actual_qty,
|
||||
table.qty_after_transaction,
|
||||
table.project,
|
||||
)
|
||||
.where(
|
||||
(table.voucher_type == self.doctype)
|
||||
& (table.voucher_no == self.name)
|
||||
& (table.is_cancelled == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
for sle in stock_ledger_entries:
|
||||
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
|
||||
|
||||
return stock_ledger
|
||||
|
||||
def check_expense_account(self, item):
|
||||
@@ -999,6 +1012,9 @@ class StockController(AccountsController):
|
||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||
qi_required = True # inward stock needs inspection
|
||||
|
||||
if row.get("is_scrap_item"):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
self.validate_qi_presence(row)
|
||||
if self.docstatus == 1:
|
||||
@@ -1758,6 +1774,9 @@ def make_bundle_for_material_transfer(**kwargs):
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
if kwargs.do_not_submit:
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
else:
|
||||
bundle_doc.submit()
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -103,6 +103,29 @@ class SubcontractingController(StockController):
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
)
|
||||
|
||||
if (
|
||||
self.doctype == "Subcontracting Order" and not item.sc_conversion_factor
|
||||
): # this condition will only be true if user has recently updated from develop branch
|
||||
service_item_qty = frappe.get_value(
|
||||
"Subcontracting Order Service Item",
|
||||
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
|
||||
fieldname=["qty"],
|
||||
)
|
||||
item.sc_conversion_factor = service_item_qty / item.qty
|
||||
|
||||
if (
|
||||
self.doctype not in "Subcontracting Receipt"
|
||||
and item.qty
|
||||
> flt(get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item))
|
||||
/ item.sc_conversion_factor
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
|
||||
).format(item.idx, item.item_name)
|
||||
)
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if item.bom:
|
||||
is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
|
||||
|
||||
@@ -1110,6 +1133,12 @@ def get_item_details(items):
|
||||
return item_details
|
||||
|
||||
|
||||
def get_pending_sco_qty(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name)
|
||||
return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(
|
||||
subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None
|
||||
|
||||
@@ -18,7 +18,7 @@ from erpnext.controllers.accounts_controller import (
|
||||
validate_inclusive_tax,
|
||||
validate_taxes_and_charges,
|
||||
)
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template, get_item_tax_map
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ class calculate_taxes_and_totals:
|
||||
self.validate_conversion_rate()
|
||||
self.calculate_item_values()
|
||||
self.validate_item_tax_template()
|
||||
self.update_item_tax_map()
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
self.calculate_net_total()
|
||||
@@ -134,6 +135,14 @@ class calculate_taxes_and_totals:
|
||||
)
|
||||
)
|
||||
|
||||
def update_item_tax_map(self):
|
||||
for item in self.doc.items:
|
||||
item.item_tax_rate = get_item_tax_map(
|
||||
company=self.doc.get("company"),
|
||||
item_tax_template=item.item_tax_template,
|
||||
as_json=True,
|
||||
)
|
||||
|
||||
def validate_conversion_rate(self):
|
||||
# validate conversion rate
|
||||
company_currency = erpnext.get_company_currency(self.doc.company)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -1979,3 +1981,95 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertEqual(len(exc_je_for_adv), 0)
|
||||
|
||||
self.remove_advance_accounts_from_party_master()
|
||||
|
||||
def test_difference_posting_date_in_pi_and_si(self):
|
||||
self.setup_advance_accounts_in_party_master()
|
||||
|
||||
# create payment entry for customer
|
||||
adv = self.create_payment_entry(amount=1, source_exc_rate=83)
|
||||
adv.save()
|
||||
self.assertEqual(adv.paid_from, self.advance_received_usd)
|
||||
adv.submit()
|
||||
adv.reload()
|
||||
|
||||
# create sales invoice with advance received
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True)
|
||||
si.debit_to = self.debtors_usd
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": adv.doctype,
|
||||
"reference_name": adv.name,
|
||||
"remarks": "Amount INR 1 received from _Test MC Customer USD\nTransaction reference no Test001 dated 2024-12-19",
|
||||
"advance_amount": 1.0,
|
||||
"allocated_amount": 1.0,
|
||||
"exchange_gain_loss": 3.0,
|
||||
"ref_exchange_rate": 83.0,
|
||||
"difference_posting_date": add_days(nowdate(), -2),
|
||||
},
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
# exc Gain/Loss journal should've been creatad
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_adv), 1)
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_adv)
|
||||
|
||||
# check jv created with difference_posting_date in sales invoice
|
||||
jv = frappe.get_doc("Journal Entry", exc_je_for_si[0].parent)
|
||||
sales_invoice = frappe.get_doc("Sales Invoice", si.name)
|
||||
self.assertEqual(sales_invoice.advances[0].difference_posting_date, jv.posting_date)
|
||||
|
||||
# create payment entry for supplier
|
||||
usd_amount = 1
|
||||
inr_amount = 85
|
||||
exc_rate = 85
|
||||
adv = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Pay",
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
paid_from=self.cash,
|
||||
paid_to=self.advance_paid_usd,
|
||||
paid_amount=inr_amount,
|
||||
)
|
||||
adv.source_exchange_rate = 1
|
||||
adv.target_exchange_rate = exc_rate
|
||||
adv.received_amount = usd_amount
|
||||
adv.paid_amount = exc_rate * usd_amount
|
||||
adv.posting_date = nowdate()
|
||||
adv.save()
|
||||
self.assertEqual(adv.paid_to, self.advance_paid_usd)
|
||||
adv.submit()
|
||||
|
||||
# create purchase invoice with advance paid
|
||||
pi = self.create_purchase_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True)
|
||||
pi.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": adv.doctype,
|
||||
"reference_name": adv.name,
|
||||
"remarks": "Amount INR 1 paid to _Test MC Supplier USD\nTransaction reference no Test001 dated 2024-12-20",
|
||||
"advance_amount": 1.0,
|
||||
"allocated_amount": 1.0,
|
||||
"exchange_gain_loss": 5.0,
|
||||
"ref_exchange_rate": 85.0,
|
||||
"difference_posting_date": add_days(nowdate(), -2),
|
||||
},
|
||||
)
|
||||
pi.save().submit()
|
||||
self.assertEqual(pi.credit_to, self.creditors_usd)
|
||||
|
||||
# exc Gain/Loss journal should've been creatad
|
||||
exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(len(exc_je_for_pi), 1)
|
||||
self.assertEqual(len(exc_je_for_adv), 1)
|
||||
self.assertEqual(exc_je_for_pi, exc_je_for_adv)
|
||||
|
||||
# check jv created with difference_posting_date in purchase invoice
|
||||
journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent)
|
||||
purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name)
|
||||
self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date)
|
||||
|
||||
@@ -1261,6 +1261,7 @@ def make_raw_materials():
|
||||
for item, properties in raw_materials.items():
|
||||
if not frappe.db.exists("Item", item):
|
||||
properties.update({"is_stock_item": 1})
|
||||
properties.update({"valuation_rate": 100})
|
||||
make_item(item, properties)
|
||||
|
||||
|
||||
@@ -1311,7 +1312,7 @@ def make_bom_for_subcontracted_items():
|
||||
|
||||
for item_code, raw_materials in boms.items():
|
||||
if not frappe.db.exists("BOM", {"item": item_code}):
|
||||
make_bom(item=item_code, raw_materials=raw_materials, rate=100)
|
||||
make_bom(item=item_code, raw_materials=raw_materials, rate=100, currency="INR")
|
||||
|
||||
|
||||
def set_backflush_based_on(based_on):
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"table_fieldname": "competitors"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:33:54.284279",
|
||||
"modified": "2024-12-10 08:26:38.496003",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Competitor",
|
||||
@@ -53,20 +53,25 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance User"
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -25,7 +25,12 @@ frappe.ui.form.on("Plaid Settings", {
|
||||
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
|
||||
let bank_transaction_link = frappe.utils.get_form_link(
|
||||
"Bank Transaction",
|
||||
"",
|
||||
true,
|
||||
"Bank Transaction"
|
||||
);
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Sync Started"),
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"column_break_8",
|
||||
"order_no",
|
||||
"order_date",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"company",
|
||||
@@ -129,15 +131,27 @@
|
||||
"fieldname": "terms",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Terms and Conditions Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "order_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Order No"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.order_no",
|
||||
"fieldname": "order_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Order Date"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-29 00:30:30.621636",
|
||||
"modified": "2024-12-05 15:44:21.520093",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Blanket Order",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -31,6 +31,8 @@ class BlanketOrder(Document):
|
||||
from_date: DF.Date
|
||||
items: DF.Table[BlanketOrderItem]
|
||||
naming_series: DF.Literal["MFG-BLR-.YYYY.-"]
|
||||
order_date: DF.Date | None
|
||||
order_no: DF.Data | None
|
||||
supplier: DF.Link | None
|
||||
supplier_name: DF.Data | None
|
||||
tc_name: DF.Link | None
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="col-md-5" style="max-height: 500px">
|
||||
{% if data.image %}
|
||||
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
|
||||
<img class="responsive" src={{ data.image }}>
|
||||
<img class="responsive" style="width: 100%;" src={{ data.image }}>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@ frappe.listview_settings["BOM"] = {
|
||||
add_fields: ["is_active", "is_default", "total_cost", "has_variants"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.is_active && doc.has_variants) {
|
||||
return [__("Template"), "orange", "has_variants,=,Yes"];
|
||||
return [__("Template"), "orange", "has_variants,=,1"];
|
||||
} else if (doc.is_default) {
|
||||
return [__("Default"), "green", "is_default,=,Yes"];
|
||||
return [__("Default"), "green", "is_default,=,1"];
|
||||
} else if (doc.is_active) {
|
||||
return [__("Active"), "blue", "is_active,=,Yes"];
|
||||
return [__("Active"), "blue", "is_active,=,1"];
|
||||
} else if (!doc.is_active) {
|
||||
return [__("Not active"), "gray", "is_active,=,No"];
|
||||
return [__("Not active"), "gray", "is_active,=,0"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, get_datetime, now
|
||||
|
||||
|
||||
class BOMUpdateTool(Document):
|
||||
@@ -50,13 +51,21 @@ def auto_update_latest_price_in_all_boms() -> None:
|
||||
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
|
||||
wip_log = frappe.get_all(
|
||||
"BOM Update Log",
|
||||
{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||
fields=["creation", "status"],
|
||||
filters={"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||
limit_page_length=1,
|
||||
order_by="creation desc",
|
||||
)
|
||||
if not wip_log:
|
||||
|
||||
if not wip_log or is_older_log(wip_log[0]):
|
||||
create_bom_update_log(update_type="Update Cost")
|
||||
|
||||
|
||||
def is_older_log(log: dict) -> bool:
|
||||
no_of_days = date_diff(get_datetime(now()), get_datetime(log.creation))
|
||||
return no_of_days > 10
|
||||
|
||||
|
||||
def create_bom_update_log(
|
||||
boms: dict[str, str] | None = None,
|
||||
update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
|
||||
|
||||
@@ -944,8 +944,9 @@ class JobCard(Document):
|
||||
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
|
||||
min_qty = []
|
||||
for d in doc.operations:
|
||||
if d.completed_qty:
|
||||
min_qty.append(d.completed_qty)
|
||||
completed_qty = flt(d.completed_qty) + flt(d.process_loss_qty)
|
||||
if completed_qty:
|
||||
min_qty.append(completed_qty)
|
||||
else:
|
||||
min_qty = []
|
||||
break
|
||||
|
||||
@@ -242,14 +242,14 @@
|
||||
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
|
||||
"fieldname": "validate_components_quantities_per_bom",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Components Quantities Per BOM"
|
||||
"label": "Validate Components and Quantities Per BOM"
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-02 12:12:03.132567",
|
||||
"modified": "2025-01-02 12:46:33.520853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
@@ -267,4 +267,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings, timeout
|
||||
from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, today
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import make_stock_entry as make_stock_entry_from_jc
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
CapacityError,
|
||||
@@ -505,6 +506,60 @@ class TestWorkOrder(FrappeTestCase):
|
||||
for stock_entry in stock_entries:
|
||||
stock_entry.cancel()
|
||||
|
||||
def test_work_order_material_transferred_qty_with_process_loss(self):
|
||||
stock_entries = []
|
||||
bom = frappe.get_doc("BOM", {"docstatus": 1, "with_operations": 1, "company": "_Test Company"})
|
||||
|
||||
work_order = make_wo_order_test_record(
|
||||
item=bom.item,
|
||||
qty=2,
|
||||
bom_no=bom.name,
|
||||
source_warehouse="_Test Warehouse - _TC",
|
||||
transfer_material_against="Job Card",
|
||||
)
|
||||
|
||||
self.assertEqual(work_order.qty, 2)
|
||||
|
||||
for row in work_order.required_items:
|
||||
stock_entry_doc = test_stock_entry.make_stock_entry(
|
||||
item_code=row.item_code, target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100
|
||||
)
|
||||
stock_entries.append(stock_entry_doc)
|
||||
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card", filters={"work_order": work_order.name}, order_by="creation asc"
|
||||
)
|
||||
|
||||
for row in job_cards:
|
||||
transfer_entry_1 = make_stock_entry_from_jc(row.name)
|
||||
transfer_entry_1.submit()
|
||||
|
||||
doc = frappe.get_doc("Job Card", row.name)
|
||||
for row in doc.scheduled_time_logs:
|
||||
doc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": row.from_time,
|
||||
"to_time": row.to_time,
|
||||
"time_in_mins": row.time_in_mins,
|
||||
"completed_qty": 1,
|
||||
},
|
||||
)
|
||||
|
||||
doc.save()
|
||||
doc.submit()
|
||||
|
||||
self.assertEqual(doc.total_completed_qty, 1)
|
||||
self.assertEqual(doc.process_loss_qty, 1)
|
||||
|
||||
work_order.reload()
|
||||
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, 2)
|
||||
|
||||
for row in work_order.operations:
|
||||
self.assertEqual(row.completed_qty, 1)
|
||||
self.assertEqual(row.process_loss_qty, 1)
|
||||
|
||||
def test_capcity_planning(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings", {"disable_capacity_planning": 0, "capacity_planning_for_days": 1}
|
||||
@@ -2346,6 +2401,56 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
|
||||
|
||||
def test_components_as_per_bom_for_manufacture_entry(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
|
||||
|
||||
fg_item = "Test FG Item For Component Validation 1"
|
||||
source_warehouse = "Stores - _TC"
|
||||
raw_materials = ["Test Component Validation RM Item 11", "Test Component Validation RM Item 12"]
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
for item in raw_materials:
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item,
|
||||
target=source_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=10,
|
||||
source_warehouse=source_warehouse,
|
||||
)
|
||||
|
||||
transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
|
||||
transfer_entry.save()
|
||||
transfer_entry.remove(transfer_entry.items[0])
|
||||
|
||||
self.assertRaises(frappe.ValidationError, transfer_entry.save)
|
||||
|
||||
transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
|
||||
transfer_entry.save()
|
||||
transfer_entry.submit()
|
||||
|
||||
manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
|
||||
manufacture_entry.save()
|
||||
|
||||
manufacture_entry.remove(manufacture_entry.items[0])
|
||||
|
||||
self.assertRaises(frappe.ValidationError, manufacture_entry.save)
|
||||
manufacture_entry.delete()
|
||||
|
||||
manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
|
||||
manufacture_entry.save()
|
||||
manufacture_entry.submit()
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
|
||||
|
||||
|
||||
def make_operation(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
@@ -130,6 +130,32 @@ frappe.ui.form.on("Work Order", {
|
||||
);
|
||||
},
|
||||
|
||||
allow_alternative_item: function (frm) {
|
||||
let has_alternative = false;
|
||||
if (frm.doc.required_items) {
|
||||
has_alternative = frm.doc.required_items.find((i) => i.allow_alternative_item === 1);
|
||||
}
|
||||
|
||||
if (frm.doc.allow_alternative_item && frm.doc.docstatus === 0 && has_alternative) {
|
||||
frm.add_custom_button(__("Alternate Item"), () => {
|
||||
erpnext.utils.select_alternate_items({
|
||||
frm: frm,
|
||||
child_docname: "required_items",
|
||||
warehouse_field: "source_warehouse",
|
||||
child_doctype: "Work Order Item",
|
||||
original_item_field: "original_item",
|
||||
condition: (d) => {
|
||||
if (d.allow_alternative_item) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
frm.remove_custom_button(__("Alternate Item"));
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
erpnext.toggle_naming_series();
|
||||
erpnext.work_order.set_custom_buttons(frm);
|
||||
@@ -163,26 +189,6 @@ frappe.ui.form.on("Work Order", {
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.required_items && frm.doc.allow_alternative_item) {
|
||||
const has_alternative = frm.doc.required_items.find((i) => i.allow_alternative_item === 1);
|
||||
if (frm.doc.docstatus == 0 && has_alternative) {
|
||||
frm.add_custom_button(__("Alternate Item"), () => {
|
||||
erpnext.utils.select_alternate_items({
|
||||
frm: frm,
|
||||
child_docname: "required_items",
|
||||
warehouse_field: "source_warehouse",
|
||||
child_doctype: "Work Order Item",
|
||||
original_item_field: "original_item",
|
||||
condition: (d) => {
|
||||
if (d.allow_alternative_item) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.status == "Completed") {
|
||||
if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") {
|
||||
frm.add_custom_button(
|
||||
@@ -210,6 +216,7 @@ frappe.ui.form.on("Work Order", {
|
||||
}
|
||||
|
||||
frm.trigger("add_custom_button_to_return_components");
|
||||
frm.trigger("allow_alternative_item");
|
||||
},
|
||||
|
||||
add_custom_button_to_return_components: function (frm) {
|
||||
@@ -540,6 +547,9 @@ frappe.ui.form.on("Work Order", {
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Work Order Item", {
|
||||
allow_alternative_item(frm) {
|
||||
frm.trigger("allow_alternative_item");
|
||||
},
|
||||
source_warehouse: function (frm, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
if (!row.item_code) {
|
||||
@@ -618,7 +628,7 @@ erpnext.work_order = {
|
||||
set_custom_buttons: function (frm) {
|
||||
var doc = frm.doc;
|
||||
|
||||
if (doc.status !== "Closed") {
|
||||
if (doc.docstatus === 1 && doc.status !== "Closed") {
|
||||
frm.add_custom_button(
|
||||
__("Close"),
|
||||
function () {
|
||||
|
||||
@@ -230,8 +230,8 @@ class ForecastingReport(ExponentialSmoothingForecast):
|
||||
"data": {
|
||||
"labels": labels,
|
||||
"datasets": [
|
||||
{"name": "Demand", "values": self.total_demand},
|
||||
{"name": "Forecast", "values": self.total_forecast},
|
||||
{"name": _("Demand"), "values": self.total_demand},
|
||||
{"name": _("Forecast"), "values": self.total_forecast},
|
||||
],
|
||||
},
|
||||
"type": "line",
|
||||
|
||||
@@ -106,7 +106,7 @@ def get_data(filters, columns):
|
||||
|
||||
for label in labels:
|
||||
work = {}
|
||||
work["Status"] = label
|
||||
work["Status"] = _(label)
|
||||
for _dummy, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
if periodic_data.get(label).get(period):
|
||||
|
||||
@@ -4,61 +4,67 @@ import frappe
|
||||
|
||||
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
|
||||
|
||||
DEFAULT_FILTERS = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2010-01-01",
|
||||
"to_date": "2030-01-01",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
|
||||
|
||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
|
||||
("BOM Operations Time", {}),
|
||||
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
|
||||
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
|
||||
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
|
||||
("Downtime Analysis", {}),
|
||||
(
|
||||
"Exponential Smoothing Forecasting",
|
||||
{
|
||||
"based_on_document": "Sales Order",
|
||||
"based_on_field": "Qty",
|
||||
"no_of_years": 3,
|
||||
"periodicity": "Yearly",
|
||||
"smoothing_constant": 0.3,
|
||||
},
|
||||
),
|
||||
("Job Card Summary", {"fiscal_year": "2021-2022"}),
|
||||
("Production Analytics", {"range": "Monthly"}),
|
||||
("Quality Inspection Summary", {}),
|
||||
("Process Loss Report", {}),
|
||||
("Work Order Stock Report", {}),
|
||||
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
|
||||
]
|
||||
|
||||
|
||||
if frappe.db.a_row_exists("Production Plan"):
|
||||
REPORT_FILTER_TEST_CASES.append(
|
||||
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
|
||||
)
|
||||
|
||||
OPTIONAL_FILTERS = {
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item": "_Test Item",
|
||||
"item_group": "_Test Item Group",
|
||||
}
|
||||
|
||||
|
||||
class TestManufacturingReports(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.setup_default_filters()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def setup_default_filters(self):
|
||||
self.last_bom = frappe.get_last_doc("BOM").name
|
||||
self.DEFAULT_FILTERS = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2010-01-01",
|
||||
"to_date": "2030-01-01",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
|
||||
self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("BOM Explorer", {"bom": self.last_bom}),
|
||||
("BOM Operations Time", {}),
|
||||
("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}),
|
||||
("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}),
|
||||
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
|
||||
("Downtime Analysis", {}),
|
||||
(
|
||||
"Exponential Smoothing Forecasting",
|
||||
{
|
||||
"based_on_document": "Sales Order",
|
||||
"based_on_field": "Qty",
|
||||
"no_of_years": 3,
|
||||
"periodicity": "Yearly",
|
||||
"smoothing_constant": 0.3,
|
||||
},
|
||||
),
|
||||
("Job Card Summary", {"fiscal_year": "2021-2022"}),
|
||||
("Production Analytics", {"range": "Monthly"}),
|
||||
("Quality Inspection Summary", {}),
|
||||
("Process Loss Report", {}),
|
||||
("Work Order Stock Report", {}),
|
||||
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
|
||||
]
|
||||
|
||||
if frappe.db.a_row_exists("Production Plan"):
|
||||
self.REPORT_FILTER_TEST_CASES.append(
|
||||
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
|
||||
)
|
||||
|
||||
self.OPTIONAL_FILTERS = {
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item": "_Test Item",
|
||||
"item_group": "_Test Item Group",
|
||||
}
|
||||
|
||||
def test_execute_all_manufacturing_reports(self):
|
||||
"""Test that all script report in manufacturing modules are executable with supported filters"""
|
||||
for report, filter in REPORT_FILTER_TEST_CASES:
|
||||
for report, filter in self.REPORT_FILTER_TEST_CASES:
|
||||
with self.subTest(report=report):
|
||||
execute_script_report(
|
||||
report_name=report,
|
||||
module="Manufacturing",
|
||||
filters=filter,
|
||||
default_filters=DEFAULT_FILTERS,
|
||||
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||
default_filters=self.DEFAULT_FILTERS,
|
||||
optional_filters=self.OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||
)
|
||||
|
||||
@@ -147,10 +147,10 @@ class BOMConfigurator {
|
||||
if (!node.expanded) {
|
||||
view.tree.load_children(node, true);
|
||||
$(node.parent[0]).find(".tree-children").show();
|
||||
node.$toolbar.find(".expand-all-btn").html("Collapse All");
|
||||
node.$toolbar.find(".expand-all-btn").html(__("Collapse All"));
|
||||
} else {
|
||||
node.$tree_link.trigger("click");
|
||||
node.$toolbar.find(".expand-all-btn").html("Expand All");
|
||||
node.$toolbar.find(".expand-all-btn").html(__("Expand All"));
|
||||
}
|
||||
},
|
||||
condition: function (node) {
|
||||
@@ -190,10 +190,10 @@ class BOMConfigurator {
|
||||
if (!node.expanded) {
|
||||
view.tree.load_children(node, true);
|
||||
$(node.parent[0]).find(".tree-children").show();
|
||||
node.$toolbar.find(".expand-all-btn").html("Collapse All");
|
||||
node.$toolbar.find(".expand-all-btn").html(__("Collapse All"));
|
||||
} else {
|
||||
node.$tree_link.trigger("click");
|
||||
node.$toolbar.find(".expand-all-btn").html("Expand All");
|
||||
node.$toolbar.find(".expand-all-btn").html(__("Expand All"));
|
||||
}
|
||||
},
|
||||
condition: function (node) {
|
||||
|
||||
@@ -25,6 +25,14 @@ erpnext.buying = {
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("project", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (this.frm.doc.__islocal
|
||||
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
|
||||
|
||||
@@ -145,6 +153,18 @@ erpnext.buying = {
|
||||
});
|
||||
}
|
||||
|
||||
company(){
|
||||
if(!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||
args: { name: this.frm.doc.company, existing_address:this.frm.doc.billing_address },
|
||||
callback: (r) => {
|
||||
this.frm.set_value("billing_address", r.message || "");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
supplier_address() {
|
||||
erpnext.utils.get_address_display(this.frm);
|
||||
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");
|
||||
|
||||
@@ -959,7 +959,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
|
||||
|
||||
if (!is_drop_ship) {
|
||||
console.log('get_shipping_address');
|
||||
erpnext.utils.get_shipping_address(this.frm, function() {
|
||||
set_party_account(set_pricing);
|
||||
});
|
||||
@@ -975,6 +974,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
transaction_date() {
|
||||
this.apply_pricing_rule()
|
||||
if (this.frm.doc.transaction_date) {
|
||||
this.frm.transaction_date = this.frm.doc.transaction_date;
|
||||
frappe.ui.form.trigger(this.frm.doc.doctype, "currency");
|
||||
@@ -983,6 +983,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
posting_date() {
|
||||
var me = this;
|
||||
me.apply_pricing_rule()
|
||||
if (this.frm.doc.posting_date) {
|
||||
this.frm.posting_date = this.frm.doc.posting_date;
|
||||
|
||||
@@ -2310,6 +2311,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
fieldname: "batch_no",
|
||||
label: __("Batch No"),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "child_row_reference",
|
||||
label: __("Child Row Reference"),
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2353,14 +2360,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
if (this.has_inspection_required(item)) {
|
||||
let dialog_items = dialog.fields_dict.items;
|
||||
dialog_items.df.data.push({
|
||||
"docname": item.name,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"description": item.description,
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no,
|
||||
"sample_size": item.sample_quantity
|
||||
"sample_size": item.sample_quantity,
|
||||
"child_row_reference": item.name,
|
||||
});
|
||||
dialog_items.grid.refresh();
|
||||
}
|
||||
|
||||
@@ -58,13 +58,17 @@ def search_by_term(search_term, warehouse, price_list):
|
||||
item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
|
||||
item.update({"actual_qty": item_stock_qty})
|
||||
|
||||
price_filters = {
|
||||
"price_list": price_list,
|
||||
"item_code": item_code,
|
||||
}
|
||||
|
||||
if batch_no:
|
||||
price_filters["batch_no"] = batch_no
|
||||
|
||||
price = frappe.get_list(
|
||||
doctype="Item Price",
|
||||
filters={
|
||||
"price_list": price_list,
|
||||
"item_code": item_code,
|
||||
"batch_no": batch_no,
|
||||
},
|
||||
filters=price_filters,
|
||||
fields=["uom", "currency", "price_list_rate", "batch_no"],
|
||||
)
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
edit_cart: () => this.payment.edit_cart(),
|
||||
|
||||
customer_details_updated: (details) => {
|
||||
this.item_selector.load_items_data();
|
||||
this.customer_details = details;
|
||||
// will add/remove LP payment method
|
||||
this.payment.render_loyalty_points_payment_mode();
|
||||
@@ -574,11 +575,19 @@ erpnext.PointOfSale.Controller = class {
|
||||
} else {
|
||||
if (!this.frm.doc.customer) return this.raise_customer_selection_alert();
|
||||
|
||||
const { item_code, batch_no, serial_no, rate, uom } = item;
|
||||
const { item_code, batch_no, serial_no, rate, uom, stock_uom } = item;
|
||||
|
||||
if (!item_code) return;
|
||||
|
||||
const new_item = { item_code, batch_no, rate, uom, [field]: value };
|
||||
if (rate == undefined || rate == 0) {
|
||||
frappe.show_alert({
|
||||
message: __("Price is not set for the item."),
|
||||
indicator: "orange",
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
return;
|
||||
}
|
||||
const new_item = { item_code, batch_no, rate, uom, [field]: value, stock_uom };
|
||||
|
||||
if (serial_no) {
|
||||
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
|
||||
|
||||
@@ -390,6 +390,14 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
input_class: "input-xs",
|
||||
onchange: function () {
|
||||
this.value = flt(this.value);
|
||||
if (this.value > 100) {
|
||||
frappe.msgprint({
|
||||
title: __("Invalid Discount"),
|
||||
indicator: "red",
|
||||
message: __("Discount cannot be greater than 100%."),
|
||||
});
|
||||
this.value = 0;
|
||||
}
|
||||
frappe.model.set_value(
|
||||
frm.doc.doctype,
|
||||
frm.doc.name,
|
||||
@@ -920,10 +928,13 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
const me = this;
|
||||
dfs.forEach((df) => {
|
||||
this[`customer_${df.fieldname}_field`] = frappe.ui.form.make_control({
|
||||
df: { ...df, onchange: handle_customer_field_change },
|
||||
df: df,
|
||||
parent: $customer_form.find(`.${df.fieldname}-field`),
|
||||
render_input: true,
|
||||
});
|
||||
this[`customer_${df.fieldname}_field`].$input?.on("blur", () => {
|
||||
handle_customer_field_change.apply(this[`customer_${df.fieldname}_field`]);
|
||||
});
|
||||
this[`customer_${df.fieldname}_field`].set_value(this.customer_info[df.fieldname]);
|
||||
});
|
||||
|
||||
|
||||
@@ -315,8 +315,12 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
frappe.model.on("POS Invoice 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 (item_row_is_being_edited && field_control && field_control.get_value() !== value) {
|
||||
if (
|
||||
item_row_is_being_edited &&
|
||||
field_control &&
|
||||
field_control.get_value() !== value &&
|
||||
value == item_row[fieldname]
|
||||
) {
|
||||
field_control.set_value(value);
|
||||
cur_pos.update_cart_html(item_row);
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
|
||||
data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
|
||||
data-rate="${escape(price_list_rate || 0)}"
|
||||
data-stock-uom="${escape(item.stock_uom)}"
|
||||
title="${item.item_name}">
|
||||
|
||||
${get_item_image_html()}
|
||||
@@ -251,17 +252,19 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
let serial_no = unescape($item.attr("data-serial-no"));
|
||||
let uom = unescape($item.attr("data-uom"));
|
||||
let rate = unescape($item.attr("data-rate"));
|
||||
let stock_uom = unescape($item.attr("data-stock-uom"));
|
||||
|
||||
// escape(undefined) returns "undefined" then unescape returns "undefined"
|
||||
batch_no = batch_no === "undefined" ? undefined : batch_no;
|
||||
serial_no = serial_no === "undefined" ? undefined : serial_no;
|
||||
uom = uom === "undefined" ? undefined : uom;
|
||||
rate = rate === "undefined" ? undefined : rate;
|
||||
stock_uom = stock_uom === "undefined" ? undefined : stock_uom;
|
||||
|
||||
me.events.item_selected({
|
||||
field: "qty",
|
||||
value: "+1",
|
||||
item: { item_code, batch_no, serial_no, uom, rate },
|
||||
item: { item_code, batch_no, serial_no, uom, rate, stock_uom },
|
||||
});
|
||||
me.search_field.set_focus();
|
||||
});
|
||||
@@ -325,13 +328,16 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
}
|
||||
|
||||
filter_items({ search_term = "" } = {}) {
|
||||
const selling_price_list = this.events.get_frm().doc.selling_price_list;
|
||||
|
||||
if (search_term) {
|
||||
search_term = search_term.toLowerCase();
|
||||
|
||||
// memoize
|
||||
this.search_index = this.search_index || {};
|
||||
if (this.search_index[search_term]) {
|
||||
const items = this.search_index[search_term];
|
||||
this.search_index[selling_price_list] = this.search_index[selling_price_list] || {};
|
||||
if (this.search_index[selling_price_list][search_term]) {
|
||||
const items = this.search_index[selling_price_list][search_term];
|
||||
this.items = items;
|
||||
this.render_item_list(items);
|
||||
this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart();
|
||||
@@ -343,7 +349,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { items, serial_no, batch_no, barcode } = message;
|
||||
if (search_term && !barcode) {
|
||||
this.search_index[search_term] = items;
|
||||
this.search_index[selling_price_list][search_term] = items;
|
||||
}
|
||||
this.items = items;
|
||||
this.render_item_list(items);
|
||||
|
||||
@@ -235,7 +235,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
|
||||
// for setting correct amount after loyalty points are redeemed
|
||||
const default_mop = locals[cdt][cdn];
|
||||
const mode = default_mop.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||
const mode = this.sanitize_mode_of_payment(default_mop.mode_of_payment);
|
||||
if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) {
|
||||
this[`${mode}_control`].set_value(default_mop.amount);
|
||||
}
|
||||
@@ -388,7 +388,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
this.$payment_modes.html(
|
||||
`${payments
|
||||
.map((p, i) => {
|
||||
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
|
||||
const payment_type = p.type;
|
||||
const margin = i % 2 === 0 ? "pr-2" : "pl-2";
|
||||
const amount = p.amount > 0 ? format_currency(p.amount, currency) : "";
|
||||
@@ -407,7 +407,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
);
|
||||
|
||||
payments.forEach((p) => {
|
||||
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
|
||||
const me = this;
|
||||
this[`${mode}_control`] = frappe.ui.form.make_control({
|
||||
df: {
|
||||
@@ -442,7 +442,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
const doc = this.events.get_frm().doc;
|
||||
const payments = doc.payments;
|
||||
payments.forEach((p) => {
|
||||
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
|
||||
if (p.default) {
|
||||
setTimeout(() => {
|
||||
this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
|
||||
@@ -612,4 +612,12 @@ erpnext.PointOfSale.Payment = class {
|
||||
toggle_component(show) {
|
||||
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
|
||||
}
|
||||
|
||||
sanitize_mode_of_payment(mode_of_payment) {
|
||||
return mode_of_payment
|
||||
.replace(/ +/g, "_")
|
||||
.replace(/[^\p{L}\p{N}_-]/gu, "")
|
||||
.replace(/^[^_a-zA-Z\p{L}]+/u, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"table_fieldname": "lost_reasons"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:31:02.743353",
|
||||
"modified": "2024-12-10 08:21:38.280627",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Quotation Lost Reason",
|
||||
@@ -49,6 +49,22 @@
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance Manager"
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -4500,9 +4500,200 @@
|
||||
},
|
||||
|
||||
"Sweden": {
|
||||
"Sweden Tax": {
|
||||
"account_name": "VAT",
|
||||
"tax_rate": 25.00
|
||||
"tax_categories": [],
|
||||
"chart_of_accounts": {
|
||||
"*": {
|
||||
"sales_tax_templates": [
|
||||
{
|
||||
"title": "Försäljning Moms 25%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Utgående moms, 25 %",
|
||||
"account_number": "2610",
|
||||
"tax_rate": 25.00
|
||||
},
|
||||
"description": "Moms 25%",
|
||||
"rate": 25.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Försäljning Moms 12%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Utgående moms, 12 %",
|
||||
"account_number": "2620",
|
||||
"tax_rate": 12.00
|
||||
},
|
||||
"description": "Moms 12%",
|
||||
"rate": 12.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Försäljning Moms 6%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Utgående moms, 6 %",
|
||||
"account_number": "2630",
|
||||
"tax_rate": 6.00
|
||||
},
|
||||
"description": "Moms 6%",
|
||||
"rate": 6.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Försäljning Moms 0%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Utgående moms, 6 %",
|
||||
"account_number": "2630",
|
||||
"tax_rate": 0.00
|
||||
},
|
||||
"description": "Moms 0%",
|
||||
"rate": 0.00
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"purchase_tax_templates": [
|
||||
{
|
||||
"title": "Inköp Moms 25%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Ingående moms",
|
||||
"account_number": "2640",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 25.00
|
||||
},
|
||||
"description": "Moms 25%",
|
||||
"rate": 25.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Inköp Moms 12%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Ingående moms",
|
||||
"account_number": "2640",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 12.00
|
||||
},
|
||||
"description": "Moms 12%",
|
||||
"rate": 12.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Inköp Moms 6%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Ingående moms",
|
||||
"account_number": "2640",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 6.00
|
||||
},
|
||||
"description": "Moms 6%",
|
||||
"rate": 6.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Inköp Moms 0%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Ingående moms",
|
||||
"account_number": "2640",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 0.00
|
||||
},
|
||||
"description": "Moms 0%",
|
||||
"rate": 0.00
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"item_tax_templates": [
|
||||
{
|
||||
"title": "Artikel Moms 25%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Utgående moms, 25 %",
|
||||
"account_number": "2610",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 25.00
|
||||
},
|
||||
"description": "Moms 25%",
|
||||
"tax_rate": 25.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Artikel Moms 12%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Utgående moms, 12 %",
|
||||
"account_number": "2620",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 12.00
|
||||
},
|
||||
"description": "Moms 12%",
|
||||
"tax_rate": 12.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Artikel Moms 6%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Utgående moms, 6 %",
|
||||
"account_number": "2630",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 6.00
|
||||
},
|
||||
"description": "Moms 6%",
|
||||
"tax_rate": 6.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Artikel Moms 0%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Utgående moms, 0 %",
|
||||
"account_number": "2611",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 0.00
|
||||
},
|
||||
"description": "Moms 0%",
|
||||
"tax_rate": 0.00
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ def get_or_create_tax_group(company_name, root_type):
|
||||
|
||||
tax_group_account.flags.ignore_links = True
|
||||
tax_group_account.flags.ignore_validate = True
|
||||
tax_group_account.insert(ignore_permissions=True)
|
||||
tax_group_account.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
tax_group_name = tax_group_account.name
|
||||
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
{
|
||||
"chart_name": "Warehouse wise Stock Value",
|
||||
"chart_type": "Custom",
|
||||
"creation": "2020-07-20 21:01:04.296157",
|
||||
"creation": "2022-03-30 00:58:02.018824",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"filters_json": "{}",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 13:01:01.815123",
|
||||
"last_synced_on": "2024-12-23 18:44:46.822164",
|
||||
"modified": "2024-12-23 19:31:17.003946",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse wise Stock Value",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"source": "Warehouse wise Stock Value",
|
||||
"timeseries": 0,
|
||||
"type": "Bar",
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import flt, nowtime
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from pypika import Order
|
||||
|
||||
@@ -112,7 +112,10 @@ class DeprecatedBatchNoValuation:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
timestamp_condition = None
|
||||
if self.sle.posting_date and self.sle.posting_time:
|
||||
if self.sle.posting_date:
|
||||
if self.sle.posting_time is None:
|
||||
self.sle.posting_time = nowtime()
|
||||
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
if not self.sle.creation:
|
||||
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
|
||||
@@ -178,6 +181,9 @@ class DeprecatedBatchNoValuation:
|
||||
stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
|
||||
self.stock_value_change += stock_value_change
|
||||
|
||||
self.non_batchwise_balance_value[batch_no] -= stock_value_change
|
||||
self.non_batchwise_balance_qty[batch_no] -= ledger.qty
|
||||
|
||||
frappe.db.set_value(
|
||||
"Serial and Batch Entry",
|
||||
ledger.name,
|
||||
@@ -217,7 +223,6 @@ class DeprecatedBatchNoValuation:
|
||||
.select(
|
||||
sle.batch_no,
|
||||
Sum(sle.actual_qty).as_("batch_qty"),
|
||||
Sum(sle.stock_value_difference).as_("batch_value"),
|
||||
)
|
||||
.where(
|
||||
(sle.item_code == self.sle.item_code)
|
||||
@@ -234,11 +239,59 @@ class DeprecatedBatchNoValuation:
|
||||
if self.sle.name:
|
||||
query = query.where(sle.name != self.sle.name)
|
||||
|
||||
for d in query.run(as_dict=True):
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
|
||||
batch_data = query.run(as_dict=True)
|
||||
for d in batch_data:
|
||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||
|
||||
last_sle = self.get_last_sle_for_non_batch()
|
||||
for d in batch_data:
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
|
||||
|
||||
def get_last_sle_for_non_batch(self):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
if not self.sle.creation:
|
||||
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
|
||||
|
||||
timestamp_condition = sle.posting_datetime < posting_datetime
|
||||
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (sle.posting_datetime == posting_datetime) & (
|
||||
sle.creation < self.sle.creation
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.inner_join(batch)
|
||||
.on(sle.batch_no == batch.name)
|
||||
.select(
|
||||
sle.stock_value,
|
||||
sle.qty_after_transaction,
|
||||
)
|
||||
.where(
|
||||
(sle.item_code == self.sle.item_code)
|
||||
& (sle.warehouse == self.sle.warehouse)
|
||||
& (sle.batch_no.isnotnull())
|
||||
& (batch.use_batchwise_valuation == 0)
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.where(timestamp_condition)
|
||||
.orderby(sle.posting_datetime, order=Order.desc)
|
||||
.orderby(sle.creation, order=Order.desc)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if self.sle.name:
|
||||
query = query.where(sle.name != self.sle.name)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
return data[0] if data else {}
|
||||
|
||||
@deprecated
|
||||
def set_balance_value_from_bundle(self) -> None:
|
||||
bundle = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-18 11:46:04.448220",
|
||||
"modified": "2024-12-19 13:48:46.618066",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Closing Stock Balance",
|
||||
@@ -121,6 +121,7 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -130,10 +131,39 @@
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
|
||||
& (
|
||||
(table.from_date.between(self.from_date, self.to_date))
|
||||
| (table.to_date.between(self.from_date, self.to_date))
|
||||
| ((table.from_date >= self.from_date) & (table.to_date >= self.to_date))
|
||||
| ((self.from_date >= table.from_date) & (table.from_date >= self.to_date))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -237,8 +237,13 @@ class InventoryDimension(Document):
|
||||
custom_fields["Stock Ledger Entry"] = dimension_field
|
||||
|
||||
filter_custom_fields = {}
|
||||
ignore_doctypes = ["Serial and Batch Bundle", "Serial and Batch Entry", "Pick List Item"]
|
||||
|
||||
if custom_fields:
|
||||
for doctype, fields in custom_fields.items():
|
||||
if doctype in ignore_doctypes:
|
||||
continue
|
||||
|
||||
if isinstance(fields, dict):
|
||||
fields = [fields]
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ frappe.ui.form.on("Material Request", {
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
primary_action_label: "Get Items",
|
||||
primary_action_label: __("Get Items"),
|
||||
primary_action(values) {
|
||||
if (!values) return;
|
||||
values["company"] = frm.doc.company;
|
||||
|
||||
@@ -766,6 +766,7 @@ def raise_work_orders(material_request):
|
||||
)
|
||||
|
||||
wo_order.set_work_order_operations()
|
||||
wo_order.flags.ignore_validate = True
|
||||
wo_order.flags.ignore_mandatory = True
|
||||
wo_order.save()
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ frappe.listview_settings["Material Request"] = {
|
||||
}
|
||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
|
||||
return [__("Pending"), "orange", "per_ordered,=,0"];
|
||||
} else if (
|
||||
doc.docstatus == 1 &&
|
||||
flt(doc.per_ordered, precision) < 100 &&
|
||||
doc.material_request_type == "Material Transfer"
|
||||
) {
|
||||
return [__("Partially Received"), "yellow", "per_ordered,<,100"];
|
||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) {
|
||||
return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
|
||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) {
|
||||
|
||||
@@ -17,6 +17,7 @@ from erpnext.stock.doctype.material_request.material_request import (
|
||||
make_supplier_quotation,
|
||||
raise_work_orders,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
|
||||
class TestMaterialRequest(FrappeTestCase):
|
||||
@@ -59,6 +60,43 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
self.assertEqual(se.doctype, "Stock Entry")
|
||||
self.assertEqual(len(se.get("items")), len(mr.get("items")))
|
||||
|
||||
def test_partial_make_stock_entry(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry as _make_stock_entry
|
||||
|
||||
mr = frappe.copy_doc(test_records[0]).insert()
|
||||
|
||||
source_wh = create_warehouse(
|
||||
warehouse_name="_Test Source Warehouse",
|
||||
properties={"parent_warehouse": "All Warehouses - _TC"},
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
mr = frappe.get_doc("Material Request", mr.name)
|
||||
mr.material_request_type = "Material Transfer"
|
||||
|
||||
for row in mr.items:
|
||||
_make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
qty=10,
|
||||
to_warehouse=source_wh,
|
||||
company="_Test Company",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
row.from_warehouse = source_wh
|
||||
row.qty = 10
|
||||
|
||||
mr.save()
|
||||
mr.submit()
|
||||
|
||||
se = make_stock_entry(mr.name)
|
||||
se.get("items")[0].qty = 5
|
||||
se.insert()
|
||||
se.submit()
|
||||
|
||||
mr.reload()
|
||||
self.assertEqual(mr.status, "Partially Received")
|
||||
|
||||
def test_in_transit_make_stock_entry(self):
|
||||
mr = frappe.copy_doc(test_records[0]).insert()
|
||||
|
||||
|
||||
@@ -1247,6 +1247,7 @@ def create_stock_entry(pick_list):
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.pick_list = pick_list.get("name")
|
||||
stock_entry.purpose = pick_list.get("purpose")
|
||||
stock_entry.company = pick_list.get("company")
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
if pick_list.get("work_order"):
|
||||
|
||||
@@ -920,12 +920,17 @@ class PurchaseReceipt(BuyingController):
|
||||
)
|
||||
|
||||
def enable_recalculate_rate_in_sles(self):
|
||||
rejected_warehouses = frappe.get_all(
|
||||
"Purchase Receipt Item", filters={"parent": self.name}, pluck="rejected_warehouse"
|
||||
)
|
||||
|
||||
sle_table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
(
|
||||
frappe.qb.update(sle_table)
|
||||
.set(sle_table.recalculate_rate, 1)
|
||||
.where(sle_table.voucher_no == self.name)
|
||||
.where(sle_table.voucher_type == "Purchase Receipt")
|
||||
.where(sle_table.warehouse.notin(rejected_warehouses))
|
||||
).run()
|
||||
|
||||
|
||||
@@ -1178,6 +1183,9 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
return pending_qty, 0
|
||||
|
||||
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
|
||||
if item_row.rejected_qty and returned_qty:
|
||||
returned_qty -= item_row.rejected_qty
|
||||
|
||||
if returned_qty:
|
||||
if returned_qty >= pending_qty:
|
||||
pending_qty = 0
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"inspection_type",
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"child_row_reference",
|
||||
"section_break_7",
|
||||
"item_code",
|
||||
"item_serial_no",
|
||||
@@ -238,6 +239,15 @@
|
||||
"fieldname": "manual_inspection",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manual Inspection"
|
||||
},
|
||||
{
|
||||
"fieldname": "child_row_reference",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Child Row Reference",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-search",
|
||||
@@ -245,7 +255,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 11:56:50.282878",
|
||||
"modified": "2024-12-30 19:08:16.611192",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Quality Inspection",
|
||||
@@ -272,4 +282,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class QualityInspection(Document):
|
||||
amended_from: DF.Link | None
|
||||
batch_no: DF.Link | None
|
||||
bom_no: DF.Link | None
|
||||
child_row_reference: DF.Data | None
|
||||
description: DF.SmallText | None
|
||||
inspected_by: DF.Link
|
||||
inspection_type: DF.Literal["", "Incoming", "Outgoing", "In Process"]
|
||||
@@ -74,6 +75,64 @@ class QualityInspection(Document):
|
||||
self.inspect_and_set_status()
|
||||
|
||||
self.validate_inspection_required()
|
||||
self.set_child_row_reference()
|
||||
|
||||
def set_child_row_reference(self):
|
||||
if self.child_row_reference:
|
||||
return
|
||||
|
||||
if not (self.reference_type and self.reference_name):
|
||||
return
|
||||
|
||||
doctype = self.reference_type + " Item"
|
||||
if self.reference_type == "Stock Entry":
|
||||
doctype = "Stock Entry Detail"
|
||||
|
||||
child_row_references = frappe.get_all(
|
||||
doctype,
|
||||
filters={"parent": self.reference_name, "item_code": self.item_code},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if not child_row_references:
|
||||
return
|
||||
|
||||
if len(child_row_references) == 1:
|
||||
self.child_row_reference = child_row_references[0]
|
||||
else:
|
||||
self.distribute_child_row_reference(child_row_references)
|
||||
|
||||
def distribute_child_row_reference(self, child_row_references):
|
||||
quality_inspections = frappe.get_all(
|
||||
"Quality Inspection",
|
||||
filters={
|
||||
"reference_name": self.reference_name,
|
||||
"item_code": self.item_code,
|
||||
"docstatus": ("<", 2),
|
||||
},
|
||||
fields=["name", "child_row_reference", "docstatus"],
|
||||
order_by="child_row_reference desc",
|
||||
)
|
||||
|
||||
for row in quality_inspections:
|
||||
if not child_row_references:
|
||||
break
|
||||
|
||||
if row.child_row_reference and row.child_row_reference in child_row_references:
|
||||
child_row_references.remove(row.child_row_reference)
|
||||
continue
|
||||
|
||||
if row.docstatus == 1:
|
||||
continue
|
||||
|
||||
if row.name == self.name:
|
||||
self.child_row_reference = child_row_references[0]
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
"Quality Inspection", row.name, "child_row_reference", child_row_references[0]
|
||||
)
|
||||
|
||||
child_row_references.remove(child_row_references[0])
|
||||
|
||||
def validate_inspection_required(self):
|
||||
if self.reference_type in ["Purchase Receipt", "Purchase Invoice"] and not frappe.get_cached_value(
|
||||
@@ -157,35 +216,38 @@ class QualityInspection(Document):
|
||||
)
|
||||
|
||||
else:
|
||||
args = [quality_inspection, self.modified, self.reference_name, self.item_code]
|
||||
doctype = self.reference_type + " Item"
|
||||
|
||||
if self.reference_type == "Stock Entry":
|
||||
doctype = "Stock Entry Detail"
|
||||
|
||||
if self.reference_type and self.reference_name:
|
||||
conditions = ""
|
||||
if doctype and self.reference_name:
|
||||
child_doc = frappe.qb.DocType(doctype)
|
||||
|
||||
query = (
|
||||
frappe.qb.update(child_doc)
|
||||
.set(child_doc.quality_inspection, quality_inspection)
|
||||
.where(
|
||||
(child_doc.parent == self.reference_name) & (child_doc.item_code == self.item_code)
|
||||
)
|
||||
)
|
||||
|
||||
if self.batch_no and self.docstatus == 1:
|
||||
conditions += " and t1.batch_no = %s"
|
||||
args.append(self.batch_no)
|
||||
query = query.where(child_doc.batch_no == self.batch_no)
|
||||
|
||||
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
|
||||
conditions += " and t1.quality_inspection = %s"
|
||||
args.append(self.name)
|
||||
query = query.where(child_doc.quality_inspection == self.name)
|
||||
|
||||
frappe.db.sql(
|
||||
f"""
|
||||
UPDATE
|
||||
`tab{doctype}` t1, `tab{self.reference_type}` t2
|
||||
SET
|
||||
t1.quality_inspection = %s, t2.modified = %s
|
||||
WHERE
|
||||
t1.parent = %s
|
||||
and t1.item_code = %s
|
||||
and t1.parent = t2.name
|
||||
{conditions}
|
||||
""",
|
||||
args,
|
||||
if self.child_row_reference:
|
||||
query = query.where(child_doc.name == self.child_row_reference)
|
||||
|
||||
query.run()
|
||||
|
||||
frappe.db.set_value(
|
||||
self.reference_type,
|
||||
self.reference_name,
|
||||
"modified",
|
||||
self.modified,
|
||||
)
|
||||
|
||||
def inspect_and_set_status(self):
|
||||
|
||||
@@ -90,12 +90,10 @@ class SerialandBatchBundle(Document):
|
||||
self.validate_duplicate_serial_and_batch_no()
|
||||
self.validate_voucher_no()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.allow_existing_serial_nos()
|
||||
|
||||
if self.type_of_transaction == "Maintenance":
|
||||
return
|
||||
|
||||
self.allow_existing_serial_nos()
|
||||
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
|
||||
self.validate_serial_nos_duplicate()
|
||||
self.check_future_entries_exists()
|
||||
@@ -809,7 +807,7 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
for serial_no, batch_no in serial_batches.items():
|
||||
if correct_batches.get(serial_no) != batch_no:
|
||||
if correct_batches.get(serial_no) and correct_batches.get(serial_no) != batch_no:
|
||||
self.throw_error_message(
|
||||
f"Serial No {bold(serial_no)} does not belong to Batch No {bold(batch_no)}"
|
||||
)
|
||||
@@ -1188,19 +1186,19 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
continue
|
||||
|
||||
if has_serial_no or (has_serial_no and has_batch_no):
|
||||
_dict = {"serial_no": row[0], "qty": 1}
|
||||
_dict = {"serial_no": row[0].strip(), "qty": 1}
|
||||
|
||||
if has_batch_no:
|
||||
_dict.update(
|
||||
{
|
||||
"batch_no": row[1],
|
||||
"batch_no": row[1].strip(),
|
||||
"qty": row[2],
|
||||
}
|
||||
)
|
||||
|
||||
batch_nos.append(
|
||||
{
|
||||
"batch_no": row[1],
|
||||
"batch_no": row[1].strip(),
|
||||
"qty": row[2],
|
||||
}
|
||||
)
|
||||
@@ -1209,7 +1207,7 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
elif has_batch_no:
|
||||
batch_nos.append(
|
||||
{
|
||||
"batch_no": row[0],
|
||||
"batch_no": row[0].strip(),
|
||||
"qty": row[1],
|
||||
}
|
||||
)
|
||||
@@ -1253,7 +1251,7 @@ def make_serial_nos(item_code, serial_nos):
|
||||
"Item", item_code, ["description", "item_code", "item_name", "warranty_period"], as_dict=1
|
||||
)
|
||||
|
||||
serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
|
||||
serial_nos = [d.get("serial_no").strip() for d in serial_nos if d.get("serial_no")]
|
||||
existing_serial_nos = frappe.get_all("Serial No", filters={"name": ("in", serial_nos)})
|
||||
|
||||
existing_serial_nos = [d.get("name") for d in existing_serial_nos if d.get("name")]
|
||||
@@ -2101,6 +2099,8 @@ def update_available_batches(available_batches, *reserved_batches) -> None:
|
||||
|
||||
|
||||
def get_available_batches(kwargs):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
|
||||
batch_table = frappe.qb.DocType("Batch")
|
||||
@@ -2128,9 +2128,9 @@ def get_available_batches(kwargs):
|
||||
if kwargs.get("posting_time") is None:
|
||||
kwargs.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = CombineDatetime(
|
||||
stock_ledger_entry.posting_date, stock_ledger_entry.posting_time
|
||||
) <= CombineDatetime(kwargs.posting_date, kwargs.posting_time)
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
for qty, valuation in {10: 100, 20: 200}.items():
|
||||
stock_queue.append([qty, valuation])
|
||||
qty_after_transaction += qty
|
||||
balance_value += qty_after_transaction * valuation
|
||||
balance_value += qty * valuation
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
@@ -177,6 +177,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
"incoming_rate": valuation,
|
||||
"qty_after_transaction": qty_after_transaction,
|
||||
"stock_value_difference": valuation * qty,
|
||||
"stock_value": balance_value,
|
||||
"balance_value": balance_value,
|
||||
"valuation_rate": balance_value / qty_after_transaction,
|
||||
"actual_qty": qty,
|
||||
@@ -186,6 +187,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
}
|
||||
)
|
||||
|
||||
doc.set_posting_datetime()
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_links = True
|
||||
@@ -586,6 +588,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
"company": "_Test Company",
|
||||
}
|
||||
)
|
||||
doc.set_posting_datetime()
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_links = True
|
||||
|
||||
@@ -371,6 +371,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
function () {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.stock_entry.stock_entry.get_expired_batch_items",
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
frm.set_value("items", []);
|
||||
|
||||
@@ -201,7 +201,6 @@ class StockEntry(StockController):
|
||||
self.validate_purpose()
|
||||
self.validate_item()
|
||||
self.validate_customer_provided_item()
|
||||
self.validate_qty()
|
||||
self.set_transfer_qty()
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
||||
@@ -232,7 +231,7 @@ class StockEntry(StockController):
|
||||
self.validate_serialized_batch()
|
||||
self.calculate_rate_and_amount()
|
||||
self.validate_putaway_capacity()
|
||||
self.validate_component_quantities()
|
||||
self.validate_component_and_quantities()
|
||||
|
||||
if not self.get("purpose") == "Manufacture":
|
||||
# ignore scrap item wh difference and empty source/target wh
|
||||
@@ -463,40 +462,6 @@ class StockEntry(StockController):
|
||||
flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
|
||||
)
|
||||
|
||||
def validate_qty(self):
|
||||
manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]
|
||||
|
||||
if self.purpose in manufacture_purpose and self.work_order:
|
||||
if not frappe.get_value("Work Order", self.work_order, "skip_transfer"):
|
||||
item_code = []
|
||||
for item in self.items:
|
||||
if cstr(item.t_warehouse) == "":
|
||||
req_items = frappe.get_all(
|
||||
"Work Order Item",
|
||||
filters={"parent": self.work_order, "item_code": item.item_code},
|
||||
fields=["item_code"],
|
||||
)
|
||||
|
||||
transferred_materials = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
sum(sed.qty) as qty
|
||||
from `tabStock Entry` se,`tabStock Entry Detail` sed
|
||||
where
|
||||
se.name = sed.parent and se.docstatus=1 and
|
||||
(se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture')
|
||||
and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != ''
|
||||
""",
|
||||
(item.item_code, self.work_order),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
stock_qty = flt(item.qty)
|
||||
trans_qty = flt(transferred_materials[0].qty)
|
||||
if req_items:
|
||||
if stock_qty > trans_qty:
|
||||
item_code.append(item.item_code)
|
||||
|
||||
def validate_fg_completed_qty(self):
|
||||
item_wise_qty = {}
|
||||
if self.purpose == "Manufacture" and self.work_order:
|
||||
@@ -748,7 +713,7 @@ class StockEntry(StockController):
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
def validate_component_quantities(self):
|
||||
def validate_component_and_quantities(self):
|
||||
if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]:
|
||||
return
|
||||
|
||||
@@ -761,20 +726,31 @@ class StockEntry(StockController):
|
||||
raw_materials = self.get_bom_raw_materials(self.fg_completed_qty)
|
||||
|
||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||
for row in self.items:
|
||||
if not row.s_warehouse:
|
||||
continue
|
||||
|
||||
if details := raw_materials.get(row.item_code):
|
||||
if flt(details.get("qty"), precision) != flt(row.qty, precision):
|
||||
for item_code, details in raw_materials.items():
|
||||
if matched_item := self.get_matched_items(item_code):
|
||||
if flt(details.get("qty"), precision) != flt(matched_item.qty, precision):
|
||||
frappe.throw(
|
||||
_("For the item {0}, the quantity should be {1} according to the BOM {2}.").format(
|
||||
frappe.bold(row.item_code),
|
||||
flt(details.get("qty"), precision),
|
||||
frappe.bold(item_code),
|
||||
flt(details.get("qty")),
|
||||
get_link_to_form("BOM", self.bom_no),
|
||||
),
|
||||
title=_("Incorrect Component Quantity"),
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("According to the BOM {0}, the Item '{1}' is missing in the stock entry.").format(
|
||||
get_link_to_form("BOM", self.bom_no), frappe.bold(item_code)
|
||||
),
|
||||
title=_("Missing Item"),
|
||||
)
|
||||
|
||||
def get_matched_items(self, item_code):
|
||||
for row in self.items:
|
||||
if row.item_code == item_code:
|
||||
return row
|
||||
|
||||
return {}
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_and_rate(self):
|
||||
@@ -2975,17 +2951,45 @@ def get_uom_details(item_code, uom, qty):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_expired_batch_items():
|
||||
return frappe.db.sql(
|
||||
"""select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\
|
||||
from `tabBatch` b, `tabStock Ledger Entry` sle
|
||||
where b.expiry_date <= %s
|
||||
and b.expiry_date is not NULL
|
||||
and b.batch_id = sle.batch_no and sle.is_cancelled = 0
|
||||
group by sle.warehouse, sle.item_code, sle.batch_no""",
|
||||
(nowdate()),
|
||||
as_dict=1,
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_auto_batch_nos
|
||||
|
||||
expired_batches = get_expired_batches()
|
||||
if not expired_batches:
|
||||
return []
|
||||
|
||||
expired_batches_stock = get_auto_batch_nos(
|
||||
frappe._dict(
|
||||
{
|
||||
"batch_no": list(expired_batches.keys()),
|
||||
"for_stock_levels": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for row in expired_batches_stock:
|
||||
row.update(expired_batches.get(row.batch_no))
|
||||
|
||||
return expired_batches_stock
|
||||
|
||||
|
||||
def get_expired_batches():
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
data = (
|
||||
frappe.qb.from_(batch)
|
||||
.select(batch.item, batch.name.as_("batch_no"), batch.stock_uom)
|
||||
.where((batch.expiry_date <= nowdate()) & (batch.expiry_date.isnotnull()))
|
||||
).run(as_dict=True)
|
||||
|
||||
if not data:
|
||||
return []
|
||||
|
||||
expired_batches = frappe._dict()
|
||||
for row in data:
|
||||
expired_batches[row.batch_no] = row
|
||||
|
||||
return expired_batches
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_warehouse_details(args):
|
||||
@@ -3261,8 +3265,10 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
|
||||
doc.append("entries", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1})
|
||||
|
||||
elif row.batches_to_be_consume:
|
||||
precision = frappe.get_precision("Serial and Batch Entry", "qty")
|
||||
doc.has_batch_no = 1
|
||||
for batch_no, qty in row.batches_to_be_consume.items():
|
||||
qty = flt(qty, precision)
|
||||
doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
|
||||
|
||||
if not doc.entries:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user