Compare commits
35 Commits
revert-328
...
v14.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
028e939cca | ||
|
|
216cb9b07b | ||
|
|
50ad612453 | ||
|
|
6b71af9008 | ||
|
|
3c8412efdb | ||
|
|
623f56a95c | ||
|
|
b637d4d5f1 | ||
|
|
601bc64618 | ||
|
|
04d3571dd9 | ||
|
|
aa5aaa113e | ||
|
|
354a9d6169 | ||
|
|
c7e2217c92 | ||
|
|
ce5fc5b457 | ||
|
|
abe18945a6 | ||
|
|
010a0ca0a9 | ||
|
|
74664a34c0 | ||
|
|
82f1dd268d | ||
|
|
c3fd802351 | ||
|
|
6046f8bc5e | ||
|
|
adcd21724b | ||
|
|
04bdff736b | ||
|
|
65bb1d8cc2 | ||
|
|
ebf766cf62 | ||
|
|
80bf47170f | ||
|
|
0faa7b0432 | ||
|
|
1d1f12f949 | ||
|
|
4c82533239 | ||
|
|
5a28ba8537 | ||
|
|
8737c10ce4 | ||
|
|
2defb89962 | ||
|
|
53b9d61c46 | ||
|
|
3092131913 | ||
|
|
6dce122825 | ||
|
|
248cc48842 | ||
|
|
ebd8f2f45b |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
- version-14
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
@@ -13,10 +13,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js v14
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 16
|
||||
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
@@ -28,4 +30,4 @@ jobs:
|
||||
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
||||
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
||||
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
||||
run: npx semantic-release
|
||||
run: npx semantic-release
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"branches": ["version-13"],
|
||||
"branches": ["version-14"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
@@ -10,7 +10,7 @@
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec", {
|
||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
|
||||
"prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" erpnext/__init__.py'
|
||||
}
|
||||
],
|
||||
[
|
||||
@@ -21,4 +21,4 @@
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.0.0-dev"
|
||||
__version__ = "14.0.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -1184,6 +1184,7 @@ def get_outstanding_reference_documents(args):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
common_filter = []
|
||||
posting_and_due_date = []
|
||||
|
||||
# confirm that Supplier is not blocked
|
||||
if args.get("party_type") == "Supplier":
|
||||
@@ -1224,7 +1225,7 @@ def get_outstanding_reference_documents(args):
|
||||
condition += " and {0} between '{1}' and '{2}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
common_filter.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
|
||||
if args.get("company"):
|
||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||
@@ -1235,6 +1236,7 @@ def get_outstanding_reference_documents(args):
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ class PaymentReconciliation(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentReconciliation, self).__init__(*args, **kwargs)
|
||||
self.common_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unreconciled_entries(self):
|
||||
@@ -150,6 +151,7 @@ class PaymentReconciliation(Document):
|
||||
return_outstanding = ple_query.get_voucher_outstandings(
|
||||
vouchers=return_invoices,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
|
||||
get_payments=True,
|
||||
@@ -187,6 +189,7 @@ class PaymentReconciliation(Document):
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
||||
)
|
||||
@@ -350,6 +353,7 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
self.common_filter_conditions.clear()
|
||||
self.ple_posting_date_filter.clear()
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
self.common_filter_conditions.append(ple.company == self.company)
|
||||
@@ -359,15 +363,15 @@ class PaymentReconciliation(Document):
|
||||
|
||||
if get_invoices:
|
||||
if self.from_invoice_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date))
|
||||
self.ple_posting_date_filter.append(ple.posting_date.gte(self.from_invoice_date))
|
||||
if self.to_invoice_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date))
|
||||
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_invoice_date))
|
||||
|
||||
elif get_return_invoices:
|
||||
if self.from_payment_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date))
|
||||
self.ple_posting_date_filter.append(ple.posting_date.gte(self.from_payment_date))
|
||||
if self.to_payment_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date))
|
||||
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
|
||||
|
||||
def get_conditions(self, get_payments=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
|
||||
@@ -283,6 +283,41 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 2)
|
||||
self.assertEqual(len(pr.get("payments")), 2)
|
||||
|
||||
def test_filter_posting_date_case2(self):
|
||||
"""
|
||||
Posting date should not affect outstanding amount calculation
|
||||
"""
|
||||
|
||||
from_date = add_days(nowdate(), -30)
|
||||
to_date = nowdate()
|
||||
self.create_payment_entry(amount=25, posting_date=from_date).submit()
|
||||
self.create_sales_invoice(rate=25, qty=1, posting_date=to_date)
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.from_invoice_date = pr.from_payment_date = from_date
|
||||
pr.to_invoice_date = pr.to_payment_date = to_date
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
pr.from_invoice_date = pr.from_payment_date = to_date
|
||||
pr.to_invoice_date = pr.to_payment_date = to_date
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
|
||||
def test_filter_invoice_limit(self):
|
||||
# check filter condition - invoice limit
|
||||
transaction_date = nowdate()
|
||||
|
||||
@@ -2103,13 +2103,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
|
||||
source_document_warehouse_field = "target_warehouse"
|
||||
target_document_warehouse_field = "from_warehouse"
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
else:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
|
||||
source_document_warehouse_field = "from_warehouse"
|
||||
target_document_warehouse_field = "target_warehouse"
|
||||
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
received_items = {}
|
||||
|
||||
validate_inter_company_transaction(source_doc, doctype)
|
||||
details = get_inter_company_details(source_doc, doctype)
|
||||
|
||||
@@ -207,7 +207,7 @@ def set_address_details(
|
||||
)
|
||||
|
||||
if company_address:
|
||||
party_details.update({"company_address": company_address})
|
||||
party_details.company_address = company_address
|
||||
else:
|
||||
party_details.update(get_company_address(company))
|
||||
|
||||
@@ -219,12 +219,31 @@ def set_address_details(
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
|
||||
if party_details.company_address:
|
||||
party_details["shipping_address"] = shipping_address or party_details["company_address"]
|
||||
party_details.shipping_address_display = get_address_display(party_details["shipping_address"])
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "shipping_address", party_details.shipping_address)
|
||||
shipping_address=shipping_address,
|
||||
shipping_address_display=get_address_display(shipping_address),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address)
|
||||
)
|
||||
|
||||
if party_details.company_address:
|
||||
# billing address
|
||||
party_details.update(
|
||||
billing_address=party_details.company_address,
|
||||
billing_address_display=(
|
||||
party_details.company_address_display or get_address_display(party_details.company_address)
|
||||
),
|
||||
**get_fetch_values(doctype, "billing_address", party_details.company_address)
|
||||
)
|
||||
|
||||
# shipping address - if not already set
|
||||
if not party_details.shipping_address:
|
||||
party_details.update(
|
||||
shipping_address=party_details.billing_address,
|
||||
shipping_address_display=party_details.billing_address_display,
|
||||
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
|
||||
)
|
||||
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_details.get(billing_address_field), party_details.shipping_address_name
|
||||
|
||||
@@ -165,7 +165,7 @@ class ReceivablePayableReport(object):
|
||||
"range4",
|
||||
"range5",
|
||||
"future_amount",
|
||||
"remaining_balance"
|
||||
"remaining_balance",
|
||||
]
|
||||
|
||||
def get_voucher_balance(self, ple):
|
||||
|
||||
@@ -44,14 +44,14 @@ frappe.query_reports["Gross Profit"] = {
|
||||
"parent_field": "parent_invoice",
|
||||
"initial_depth": 3,
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
|
||||
column._options = "Sales Invoice";
|
||||
} else {
|
||||
column._options = "Item";
|
||||
}
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (data && (data.indent == 0.0 || row[1].content == "Total")) {
|
||||
if (data && (data.indent == 0.0 || (row[1] && row[1].content == "Total"))) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
|
||||
@@ -823,7 +823,13 @@ def get_held_invoices(party_type, party):
|
||||
|
||||
|
||||
def get_outstanding_invoices(
|
||||
party_type, party, account, common_filter=None, min_outstanding=None, max_outstanding=None
|
||||
party_type,
|
||||
party,
|
||||
account,
|
||||
common_filter=None,
|
||||
posting_date=None,
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@@ -850,6 +856,7 @@ def get_outstanding_invoices(
|
||||
ple_query = QueryPaymentLedger()
|
||||
invoice_list = ple_query.get_voucher_outstandings(
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_date,
|
||||
min_outstanding=min_outstanding,
|
||||
max_outstanding=max_outstanding,
|
||||
get_invoices=True,
|
||||
@@ -1501,6 +1508,7 @@ class QueryPaymentLedger(object):
|
||||
# query filters
|
||||
self.vouchers = []
|
||||
self.common_filter = []
|
||||
self.voucher_posting_date = []
|
||||
self.min_outstanding = None
|
||||
self.max_outstanding = None
|
||||
|
||||
@@ -1571,6 +1579,7 @@ class QueryPaymentLedger(object):
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_voucher_no))
|
||||
.where(Criterion.all(self.common_filter))
|
||||
.where(Criterion.all(self.voucher_posting_date))
|
||||
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
|
||||
)
|
||||
|
||||
@@ -1652,6 +1661,7 @@ class QueryPaymentLedger(object):
|
||||
self,
|
||||
vouchers=None,
|
||||
common_filter=None,
|
||||
posting_date=None,
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
get_payments=False,
|
||||
@@ -1671,6 +1681,7 @@ class QueryPaymentLedger(object):
|
||||
self.reset()
|
||||
self.vouchers = vouchers
|
||||
self.common_filter = common_filter or []
|
||||
self.voucher_posting_date = posting_date or []
|
||||
self.min_outstanding = min_outstanding
|
||||
self.max_outstanding = max_outstanding
|
||||
self.get_payments = get_payments
|
||||
|
||||
@@ -15,9 +15,12 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
frm.fields_dict["suppliers"].grid.get_field("contact").get_query = function(doc, cdt, cdn) {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
query: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_contacts",
|
||||
filters: {'supplier': d.supplier}
|
||||
}
|
||||
query: "frappe.contacts.doctype.contact.contact.contact_query",
|
||||
filters: {
|
||||
link_doctype: "Supplier",
|
||||
link_name: d.supplier || ""
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -286,18 +286,6 @@ def get_list_context(context=None):
|
||||
return list_context
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
"""select `tabContact`.name from `tabContact`, `tabDynamic Link`
|
||||
where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s
|
||||
and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None):
|
||||
def postprocess(source, target_doc):
|
||||
|
||||
105
erpnext/change_log/v14/v14_0_0.md
Normal file
105
erpnext/change_log/v14/v14_0_0.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Version 14.0.0 Release Notes
|
||||
|
||||
### Accounting
|
||||
|
||||
- [Improved Indian Compliance and GST APIs](https://docs.erpnext.com/docs/v14/user/manual/en/regional/india)
|
||||
- [Common Party Accounting](https://docs.erpnext.com/docs/v14/user/manual/en/accounts/articles/common_party_accounting)
|
||||
- [Provisional accounting for expenses](https://github.com/frappe/erpnext/pull/29451)
|
||||
- [Discount Accounting](https://github.com/frappe/erpnext/pull/26359)
|
||||
- [New Payment Reconciliation Tool](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/payment-reconciliation)
|
||||
- [Coupon Code in POS](https://github.com/frappe/erpnext/pull/27004)
|
||||
- [Configurable cost center allocation](https://docs.erpnext.com/docs/v14/user/manual/en/cost_center_allocation)
|
||||
- [Payment Ledger](https://docs.erpnext.com/docs/v14/user/manual/en/accounts/articles/payment_ledger)
|
||||
- [Cash and Non trade discounts in Sales Invoice](https://github.com/frappe/erpnext/pull/31405)
|
||||
- [Improved TaxJar Integration](https://docs.erpnext.com/docs/v14/user/manual/en/erpnext_integration/taxjar_integration)
|
||||
- [KSA E-Invoicing](https://docs.erpnext.com/docs/v14/user/manual/en/simplified_ksa_vat_management_and_reporting)
|
||||
- [South Africa VAT Audit Report](https://docs.erpnext.com/docs/v14/user/manual/en/regional/south_africa/vat_audit_report)
|
||||
- [E Invoice Eway Bill Distance is calculated automatically](https://github.com/frappe/erpnext/pull/30908)
|
||||
- [Payment Terms Status report](https://github.com/frappe/erpnext/pull/29137)
|
||||
- [Merge POS invoices based on customer group](https://github.com/frappe/erpnext/pull/27471)
|
||||
- [Ledger Merger](https://github.com/frappe/erpnext/pull/28812)
|
||||
- [Increase number of supported currency exchanges](https://github.com/frappe/erpnext/pull/26763)
|
||||
|
||||
|
||||
|
||||
### Stock
|
||||
- [LIFO Valuation](https://github.com/frappe/erpnext/pull/29296)
|
||||
- [Batch-wise Valuation Rates](https://github.com/frappe/erpnext/pull/29804)
|
||||
- [Better Barcode Scanning](https://github.com/frappe/erpnext/pull/30516)
|
||||
- [Over transfer allowance for material transfers](https://github.com/frappe/erpnext/pull/26264)
|
||||
- [Scanning in Pick List](https://github.com/frappe/erpnext/pull/30832)
|
||||
- [GLE reposting with progress and chunking for backdated entries](https://github.com/frappe/erpnext/pull/31343)
|
||||
|
||||
### E-Commerce
|
||||
- [Redesigned E-commerce Portal](https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce)
|
||||
- [E-commerce Search](https://docs.erpnext.com/docs/v14/user/manual/en/e_commerce/e_commerce_search)
|
||||
|
||||
|
||||
### Assets
|
||||
- [Asset Splitting](https://github.com/frappe/erpnext/pull/29350)
|
||||
- [Grouped Asset](https://github.com/frappe/erpnext/pull/29334)
|
||||
- [Asset Repair](https://github.com/frappe/erpnext/pull/25798)
|
||||
- [Consume serialized items during Asset Repair](https://github.com/frappe/erpnext/pull/28349)
|
||||
|
||||
### Manufacturing
|
||||
- [Faster BOM Update Tool](https://github.com/frappe/erpnext/pull/31078)
|
||||
- [Scrap Item in Job Card](https://github.com/frappe/erpnext/pull/27518)
|
||||
- [Process Loss in manufacturing](https://github.com/frappe/erpnext/pull/26151)
|
||||
- [Production Plan Summary Report](https://github.com/frappe/erpnext/pull/26240)
|
||||
- [Work Order Consumed Materials Report](https://github.com/frappe/erpnext/pull/28500)
|
||||
- [Provision to close the Work Order](https://github.com/frappe/erpnext/pull/28150)
|
||||
- [Provision to aggregate subassembly items in production plan](https://github.com/frappe/erpnext/pull/28939)
|
||||
|
||||
### Subcontracting
|
||||
- [New Subcontracting Module](https://github.com/frappe/erpnext/pull/30955)
|
||||
- [Subcontracted Purchase Order from the Production Plan](https://github.com/frappe/erpnext/pull/26240)
|
||||
|
||||
|
||||
### CRM
|
||||
- [Refreshed CRM Flows](https://github.com/frappe/erpnext/pull/31311)
|
||||
- [New Prospect document](https://github.com/frappe/erpnext/pull/27102)
|
||||
- [CRM Settings Page](https://docs.erpnext.com/docs/v13/user/manual/en/CRM/crm_settings)
|
||||
- [Competitor Tagging in Opportunity and Quotation](https://github.com/frappe/erpnext/pull/28050)
|
||||
- [Sales Pipeline Analytics Report](https://github.com/frappe/erpnext/pull/26639)
|
||||
- [Opportunity Summary by Sales Stage Report](https://github.com/frappe/erpnext/pull/26639)
|
||||
|
||||
|
||||
### HR & Payroll
|
||||
- [Organizational Chart](https://github.com/frappe/erpnext/pull/26261)
|
||||
- [Full and Final Settlement](https://github.com/frappe/erpnext/pull/26364)
|
||||
- [Income tax computation Report](https://github.com/frappe/erpnext/pull/29963)
|
||||
- [Employee Grievance](https://github.com/frappe/erpnext/pull/25705)
|
||||
- [Tax for recurring additional salary](https://github.com/frappe/erpnext/pull/27459)
|
||||
- [Tracking Multi-round interview](https://github.com/frappe/erpnext/pull/25482)
|
||||
- [Exit Interview and Employee Exits Report](https://github.com/frappe/erpnext/pull/28741)
|
||||
- [Leave Type configuration to allow over allocation](https://github.com/frappe/erpnext/pull/30940)
|
||||
- [Employee Reminders](https://github.com/frappe/erpnext/pull/25735)
|
||||
- [Refactored Employee Leave Balance](https://github.com/frappe/erpnext/pull/29439)
|
||||
|
||||
### Healthcare
|
||||
- [Treatment Plan Template](https://github.com/frappe/erpnext/pull/26557)
|
||||
- [Capacity for Service Unit, concurrent appointments based on capacity, Patient Appointments](https://github.com/frappe/erpnext/pull/27219)
|
||||
- [UOM specific barcode](https://docs.erpnext.com/docs/v14/user/manual/en/stock/articles/track-items-using-barcode#uom-specific-barcode)
|
||||
- [Redesigned Patient History and Patient Progress](https://github.com/frappe/erpnext/pull/27100)
|
||||
|
||||
|
||||
### New apps
|
||||
The following modules has been separated out from ERPNext and new apps has been created.
|
||||
|
||||
- [HR and Payroll](https://github.com/frappe/hrms)
|
||||
- [Healthcare](https://github.com/frappe/health)
|
||||
- [Education](https://github.com/frappe/education)
|
||||
- [E-commerce Integration](https://github.com/frappe/ecommerce_integrations)
|
||||
- [Hospitality](https://github.com/frappe/hospitality)
|
||||
- [Non-Profit](https://github.com/frappe/non_profit)
|
||||
- [Agriculture](https://github.com/frappe/agriculture)
|
||||
- [Datev Integration](https://github.com/alyf-de/erpnext_datev)
|
||||
- [Germany Localisation](https://github.com/alyf-de/erpnext_germany)
|
||||
|
||||
### Others
|
||||
- [Unicommerce Integration](https://docs.erpnext.com/docs/v13/user/manual/en/erpnext_integration/unicommerce_integration)
|
||||
- [Bulk Transaction Processing](https://github.com/frappe/erpnext/pull/28580)
|
||||
- [Refactored Document Naming Settings](https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/settings/document-naming-settings)
|
||||
- [Project Portal Enhancements](https://github.com/frappe/erpnext/pull/26090)
|
||||
- [Refund entry against loans](https://github.com/frappe/erpnext/pull/29460)
|
||||
- [Bank Reconciliation for loan documents](https://github.com/frappe/erpnext/pull/29865)
|
||||
@@ -86,6 +86,7 @@ class BuyingController(SubcontractingController):
|
||||
company=self.company,
|
||||
party_address=self.get("supplier_address"),
|
||||
shipping_address=self.get("shipping_address"),
|
||||
company_address=self.get("billing_address"),
|
||||
fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"),
|
||||
ignore_permissions=self.flags.ignore_permissions,
|
||||
)
|
||||
|
||||
@@ -18,8 +18,9 @@ from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Employee"
|
||||
conditions = []
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
fields = get_fields(doctype, ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabEmployee`
|
||||
@@ -49,7 +50,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Lead", ["name", "lead_name", "company_name"])
|
||||
doctype = "Lead"
|
||||
fields = get_fields(doctype, ["name", "lead_name", "company_name"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabLead`
|
||||
@@ -77,6 +79,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Customer"
|
||||
conditions = []
|
||||
cust_master_name = frappe.defaults.get_user_default("cust_master_name")
|
||||
|
||||
@@ -85,9 +88,9 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
else:
|
||||
fields = ["name", "customer_name", "customer_group", "territory"]
|
||||
|
||||
fields = get_fields("Customer", fields)
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
searchfields = frappe.get_meta("Customer").get_search_fields()
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
return frappe.db.sql(
|
||||
@@ -116,6 +119,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Supplier"
|
||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
||||
|
||||
if supp_master_name == "Supplier Name":
|
||||
@@ -123,7 +127,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
else:
|
||||
fields = ["name", "supplier_name", "supplier_group"]
|
||||
|
||||
fields = get_fields("Supplier", fields)
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {field} from `tabSupplier`
|
||||
@@ -147,6 +151,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Account"
|
||||
company_currency = erpnext.get_company_currency(filters.get("company"))
|
||||
|
||||
def get_accounts(with_account_type_filter):
|
||||
@@ -197,13 +202,14 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
doctype = "Item"
|
||||
conditions = []
|
||||
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
|
||||
# Get searchfields from meta and use in Item Link field query
|
||||
meta = frappe.get_meta("Item", cached=True)
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
# these are handled separately
|
||||
@@ -257,7 +263,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
filters.pop("supplier", None)
|
||||
|
||||
description_cond = ""
|
||||
if frappe.db.count("Item", cache=True) < 50000:
|
||||
if frappe.db.count(doctype, cache=True) < 50000:
|
||||
# scan description only if items are less than 50000
|
||||
description_cond = "or tabItem.description LIKE %(txt)s"
|
||||
return frappe.db.sql(
|
||||
@@ -300,8 +306,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "BOM"
|
||||
conditions = []
|
||||
fields = get_fields("BOM", ["name", "item"])
|
||||
fields = get_fields(doctype, ["name", "item"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields}
|
||||
@@ -331,6 +338,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Project"
|
||||
cond = ""
|
||||
if filters and filters.get("customer"):
|
||||
cond = """(`tabProject`.customer = %s or
|
||||
@@ -338,8 +346,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
frappe.db.escape(filters.get("customer"))
|
||||
)
|
||||
|
||||
fields = get_fields("Project", ["name", "project_name"])
|
||||
searchfields = frappe.get_meta("Project").get_search_fields()
|
||||
fields = get_fields(doctype, ["name", "project_name"])
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
|
||||
|
||||
return frappe.db.sql(
|
||||
@@ -366,7 +374,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
|
||||
fields = get_fields("Delivery Note", ["name", "customer", "posting_date"])
|
||||
doctype = "Delivery Note"
|
||||
fields = get_fields(doctype, ["name", "customer", "posting_date"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
@@ -402,6 +411,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Batch"
|
||||
cond = ""
|
||||
if filters.get("posting_date"):
|
||||
cond = "and (batch.expiry_date is null or batch.expiry_date >= %(posting_date)s)"
|
||||
@@ -420,7 +430,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("is_return"):
|
||||
having_clause = ""
|
||||
|
||||
meta = frappe.get_meta("Batch", cached=True)
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
search_columns = ""
|
||||
@@ -496,6 +506,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Account"
|
||||
filter_list = []
|
||||
|
||||
if isinstance(filters, dict):
|
||||
@@ -514,7 +525,7 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
|
||||
|
||||
return frappe.desk.reportview.execute(
|
||||
"Account",
|
||||
doctype,
|
||||
filters=filter_list,
|
||||
fields=["name", "parent_account"],
|
||||
limit_start=start,
|
||||
@@ -553,6 +564,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
@@ -628,6 +640,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
@@ -650,6 +663,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# Should be used when item code is passed in filters.
|
||||
doctype = "Warehouse"
|
||||
conditions, bin_conditions = [], []
|
||||
filter_dict = get_doctype_wise_filters(filters)
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BatchExpiredError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class StockController(AccountsController):
|
||||
def validate(self):
|
||||
super(StockController, self).validate()
|
||||
@@ -77,6 +81,10 @@ class StockController(AccountsController):
|
||||
def validate_serialized_batch(self):
|
||||
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":
|
||||
is_material_issue = True
|
||||
|
||||
for d in self.get("items"):
|
||||
if hasattr(d, "serial_no") and hasattr(d, "batch_no") and d.serial_no and d.batch_no:
|
||||
serial_nos = frappe.get_all(
|
||||
@@ -93,6 +101,9 @@ class StockController(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
if is_material_issue:
|
||||
continue
|
||||
|
||||
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
|
||||
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
|
||||
|
||||
@@ -100,7 +111,8 @@ class StockController(AccountsController):
|
||||
frappe.throw(
|
||||
_("Row #{0}: The batch {1} has already expired.").format(
|
||||
d.idx, get_link_to_form("Batch", d.get("batch_no"))
|
||||
)
|
||||
),
|
||||
BatchExpiredError,
|
||||
)
|
||||
|
||||
def clean_serial_nos(self):
|
||||
@@ -310,7 +322,13 @@ class StockController(AccountsController):
|
||||
)
|
||||
if (
|
||||
self.doctype
|
||||
not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry")
|
||||
not in (
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Stock Reconciliation",
|
||||
"Stock Entry",
|
||||
"Subcontracting Receipt",
|
||||
)
|
||||
and not is_expense_account
|
||||
):
|
||||
frappe.throw(
|
||||
@@ -374,9 +392,24 @@ class StockController(AccountsController):
|
||||
def update_inventory_dimensions(self, row, sl_dict) -> None:
|
||||
dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self)
|
||||
for dimension in dimensions:
|
||||
if dimension and row.get(dimension.source_fieldname):
|
||||
if not dimension:
|
||||
continue
|
||||
|
||||
if row.get(dimension.source_fieldname):
|
||||
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
|
||||
|
||||
if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent:
|
||||
sl_dict[dimension.target_fieldname] = self.get(dimension.fetch_from_parent)
|
||||
|
||||
# Get value based on doctype name
|
||||
if not sl_dict.get(dimension.target_fieldname):
|
||||
fieldname = frappe.get_cached_value(
|
||||
"DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname"
|
||||
)
|
||||
|
||||
if fieldname and self.get(fieldname):
|
||||
sl_dict[dimension.target_fieldname] = self.get(fieldname)
|
||||
|
||||
def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
from erpnext.stock.stock_ledger import make_sl_entries
|
||||
|
||||
|
||||
@@ -507,6 +507,7 @@ accounting_dimension_doctypes = [
|
||||
"Shipping Rule",
|
||||
"Landed Cost Item",
|
||||
"Asset Value Adjustment",
|
||||
"Asset Repair",
|
||||
"Loyalty Program",
|
||||
"Stock Reconciliation",
|
||||
"POS Profile",
|
||||
@@ -519,6 +520,10 @@ accounting_dimension_doctypes = [
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Sales Order",
|
||||
"Subcontracting Order",
|
||||
"Subcontracting Order Item",
|
||||
"Subcontracting Receipt",
|
||||
"Subcontracting Receipt Item",
|
||||
]
|
||||
|
||||
# get matching queries for Bank Reconciliation
|
||||
|
||||
@@ -189,8 +189,8 @@ class BOM(WebsiteGenerator):
|
||||
self.validate_transfer_against()
|
||||
self.set_routing_operations()
|
||||
self.validate_operations()
|
||||
self.update_exploded_items(save=False)
|
||||
self.calculate_cost()
|
||||
self.update_exploded_items(save=False)
|
||||
self.update_stock_qty()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
||||
self.validate_scrap_items()
|
||||
|
||||
@@ -611,6 +611,34 @@ class TestBOM(FrappeTestCase):
|
||||
bom.reload()
|
||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||
|
||||
def test_exploded_items_rate(self):
|
||||
rm_item = make_item(
|
||||
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
||||
).name
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_save=True)
|
||||
|
||||
bom.rm_cost_as_per = "Last Purchase Rate"
|
||||
bom.save()
|
||||
self.assertEqual(bom.items[0].base_rate, 89)
|
||||
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
||||
|
||||
bom.rm_cost_as_per = "Price List"
|
||||
bom.save()
|
||||
self.assertEqual(bom.items[0].base_rate, 0.0)
|
||||
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
||||
|
||||
bom.rm_cost_as_per = "Valuation Rate"
|
||||
bom.save()
|
||||
self.assertEqual(bom.items[0].base_rate, 99)
|
||||
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
||||
|
||||
bom.submit()
|
||||
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"options": "currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -288,7 +289,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-19 02:32:43.785470",
|
||||
"modified": "2022-07-28 10:20:51.559010",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Item",
|
||||
|
||||
@@ -482,7 +482,6 @@ class ProductionPlan(Document):
|
||||
"bom_no",
|
||||
"stock_uom",
|
||||
"bom_level",
|
||||
"production_plan_item",
|
||||
"schedule_date",
|
||||
]:
|
||||
if row.get(field):
|
||||
@@ -639,6 +638,9 @@ class ProductionPlan(Document):
|
||||
sub_assembly_items_store = [] # temporary store to process all subassembly items
|
||||
|
||||
for row in self.po_items:
|
||||
if not row.item_code:
|
||||
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
|
||||
|
||||
bom_data = []
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||
|
||||
@@ -11,8 +11,9 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_warehouse_list,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_se_from_wo
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
@@ -583,9 +584,6 @@ class TestProductionPlan(FrappeTestCase):
|
||||
Test Prod Plan impact via: SO -> Prod Plan -> WO -> SE -> SE (cancel)
|
||||
"""
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_se_from_wo,
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100
|
||||
@@ -629,9 +627,6 @@ class TestProductionPlan(FrappeTestCase):
|
||||
def test_production_plan_pending_qty_independent_items(self):
|
||||
"Test Prod Plan impact if items are added independently (no from SO or MR)."
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_se_from_wo,
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100
|
||||
@@ -728,6 +723,57 @@ class TestProductionPlan(FrappeTestCase):
|
||||
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
|
||||
self.assertEqual(po_item.name, subassy_item.production_plan_item)
|
||||
|
||||
def test_produced_qty_for_multi_level_bom_item(self):
|
||||
# Create Items and BOMs
|
||||
rm_item = make_item(properties={"is_stock_item": 1}).name
|
||||
sub_assembly_item = make_item(properties={"is_stock_item": 1}).name
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
make_stock_entry(
|
||||
item_code=rm_item,
|
||||
qty=60,
|
||||
to_warehouse="Work In Progress - _TC",
|
||||
rate=99,
|
||||
purpose="Material Receipt",
|
||||
)
|
||||
|
||||
make_bom(item=sub_assembly_item, raw_materials=[rm_item], rm_qty=3)
|
||||
make_bom(item=fg_item, raw_materials=[sub_assembly_item], rm_qty=4)
|
||||
|
||||
# Step - 1: Create Production Plan
|
||||
pln = create_production_plan(item_code=fg_item, planned_qty=5, skip_getting_mr_items=1)
|
||||
pln.get_sub_assembly_items()
|
||||
|
||||
# Step - 2: Create Work Orders
|
||||
pln.make_work_order()
|
||||
work_orders = frappe.get_all("Work Order", filters={"production_plan": pln.name}, pluck="name")
|
||||
sa_wo = fg_wo = None
|
||||
for work_order in work_orders:
|
||||
wo_doc = frappe.get_doc("Work Order", work_order)
|
||||
if wo_doc.production_plan_item:
|
||||
wo_doc.update(
|
||||
{"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"}
|
||||
)
|
||||
fg_wo = wo_doc.name
|
||||
else:
|
||||
wo_doc.update(
|
||||
{"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Work In Progress - _TC"}
|
||||
)
|
||||
sa_wo = wo_doc.name
|
||||
wo_doc.submit()
|
||||
|
||||
# Step - 3: Complete Work Orders
|
||||
se = frappe.get_doc(make_se_from_wo(sa_wo, "Manufacture"))
|
||||
se.submit()
|
||||
|
||||
se = frappe.get_doc(make_se_from_wo(fg_wo, "Manufacture"))
|
||||
se.submit()
|
||||
|
||||
# Step - 4: Check Production Plan Item Produced Qty
|
||||
pln.load_from_db()
|
||||
self.assertEqual(pln.status, "Completed")
|
||||
self.assertEqual(pln.po_items[0].produced_qty, 5)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -268,6 +268,7 @@ erpnext.patches.v13_0.enable_ksa_vat_docs #1
|
||||
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
|
||||
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
|
||||
[post_model_sync]
|
||||
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||
@@ -308,4 +309,5 @@ erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.patches.v14_0.crm_ux_cleanup
|
||||
erpnext.patches.v14_0.remove_india_localisation # 14-07-2022
|
||||
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
|
||||
erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022
|
||||
erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
|
||||
@@ -0,0 +1,29 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
|
||||
def execute():
|
||||
accounting_dimensions = frappe.db.get_all(
|
||||
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||
)
|
||||
|
||||
if not accounting_dimensions:
|
||||
return
|
||||
|
||||
for d in accounting_dimensions:
|
||||
doctype = "Asset Repair"
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
continue
|
||||
|
||||
df = {
|
||||
"fieldname": d.fieldname,
|
||||
"label": d.label,
|
||||
"fieldtype": "Link",
|
||||
"options": d.document_type,
|
||||
"insert_after": "accounting_dimensions_section",
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
@@ -0,0 +1,47 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
|
||||
def execute():
|
||||
accounting_dimensions = frappe.db.get_all(
|
||||
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||
)
|
||||
|
||||
if not accounting_dimensions:
|
||||
return
|
||||
|
||||
count = 1
|
||||
for d in accounting_dimensions:
|
||||
|
||||
if count % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
insert_after_field = "accounting_dimensions_section"
|
||||
|
||||
for doctype in [
|
||||
"Subcontracting Order",
|
||||
"Subcontracting Order Item",
|
||||
"Subcontracting Receipt",
|
||||
"Subcontracting Receipt Item",
|
||||
]:
|
||||
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
continue
|
||||
|
||||
df = {
|
||||
"fieldname": d.fieldname,
|
||||
"label": d.label,
|
||||
"fieldtype": "Link",
|
||||
"options": d.document_type,
|
||||
"insert_after": insert_after_field,
|
||||
}
|
||||
|
||||
try:
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
count += 1
|
||||
@@ -57,7 +57,11 @@ class TestHomepageSection(unittest.TestCase):
|
||||
self.assertEqual(cards[0].h5.text, "Card 1")
|
||||
self.assertEqual(cards[0].a["href"], "/card-1")
|
||||
self.assertEqual(cards[1].p.text, "Subtitle 2")
|
||||
self.assertEqual(cards[1].find(class_="website-image-lazy")["data-src"], "test.jpg")
|
||||
|
||||
img = cards[1].find(class_="card-img-top")
|
||||
|
||||
self.assertEqual(img["src"], "test.jpg")
|
||||
self.assertEqual(img["loading"], "lazy")
|
||||
|
||||
# cleanup
|
||||
frappe.db.rollback()
|
||||
|
||||
@@ -379,7 +379,7 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters):
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, full_name) > 0 then locate(%(_txt)s, full_name) else 99999 end)
|
||||
(case when locate(%(_txt)s, full_name) > 0 then locate(%(_txt)s, full_name) else 99999 end),
|
||||
idx desc,
|
||||
name, full_name
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide('erpnext.accounts.dimensions');
|
||||
|
||||
erpnext.TransactionController = class TransactionController extends erpnext.taxes_and_totals {
|
||||
setup() {
|
||||
@@ -794,24 +793,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
set_party_account(set_pricing);
|
||||
});
|
||||
|
||||
// Get default company billing address in Purchase Invoice, Order and Receipt
|
||||
if (this.frm.doc.company && frappe.meta.get_docfield(this.frm.doctype, "billing_address")) {
|
||||
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 || ""},
|
||||
debounce: 2000,
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
me.frm.set_value("billing_address", r.message);
|
||||
} else {
|
||||
if (frappe.meta.get_docfield(me.frm.doctype, 'company_address')) {
|
||||
me.frm.set_value("company_address", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
set_party_account(set_pricing);
|
||||
}
|
||||
|
||||
@@ -3,25 +3,14 @@
|
||||
|
||||
frappe.provide("erpnext.utils");
|
||||
|
||||
const SALES_DOCTYPES = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice'];
|
||||
const PURCHASE_DOCTYPES = ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'];
|
||||
|
||||
erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
if (!method) {
|
||||
method = "erpnext.accounts.party.get_party_details";
|
||||
}
|
||||
|
||||
if (args) {
|
||||
if (in_list(['Sales Invoice', 'Sales Order', 'Delivery Note'], frm.doc.doctype)) {
|
||||
if (frm.doc.company_address && (!args.company_address)) {
|
||||
args.company_address = frm.doc.company_address;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_list(['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'], frm.doc.doctype)) {
|
||||
if (frm.doc.shipping_address && (!args.shipping_address)) {
|
||||
args.shipping_address = frm.doc.shipping_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
if ((frm.doctype != "Purchase Order" && frm.doc.customer)
|
||||
|| (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) {
|
||||
@@ -45,41 +34,44 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
};
|
||||
}
|
||||
|
||||
if (in_list(['Sales Invoice', 'Sales Order', 'Delivery Note'], frm.doc.doctype)) {
|
||||
if (!args) {
|
||||
if (!args) {
|
||||
if (in_list(SALES_DOCTYPES, frm.doc.doctype)) {
|
||||
args = {
|
||||
party: frm.doc.customer || frm.doc.party_name,
|
||||
party_type: 'Customer'
|
||||
}
|
||||
}
|
||||
if (frm.doc.company_address && (!args.company_address)) {
|
||||
args.company_address = frm.doc.company_address;
|
||||
};
|
||||
}
|
||||
|
||||
if (frm.doc.shipping_address_name &&(!args.shipping_address_name)) {
|
||||
args.shipping_address_name = frm.doc.shipping_address_name;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_list(['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'], frm.doc.doctype)) {
|
||||
if (!args) {
|
||||
if (in_list(PURCHASE_DOCTYPES, frm.doc.doctype)) {
|
||||
args = {
|
||||
party: frm.doc.supplier,
|
||||
party_type: 'Supplier'
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.shipping_address && (!args.shipping_address)) {
|
||||
args.shipping_address = frm.doc.shipping_address;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (args) {
|
||||
args.posting_date = frm.doc.posting_date || frm.doc.transaction_date;
|
||||
args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template);
|
||||
if (!args || !args.party) return;
|
||||
|
||||
args.posting_date = frm.doc.posting_date || frm.doc.transaction_date;
|
||||
args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template);
|
||||
}
|
||||
|
||||
if (in_list(SALES_DOCTYPES, frm.doc.doctype)) {
|
||||
if (!args.company_address && frm.doc.company_address) {
|
||||
args.company_address = frm.doc.company_address;
|
||||
}
|
||||
}
|
||||
if (!args || !args.party) return;
|
||||
|
||||
if (in_list(PURCHASE_DOCTYPES, frm.doc.doctype)) {
|
||||
if (!args.company_address && frm.doc.billing_address) {
|
||||
args.company_address = frm.doc.billing_address;
|
||||
}
|
||||
|
||||
if (!args.shipping_address && frm.doc.shipping_address) {
|
||||
args.shipping_address = frm.doc.shipping_address;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {
|
||||
if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date",
|
||||
|
||||
@@ -142,10 +142,6 @@ def get_item_for_list_in_html(context):
|
||||
if (context.get("website_image") or "").startswith("files/"):
|
||||
context["website_image"] = "/" + quote(context["website_image"])
|
||||
|
||||
context["show_availability_status"] = cint(
|
||||
frappe.db.get_single_value("E Commerce Settings", "show_availability_status")
|
||||
)
|
||||
|
||||
products_template = "templates/includes/products_as_list.html"
|
||||
|
||||
return frappe.get_template(products_template).render(context)
|
||||
|
||||
@@ -473,7 +473,13 @@ def make_new_batch(**args):
|
||||
"doctype": "Batch",
|
||||
"batch_id": args.batch_id,
|
||||
"item": args.item_code,
|
||||
"expiry_date": args.expiry_date,
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
|
||||
if args.expiry_date:
|
||||
batch.expiry_date = args.expiry_date
|
||||
|
||||
batch.insert()
|
||||
|
||||
return batch
|
||||
|
||||
@@ -35,14 +35,39 @@ frappe.ui.form.on('Inventory Dimension', {
|
||||
refresh(frm) {
|
||||
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
|
||||
&& frm.doc.__onload.has_stock_ledger.length) {
|
||||
let msg = __('Stock transactions exists against this dimension, user can not update document.');
|
||||
frm.dashboard.add_comment(msg, 'blue', true);
|
||||
let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
|
||||
'type_of_transaction', 'condition'];
|
||||
|
||||
frm.fields.forEach((field) => {
|
||||
if (field.df.fieldname !== 'disabled') {
|
||||
if (!in_list(allow_to_edit_fields, field.df.fieldname)) {
|
||||
frm.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frm.add_custom_button(__('Delete Dimension'), () => {
|
||||
frm.trigger('delete_dimension');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
delete_dimension(frm) {
|
||||
let msg = (`
|
||||
Custom fields related to this dimension will be deleted on deletion of dimension.
|
||||
<br> Do you want to delete {0} dimension?
|
||||
`);
|
||||
|
||||
frappe.confirm(__(msg, [frm.doc.name.bold()]), () => {
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.delete_dimension',
|
||||
args: {
|
||||
dimension: frm.doc.name
|
||||
},
|
||||
callback: function() {
|
||||
frappe.set_route('List', 'Inventory Dimension');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:dimension_name",
|
||||
"creation": "2022-06-17 13:04:16.554051",
|
||||
"doctype": "DocType",
|
||||
@@ -22,6 +21,7 @@
|
||||
"document_type",
|
||||
"istable",
|
||||
"type_of_transaction",
|
||||
"fetch_from_parent",
|
||||
"column_break_16",
|
||||
"condition",
|
||||
"applicable_condition_example_section",
|
||||
@@ -101,12 +101,14 @@
|
||||
"fieldname": "target_fieldname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Target Fieldname (Stock Ledger Entry)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "source_fieldname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Source Fieldname",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -123,7 +125,7 @@
|
||||
"fieldname": "type_of_transaction",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type of Transaction",
|
||||
"options": "\nInward\nOutward"
|
||||
"options": "\nInward\nOutward\nBoth"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_19",
|
||||
@@ -140,11 +142,18 @@
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "istable",
|
||||
"description": "Set fieldname or DocType name like Supplier, Customer etc.",
|
||||
"fieldname": "fetch_from_parent",
|
||||
"fieldtype": "Data",
|
||||
"label": "Fetch Value From Parent Form"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-19 21:06:11.824976",
|
||||
"modified": "2022-08-17 11:43:24.722441",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Inventory Dimension",
|
||||
|
||||
@@ -43,13 +43,37 @@ class InventoryDimension(Document):
|
||||
return
|
||||
|
||||
old_doc = self._doc_before_save
|
||||
allow_to_edit_fields = [
|
||||
"disabled",
|
||||
"fetch_from_parent",
|
||||
"type_of_transaction",
|
||||
"condition",
|
||||
]
|
||||
|
||||
for field in frappe.get_meta("Inventory Dimension").fields:
|
||||
if field.fieldname != "disabled" and old_doc.get(field.fieldname) != self.get(field.fieldname):
|
||||
if field.fieldname not in allow_to_edit_fields and old_doc.get(field.fieldname) != self.get(
|
||||
field.fieldname
|
||||
):
|
||||
msg = f"""The user can not change value of the field {bold(field.label)} because
|
||||
stock transactions exists against the dimension {bold(self.name)}."""
|
||||
|
||||
frappe.throw(_(msg), DoNotChangeError)
|
||||
|
||||
def on_trash(self):
|
||||
self.delete_custom_fields()
|
||||
|
||||
def delete_custom_fields(self):
|
||||
filters = {"fieldname": self.source_fieldname}
|
||||
|
||||
if self.document_type:
|
||||
filters["dt"] = self.document_type
|
||||
|
||||
for field in frappe.get_all("Custom Field", filters=filters):
|
||||
frappe.delete_doc("Custom Field", field.name)
|
||||
|
||||
msg = f"Deleted custom fields related to the dimension {self.name}"
|
||||
frappe.msgprint(_(msg))
|
||||
|
||||
def reset_value(self):
|
||||
if self.apply_to_all_doctypes:
|
||||
self.istable = 0
|
||||
@@ -76,30 +100,35 @@ class InventoryDimension(Document):
|
||||
self.add_custom_fields()
|
||||
|
||||
def add_custom_fields(self):
|
||||
dimension_field = dict(
|
||||
fieldname=self.source_fieldname,
|
||||
fieldtype="Link",
|
||||
insert_after="warehouse",
|
||||
options=self.reference_document,
|
||||
label=self.dimension_name,
|
||||
)
|
||||
dimension_fields = [
|
||||
dict(
|
||||
fieldname="inventory_dimension",
|
||||
fieldtype="Section Break",
|
||||
insert_after="warehouse",
|
||||
label="Inventory Dimension",
|
||||
collapsible=1,
|
||||
),
|
||||
dict(
|
||||
fieldname=self.source_fieldname,
|
||||
fieldtype="Link",
|
||||
insert_after="inventory_dimension",
|
||||
options=self.reference_document,
|
||||
label=self.dimension_name,
|
||||
),
|
||||
]
|
||||
|
||||
custom_fields = {}
|
||||
|
||||
if self.apply_to_all_doctypes:
|
||||
for doctype in get_inventory_documents():
|
||||
if not frappe.db.get_value(
|
||||
"Custom Field", {"dt": doctype[0], "fieldname": self.source_fieldname}
|
||||
):
|
||||
custom_fields.setdefault(doctype[0], dimension_field)
|
||||
elif not frappe.db.get_value(
|
||||
"Custom Field", {"dt": self.document_type, "fieldname": self.source_fieldname}
|
||||
):
|
||||
custom_fields.setdefault(self.document_type, dimension_field)
|
||||
custom_fields.setdefault(doctype[0], dimension_fields)
|
||||
else:
|
||||
custom_fields.setdefault(self.document_type, dimension_fields)
|
||||
|
||||
if not frappe.db.get_value(
|
||||
"Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname}
|
||||
):
|
||||
dimension_field = dimension_fields[1]
|
||||
dimension_field["fieldname"] = self.target_fieldname
|
||||
custom_fields["Stock Ledger Entry"] = dimension_field
|
||||
|
||||
@@ -143,7 +172,7 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None):
|
||||
elif (
|
||||
row.type_of_transaction == "Outward"
|
||||
if doc.docstatus == 1
|
||||
else row.type_of_transaction != "Inward"
|
||||
else row.type_of_transaction != "Outward"
|
||||
) and sl_dict.actual_qty > 0:
|
||||
continue
|
||||
|
||||
@@ -166,7 +195,14 @@ def get_document_wise_inventory_dimensions(doctype) -> dict:
|
||||
if not frappe.local.document_wise_inventory_dimensions.get(doctype):
|
||||
dimensions = frappe.get_all(
|
||||
"Inventory Dimension",
|
||||
fields=["name", "source_fieldname", "condition", "target_fieldname", "type_of_transaction"],
|
||||
fields=[
|
||||
"name",
|
||||
"source_fieldname",
|
||||
"condition",
|
||||
"target_fieldname",
|
||||
"type_of_transaction",
|
||||
"fetch_from_parent",
|
||||
],
|
||||
filters={"disabled": 0},
|
||||
or_filters={"document_type": doctype, "apply_to_all_doctypes": 1},
|
||||
)
|
||||
@@ -194,3 +230,9 @@ def get_inventory_dimensions():
|
||||
frappe.local.inventory_dimensions = dimensions
|
||||
|
||||
return frappe.local.inventory_dimensions
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_dimension(dimension):
|
||||
doc = frappe.get_doc("Inventory Dimension", dimension)
|
||||
doc.delete()
|
||||
|
||||
@@ -8,6 +8,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
CanNotBeChildDoc,
|
||||
CanNotBeDefaultDimension,
|
||||
DoNotChangeError,
|
||||
delete_dimension,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
@@ -42,6 +43,32 @@ class TestInventoryDimension(FrappeTestCase):
|
||||
|
||||
self.assertRaises(CanNotBeDefaultDimension, inv_dim1.insert)
|
||||
|
||||
def test_delete_inventory_dimension(self):
|
||||
inv_dim1 = create_inventory_dimension(
|
||||
reference_document="Shelf",
|
||||
type_of_transaction="Outward",
|
||||
dimension_name="From Shelf",
|
||||
apply_to_all_doctypes=0,
|
||||
document_type="Stock Entry Detail",
|
||||
condition="parent.purpose == 'Material Issue'",
|
||||
)
|
||||
|
||||
inv_dim1.save()
|
||||
|
||||
custom_field = frappe.db.get_value(
|
||||
"Custom Field", {"fieldname": "from_shelf", "dt": "Stock Entry Detail"}, "name"
|
||||
)
|
||||
|
||||
self.assertTrue(custom_field)
|
||||
|
||||
delete_dimension(inv_dim1.name)
|
||||
|
||||
custom_field = frappe.db.get_value(
|
||||
"Custom Field", {"fieldname": "from_shelf", "dt": "Stock Entry Detail"}, "name"
|
||||
)
|
||||
|
||||
self.assertFalse(custom_field)
|
||||
|
||||
def test_inventory_dimension(self):
|
||||
warehouse = "Shelf Warehouse - _TC"
|
||||
item_code = "_Test Item"
|
||||
|
||||
@@ -792,10 +792,8 @@
|
||||
{
|
||||
"fieldname": "expense_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Expense Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
@@ -1001,7 +999,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-17 05:32:16.483178",
|
||||
"modified": "2022-07-28 19:27:54.880781",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
||||
@@ -583,18 +583,23 @@ frappe.ui.form.on('Stock Entry', {
|
||||
},
|
||||
|
||||
add_to_transit: function(frm) {
|
||||
if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') {
|
||||
frm.set_value('to_warehouse', '');
|
||||
if(frm.doc.purpose=='Material Transfer') {
|
||||
var filters = {
|
||||
'is_group': 0,
|
||||
'company': frm.doc.company
|
||||
}
|
||||
|
||||
if(frm.doc.add_to_transit){
|
||||
filters['warehouse_type'] = 'Transit';
|
||||
frm.set_value('to_warehouse', '');
|
||||
frm.trigger('set_transit_warehouse');
|
||||
}
|
||||
|
||||
frm.fields_dict.to_warehouse.get_query = function() {
|
||||
return {
|
||||
filters:{
|
||||
'warehouse_type' : 'Transit',
|
||||
'is_group': 0,
|
||||
'company': frm.doc.company
|
||||
}
|
||||
filters:filters
|
||||
};
|
||||
};
|
||||
frm.trigger('set_transit_warehouse');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe.permissions import add_user_permission, remove_user_permission
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowdate, nowtime
|
||||
from frappe.utils import add_days, flt, nowdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.stock.doctype.item.test_item import (
|
||||
@@ -1589,6 +1589,31 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertEqual(obj.items[index].basic_rate, 200)
|
||||
self.assertEqual(obj.items[index].basic_amount, 2000)
|
||||
|
||||
def test_batch_expiry(self):
|
||||
from erpnext.controllers.stock_controller import BatchExpiredError
|
||||
from erpnext.stock.doctype.batch.test_batch import make_new_batch
|
||||
|
||||
item_code = "Test Batch Expiry Test Item - 001"
|
||||
item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
|
||||
|
||||
item_doc.has_batch_no = 1
|
||||
item_doc.save()
|
||||
|
||||
batch = make_new_batch(
|
||||
batch_id=frappe.generate_hash("", 5), item_code=item_doc.name, expiry_date=add_days(today(), -1)
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Receipt",
|
||||
qty=4,
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
batch_no=batch.name,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
self.assertRaises(BatchExpiredError, se.save)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"address_and_contact_section",
|
||||
"supplier_address",
|
||||
"address_display",
|
||||
@@ -422,12 +426,34 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Distribute Additional Costs Based On ",
|
||||
"options": "Qty\nAmount"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-11 21:02:44.097841",
|
||||
"modified": "2022-08-15 14:08:49.204218",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order",
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
"manufacture_section",
|
||||
"manufacturer",
|
||||
"manufacturer_part_no",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_34",
|
||||
"page_break"
|
||||
],
|
||||
@@ -304,13 +308,35 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-11 21:28:06.585338",
|
||||
"modified": "2022-08-15 14:25:45.177703",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Item",
|
||||
|
||||
@@ -48,6 +48,13 @@ frappe.ui.form.on('Subcontracting Receipt', {
|
||||
is_group: 0
|
||||
}
|
||||
}));
|
||||
|
||||
frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { 'company': frm.doc.company }
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
"posting_time",
|
||||
"is_return",
|
||||
"return_against",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_addresses",
|
||||
"supplier_address",
|
||||
"contact_person",
|
||||
@@ -569,11 +573,33 @@
|
||||
{
|
||||
"fieldname": "section_break_47",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions "
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-18 13:15:12.011682",
|
||||
"modified": "2022-08-15 14:30:29.447307",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt",
|
||||
|
||||
@@ -49,15 +49,16 @@
|
||||
"col_break5",
|
||||
"batch_no",
|
||||
"rejected_serial_no",
|
||||
"expense_account",
|
||||
"manufacture_details",
|
||||
"manufacturer",
|
||||
"column_break_16",
|
||||
"manufacturer_part_no",
|
||||
"accounting_details_section",
|
||||
"expense_account",
|
||||
"accounting_dimensions_section",
|
||||
"project",
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_80",
|
||||
"page_break"
|
||||
],
|
||||
@@ -363,10 +364,8 @@
|
||||
{
|
||||
"fieldname": "expense_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Expense Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -456,12 +455,17 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Details"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-21 12:07:55.899701",
|
||||
"modified": "2022-08-15 14:51:10.613347",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt Item",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="col-md-{{ section.column_value }} mb-4">
|
||||
<div class="card h-100 justify-content-between">
|
||||
{% if card.image %}
|
||||
<div class="website-image-lazy" data-class="card-img-top h-75" data-src="{{ card.image }}" data-alt="{{ card.title }}"></div>
|
||||
<img class="card-img-top h-75" src="{{ card.image }}" loading="lazy" alt="{{ card.title }}"></img>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ card.title }}</h5>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{% for item in homepage.products %}
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100 justify-content-between">
|
||||
<div class="website-image-lazy" data-class="card-img-top website-image-extra-large" data-src="{{ item.image }}" data-alt="{{ item.item_name }}"></div>
|
||||
<img class="card-img-top website-image-extra-large" src="{{ item.image }}" loading="lazy" alt="{{ item.item_name }}"></img>
|
||||
<div class="card-body flex-grow-0">
|
||||
<h5 class="card-title">{{ item.item_name }}</h5>
|
||||
<a href="{{ item.route }}" class="card-link">{{ _('More details') }}</a>
|
||||
|
||||
Reference in New Issue
Block a user