Merge branch 'develop' into campaign-module
This commit is contained in:
		
						commit
						620b54846e
					
				| @ -147,11 +147,14 @@ | ||||
| 		"Chart": true, | ||||
| 		"Cypress": true, | ||||
| 		"cy": true, | ||||
| 		"describe": true, | ||||
| 		"expect": true, | ||||
| 		"it": true, | ||||
| 		"context": true, | ||||
| 		"before": true, | ||||
| 		"beforeEach": true, | ||||
| 		"onScan": true, | ||||
| 		"extend_cscript": true | ||||
| 		"extend_cscript": true, | ||||
| 		"localforage": true, | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/helper/install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/helper/install.sh
									
									
									
									
										vendored
									
									
								
							| @ -42,6 +42,6 @@ sed -i 's/socketio:/# socketio:/g' Procfile | ||||
| sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile | ||||
| 
 | ||||
| bench get-app erpnext "${GITHUB_WORKSPACE}" | ||||
| bench start & | ||||
| bench start &> bench_run_logs.txt & | ||||
| bench --site test_site reinstall --yes | ||||
| bench build --app frappe | ||||
|  | ||||
							
								
								
									
										108
									
								
								.github/workflows/ui-tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								.github/workflows/ui-tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| name: UI | ||||
| 
 | ||||
| on: | ||||
|   pull_request: | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: ubuntu-18.04 | ||||
| 
 | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
| 
 | ||||
|     name: UI Tests (Cypress) | ||||
| 
 | ||||
|     services: | ||||
|       mysql: | ||||
|         image: mariadb:10.3 | ||||
|         env: | ||||
|           MYSQL_ALLOW_EMPTY_PASSWORD: YES | ||||
|         ports: | ||||
|           - 3306:3306 | ||||
|         options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 | ||||
| 
 | ||||
|     steps: | ||||
|       - name: Clone | ||||
|         uses: actions/checkout@v2 | ||||
| 
 | ||||
|       - name: Setup Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: 3.7 | ||||
| 
 | ||||
|       - uses: actions/setup-node@v2 | ||||
|         with: | ||||
|           node-version: 14 | ||||
|           check-latest: true | ||||
| 
 | ||||
|       - name: Add to Hosts | ||||
|         run: | | ||||
|           echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts | ||||
| 
 | ||||
|       - name: Cache pip | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-pip- | ||||
|             ${{ runner.os }}- | ||||
| 
 | ||||
|       - name: Cache node modules | ||||
|         uses: actions/cache@v2 | ||||
|         env: | ||||
|           cache-name: cache-node-modules | ||||
|         with: | ||||
|           path: ~/.npm | ||||
|           key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-build-${{ env.cache-name }}- | ||||
|             ${{ runner.os }}-build- | ||||
|             ${{ runner.os }}- | ||||
| 
 | ||||
|       - name: Get yarn cache directory path | ||||
|         id: yarn-cache-dir-path | ||||
|         run: echo "::set-output name=dir::$(yarn cache dir)" | ||||
| 
 | ||||
|       - uses: actions/cache@v2 | ||||
|         id: yarn-cache | ||||
|         with: | ||||
|           path: ${{ steps.yarn-cache-dir-path.outputs.dir }} | ||||
|           key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-yarn- | ||||
| 
 | ||||
|       - name: Cache cypress binary | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: ~/.cache | ||||
|           key: ${{ runner.os }}-cypress- | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-cypress- | ||||
|             ${{ runner.os }}- | ||||
| 
 | ||||
|       - name: Install | ||||
|         run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh | ||||
|         env: | ||||
|           DB: mariadb | ||||
|           TYPE: ui | ||||
| 
 | ||||
|       - name: Site Setup | ||||
|         run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests | ||||
| 
 | ||||
|       - name: cypress pre-requisites | ||||
|         run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile | ||||
| 
 | ||||
| 
 | ||||
|       - name: Build Assets | ||||
|         run: cd ~/frappe-bench/ && bench build | ||||
| 
 | ||||
|       - name: UI Tests | ||||
|         run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless | ||||
|         env: | ||||
|           CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} | ||||
| 
 | ||||
|       - name: Show bench console if tests failed | ||||
|         if: ${{ failure() }} | ||||
|         run: cat ~/frappe-bench/bench_run_logs.txt | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -16,3 +16,4 @@ __pycache__ | ||||
| .idea/ | ||||
| .vscode/ | ||||
| node_modules/ | ||||
| .backportrc.json | ||||
							
								
								
									
										11
									
								
								cypress.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								cypress.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| { | ||||
|   "baseUrl": "http://test_site:8000/", | ||||
|   "projectId": "da59y9", | ||||
|   "adminPassword": "admin", | ||||
|   "defaultCommandTimeout": 20000, | ||||
|   "pageLoadTimeout": 15000, | ||||
|   "retries": { | ||||
|     "runMode": 2, | ||||
|     "openMode": 2 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										5
									
								
								cypress/fixtures/example.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								cypress/fixtures/example.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| { | ||||
|   "name": "Using fixtures to represent data", | ||||
|   "email": "hello@cypress.io", | ||||
|   "body": "Fixtures are a great way to mock data for responses to routes" | ||||
| } | ||||
							
								
								
									
										13
									
								
								cypress/integration/test_customer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								cypress/integration/test_customer.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| 
 | ||||
| context('Customer', () => { | ||||
| 	before(() => { | ||||
| 		cy.login(); | ||||
| 	}); | ||||
| 	it('Check Customer Group', () => { | ||||
| 		cy.visit(`app/customer/`); | ||||
| 		cy.get('.primary-action').click(); | ||||
| 		cy.wait(500); | ||||
| 		cy.get('.custom-actions > .btn').click(); | ||||
| 		cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups'); | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										44
									
								
								cypress/integration/test_item.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								cypress/integration/test_item.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| describe("Test Item Dashboard", () => { | ||||
| 	before(() => { | ||||
| 		cy.login(); | ||||
| 		cy.visit("/app/item"); | ||||
| 		cy.insert_doc( | ||||
| 			"Item", | ||||
| 			{ | ||||
| 				item_code: "e2e_test_item", | ||||
| 				item_group: "All Item Groups", | ||||
| 				opening_stock: 42, | ||||
| 				valuation_rate: 100, | ||||
| 			}, | ||||
| 			true | ||||
| 		); | ||||
| 		cy.go_to_doc("item", "e2e_test_item"); | ||||
| 	}); | ||||
| 
 | ||||
| 	it("should show dashboard with correct data on first load", () => { | ||||
| 		cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); | ||||
| 		cy.get(".stock-levels").contains("e2e_test_item").should("exist"); | ||||
| 
 | ||||
| 		// reserved and available qty
 | ||||
| 		cy.get(".stock-levels .inline-graph-count") | ||||
| 			.eq(0) | ||||
| 			.contains("0") | ||||
| 			.should("exist"); | ||||
| 		cy.get(".stock-levels .inline-graph-count") | ||||
| 			.eq(1) | ||||
| 			.contains("42") | ||||
| 			.should("exist"); | ||||
| 	}); | ||||
| 
 | ||||
| 	it("should persist on field change", () => { | ||||
| 		cy.get('input[data-fieldname="disabled"]').check(); | ||||
| 		cy.wait(500); | ||||
| 		cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); | ||||
| 		cy.get(".stock-levels").should("have.length", 1); | ||||
| 	}); | ||||
| 
 | ||||
| 	it("should persist on reload", () => { | ||||
| 		cy.reload(); | ||||
| 		cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										17
									
								
								cypress/plugins/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cypress/plugins/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| // ***********************************************************
 | ||||
| // This example plugins/index.js can be used to load plugins
 | ||||
| //
 | ||||
| // You can change the location of this file or turn off loading
 | ||||
| // the plugins file with the 'pluginsFile' configuration option.
 | ||||
| //
 | ||||
| // You can read more here:
 | ||||
| // https://on.cypress.io/plugins-guide
 | ||||
| // ***********************************************************
 | ||||
| 
 | ||||
| // This function is called when a project is opened or re-opened (e.g. due to
 | ||||
| // the project's config changing)
 | ||||
| 
 | ||||
| module.exports = () => { | ||||
| 	// `on` is used to hook into various events Cypress emits
 | ||||
| 	// `config` is the resolved Cypress config
 | ||||
| }; | ||||
							
								
								
									
										31
									
								
								cypress/support/commands.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								cypress/support/commands.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| // ***********************************************
 | ||||
| // This example commands.js shows you how to
 | ||||
| // create various custom commands and overwrite
 | ||||
| // existing commands.
 | ||||
| //
 | ||||
| // For more comprehensive examples of custom
 | ||||
| // commands please read more here:
 | ||||
| // https://on.cypress.io/custom-commands
 | ||||
| // ***********************************************
 | ||||
| //
 | ||||
| //
 | ||||
| // -- This is a parent command --
 | ||||
| // Cypress.Commands.add("login", (email, password) => { ... });
 | ||||
| //
 | ||||
| //
 | ||||
| // -- This is a child command --
 | ||||
| // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... });
 | ||||
| //
 | ||||
| //
 | ||||
| // -- This is a dual command --
 | ||||
| // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... });
 | ||||
| //
 | ||||
| //
 | ||||
| // -- This is will overwrite an existing command --
 | ||||
| // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
 | ||||
| 
 | ||||
| const slug = (name) => name.toLowerCase().replace(" ", "-"); | ||||
| 
 | ||||
| Cypress.Commands.add("go_to_doc", (doctype, name) => { | ||||
| 	cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`); | ||||
| }); | ||||
							
								
								
									
										26
									
								
								cypress/support/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								cypress/support/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| // ***********************************************************
 | ||||
| // This example support/index.js is processed and
 | ||||
| // loaded automatically before your test files.
 | ||||
| //
 | ||||
| // This is a great place to put global configuration and
 | ||||
| // behavior that modifies Cypress.
 | ||||
| //
 | ||||
| // You can change the location of this file or turn off
 | ||||
| // automatically serving support files with the
 | ||||
| // 'supportFile' configuration option.
 | ||||
| //
 | ||||
| // You can read more here:
 | ||||
| // https://on.cypress.io/configuration
 | ||||
| // ***********************************************************
 | ||||
| 
 | ||||
| // Import commands.js using ES2015 syntax:
 | ||||
| import './commands'; | ||||
| import '../../../frappe/cypress/support/commands' // eslint-disable-line
 | ||||
| 
 | ||||
| 
 | ||||
| // Alternatively you can use CommonJS syntax:
 | ||||
| // require('./commands')
 | ||||
| 
 | ||||
| Cypress.Cookies.defaults({ | ||||
| 	preserve: 'sid' | ||||
| }); | ||||
							
								
								
									
										12
									
								
								cypress/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								cypress/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| { | ||||
|     "compilerOptions": { | ||||
|         "allowJs": true, | ||||
|         "baseUrl": "../node_modules", | ||||
|         "types": [ | ||||
|             "cypress" | ||||
|         ] | ||||
|     }, | ||||
|     "include": [ | ||||
|         "**/*.*" | ||||
|     ] | ||||
| } | ||||
| @ -5,7 +5,7 @@ import frappe | ||||
| from erpnext.hooks import regional_overrides | ||||
| from frappe.utils import getdate | ||||
| 
 | ||||
| __version__ = '13.5.2' | ||||
| __version__ = '13.6.0' | ||||
| 
 | ||||
| def get_default_company(user=None): | ||||
| 	'''Get default company for user''' | ||||
|  | ||||
| @ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None): | ||||
| 	start_date = add_months(today(), -1) | ||||
| 	end_date = add_days(today(), -1) | ||||
| 
 | ||||
| 	for record_type in ('Income', 'Expense'): | ||||
| 		doc = frappe.get_doc(dict( | ||||
| 			doctype='Process Deferred Accounting', | ||||
| 			posting_date=posting_date, | ||||
| 			start_date=start_date, | ||||
| 			end_date=end_date, | ||||
| 			type=record_type | ||||
| 		)) | ||||
| 	companies = frappe.get_all('Company') | ||||
| 
 | ||||
| 		doc.insert() | ||||
| 		doc.submit() | ||||
| 	for company in companies: | ||||
| 		for record_type in ('Income', 'Expense'): | ||||
| 			doc = frappe.get_doc(dict( | ||||
| 				doctype='Process Deferred Accounting', | ||||
| 				company=company.name, | ||||
| 				posting_date=posting_date, | ||||
| 				start_date=start_date, | ||||
| 				end_date=end_date, | ||||
| 				type=record_type | ||||
| 			)) | ||||
| 
 | ||||
| 			doc.insert() | ||||
| 			doc.submit() | ||||
| 
 | ||||
| def make_gl_entries(doc, credit_account, debit_account, against, | ||||
| 	amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): | ||||
|  | ||||
| @ -19,7 +19,7 @@ class AccountingDimension(Document): | ||||
| 
 | ||||
| 	def validate(self): | ||||
| 		if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', | ||||
| 				'Cost Center', 'Accounting Dimension Detail', 'Company') : | ||||
| 				'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') : | ||||
| 
 | ||||
| 			msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) | ||||
| 			frappe.throw(msg) | ||||
|  | ||||
| @ -51,7 +51,7 @@ class BankStatementImport(DataImport): | ||||
| 			self.import_file, self.google_sheets_url | ||||
| 		) | ||||
| 
 | ||||
| 		if 'Bank Account' not in json.dumps(preview): | ||||
| 		if 'Bank Account' not in json.dumps(preview['columns']): | ||||
| 			frappe.throw(_("Please add the Bank Account column")) | ||||
| 
 | ||||
| 		from frappe.core.page.background_jobs.background_jobs import get_info | ||||
|  | ||||
| @ -96,7 +96,7 @@ def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_inte | ||||
| 	grand_total = 0 | ||||
| 	if rate_of_interest: | ||||
| 		interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 | ||||
| 		interest_amount = (interest_per_year * cint(overdue_days)) / 365  | ||||
| 		interest_amount = (interest_per_year * cint(overdue_days)) / 365 | ||||
| 		grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) | ||||
| 	dunning_amount = flt(interest_amount) + flt(dunning_fee) | ||||
| 	return { | ||||
|  | ||||
| @ -121,8 +121,7 @@ class GLEntry(Document): | ||||
| 
 | ||||
| 	def check_pl_account(self): | ||||
| 		if self.is_opening=='Yes' and \ | ||||
| 				frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \ | ||||
| 				self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']: | ||||
| 				frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss": | ||||
| 			frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") | ||||
| 				.format(self.voucher_type, self.voucher_no, self.account)) | ||||
| 
 | ||||
|  | ||||
| @ -1318,9 +1318,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre | ||||
| 
 | ||||
| 	return frappe._dict({ | ||||
| 		"due_date": ref_doc.get("due_date"), | ||||
| 		"total_amount": total_amount, | ||||
| 		"outstanding_amount": outstanding_amount, | ||||
| 		"exchange_rate": exchange_rate, | ||||
| 		"total_amount": flt(total_amount), | ||||
| 		"outstanding_amount": flt(outstanding_amount), | ||||
| 		"exchange_rate": flt(exchange_rate), | ||||
| 		"bill_no": bill_no | ||||
| 	}) | ||||
| 
 | ||||
|  | ||||
| @ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase): | ||||
| 		party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center) | ||||
| 
 | ||||
| 		self.assertEqual(pe.cost_center, si.cost_center) | ||||
| 		self.assertEqual(expected_account_balance, account_balance) | ||||
| 		self.assertEqual(expected_party_balance, party_balance) | ||||
| 		self.assertEqual(expected_party_account_balance, party_account_balance) | ||||
| 		self.assertEqual(flt(expected_account_balance), account_balance) | ||||
| 		self.assertEqual(flt(expected_party_balance), party_balance) | ||||
| 		self.assertEqual(flt(expected_party_account_balance), party_account_balance) | ||||
| 
 | ||||
| def create_payment_terms_template(): | ||||
| 
 | ||||
|  | ||||
| @ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): | ||||
| @frappe.whitelist() | ||||
| def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): | ||||
| 	billing_email = frappe.db.sql(""" | ||||
| 		SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \ | ||||
| 		WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \ | ||||
| 		c.is_billing_contact=1 \ | ||||
| 		order by c.creation desc""") | ||||
| 		SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent | ||||
| 		WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1 | ||||
| 		order by c.creation desc""", customer_name) | ||||
| 
 | ||||
| 	if len(billing_email) == 0 or (billing_email[0][0] is None): | ||||
| 		if billing_and_primary: | ||||
|  | ||||
| @ -517,6 +517,8 @@ class PurchaseInvoice(BuyingController): | ||||
| 			if d.category in ('Valuation', 'Total and Valuation') | ||||
| 			and flt(d.base_tax_amount_after_discount_amount)] | ||||
| 
 | ||||
| 		exchange_rate_map, net_rate_map = get_purchase_document_details(self) | ||||
| 
 | ||||
| 		for item in self.get("items"): | ||||
| 			if flt(item.base_net_amount): | ||||
| 				account_currency = get_account_currency(item.expense_account) | ||||
| @ -634,6 +636,34 @@ class PurchaseInvoice(BuyingController): | ||||
| 								"project": item.project or self.project | ||||
| 							}, account_currency, item=item)) | ||||
| 
 | ||||
| 						# check if the exchange rate has changed | ||||
| 						if item.get('purchase_receipt'): | ||||
| 							if exchange_rate_map[item.purchase_receipt] and \ | ||||
| 								self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \ | ||||
| 								item.net_rate == net_rate_map[item.pr_detail]: | ||||
| 
 | ||||
| 								discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ | ||||
| 									(exchange_rate_map[item.purchase_receipt] - self.conversion_rate) | ||||
| 
 | ||||
| 								gl_entries.append( | ||||
| 									self.get_gl_dict({ | ||||
| 										"account": expense_account, | ||||
| 										"against": self.supplier, | ||||
| 										"debit": discrepancy_caused_by_exchange_rate_difference, | ||||
| 										"cost_center": item.cost_center, | ||||
| 										"project": item.project or self.project | ||||
| 									}, account_currency, item=item) | ||||
| 								) | ||||
| 								gl_entries.append( | ||||
| 									self.get_gl_dict({ | ||||
| 										"account": self.get_company_default("exchange_gain_loss_account"),		 | ||||
| 										"against": self.supplier, | ||||
| 										"credit": discrepancy_caused_by_exchange_rate_difference, | ||||
| 										"cost_center": item.cost_center, | ||||
| 										"project": item.project or self.project | ||||
| 									}, account_currency, item=item) | ||||
| 								) | ||||
| 
 | ||||
| 					# If asset is bought through this document and not linked to PR | ||||
| 					if self.update_stock and item.landed_cost_voucher_amount: | ||||
| 						expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") | ||||
| @ -1141,6 +1171,36 @@ class PurchaseInvoice(BuyingController): | ||||
| 		if update: | ||||
| 			self.db_set('status', self.status, update_modified = update_modified) | ||||
| 
 | ||||
| # to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling | ||||
| def get_purchase_document_details(doc): | ||||
| 	if doc.doctype == 'Purchase Invoice': | ||||
| 		doc_reference = 'purchase_receipt' | ||||
| 		items_reference = 'pr_detail' | ||||
| 		parent_doctype = 'Purchase Receipt' | ||||
| 		child_doctype = 'Purchase Receipt Item' | ||||
| 	else: | ||||
| 		doc_reference = 'purchase_invoice' | ||||
| 		items_reference = 'purchase_invoice_item' | ||||
| 		parent_doctype = 'Purchase Invoice' | ||||
| 		child_doctype = 'Purchase Invoice Item' | ||||
| 
 | ||||
| 	purchase_receipts_or_invoices = [] | ||||
| 	items = [] | ||||
| 
 | ||||
| 	for item in doc.get('items'): | ||||
| 		if item.get(doc_reference): | ||||
| 			purchase_receipts_or_invoices.append(item.get(doc_reference)) | ||||
| 		if item.get(items_reference): | ||||
| 			items.append(item.get(items_reference)) | ||||
| 	 | ||||
| 	exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in', | ||||
| 		purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) | ||||
| 
 | ||||
| 	net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in', | ||||
| 		items)}, fields=['name', 'net_rate'], as_list=1)) | ||||
| 
 | ||||
| 	return exchange_rate_map, net_rate_map | ||||
| 
 | ||||
| def get_list_context(context=None): | ||||
| 	from erpnext.controllers.website_list_for_contact import get_list_context | ||||
| 	list_context = get_list_context(context) | ||||
|  | ||||
| @ -230,6 +230,27 @@ class TestPurchaseInvoice(unittest.TestCase): | ||||
| 			self.assertEqual(expected_values[gle.account][1], gle.debit) | ||||
| 			self.assertEqual(expected_values[gle.account][2], gle.credit) | ||||
| 
 | ||||
| 	def test_purchase_invoice_with_exchange_rate_difference(self): | ||||
| 		from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice | ||||
| 
 | ||||
| 		pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', | ||||
| 			currency = "USD", conversion_rate = 70) | ||||
| 
 | ||||
| 		pi = create_purchase_invoice(pr.name) | ||||
| 		pi.conversion_rate = 80 | ||||
| 
 | ||||
| 		pi.insert() | ||||
| 		pi.submit()		 | ||||
| 
 | ||||
| 		# Get exchnage gain and loss account | ||||
| 		exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account') | ||||
| 
 | ||||
| 		# fetching the latest GL Entry with exchange gain and loss account account | ||||
| 		amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pi.name}, 'debit') | ||||
| 		discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) | ||||
| 
 | ||||
| 		self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) | ||||
| 
 | ||||
| 	def test_purchase_invoice_change_naming_series(self): | ||||
| 		pi = frappe.copy_doc(test_records[1]) | ||||
| 		pi.insert() | ||||
| @ -1010,21 +1031,21 @@ class TestPurchaseInvoice(unittest.TestCase): | ||||
| 		# Check GLE for Purchase Invoice | ||||
| 		# Zero net effect on final TDS Payable on invoice | ||||
| 		expected_gle = [ | ||||
| 			['_Test Account Cost for Goods Sold - _TC', 30000, 0], | ||||
| 			['_Test Account Excise Duty - _TC', 0, 3000], | ||||
| 			['Creditors - _TC', 0, 27000], | ||||
| 			['TDS Payable - _TC', 3000, 3000] | ||||
| 			['_Test Account Cost for Goods Sold - _TC', 30000], | ||||
| 			['_Test Account Excise Duty - _TC', -3000], | ||||
| 			['Creditors - _TC', -27000], | ||||
| 			['TDS Payable - _TC', 0] | ||||
| 		] | ||||
| 
 | ||||
| 		gl_entries = frappe.db.sql("""select account, debit, credit | ||||
| 		gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount | ||||
| 			from `tabGL Entry` | ||||
| 			where voucher_type='Purchase Invoice' and voucher_no=%s | ||||
| 			group by account | ||||
| 			order by account asc""", (purchase_invoice.name), as_dict=1) | ||||
| 
 | ||||
| 		for i, gle in enumerate(gl_entries): | ||||
| 			self.assertEqual(expected_gle[i][0], gle.account) | ||||
| 			self.assertEqual(expected_gle[i][1], gle.debit) | ||||
| 			self.assertEqual(expected_gle[i][2], gle.credit) | ||||
| 			self.assertEqual(expected_gle[i][1], gle.amount) | ||||
| 
 | ||||
| def update_tax_witholding_category(company, account, date): | ||||
| 	from erpnext.accounts.utils import get_fiscal_year | ||||
|  | ||||
| @ -854,7 +854,7 @@ | ||||
|  "idx": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-06-16 19:33:51.099386", | ||||
|  "modified": "2021-06-16 19:43:51.099386", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Purchase Invoice Item", | ||||
|  | ||||
| @ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): | ||||
| 		select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) | ||||
| 		from `tabGL Entry` | ||||
| 		where party_type = %s and party=%s | ||||
| 		and is_cancelled = 0 | ||||
| 		group by company""", (party_type, party))) | ||||
| 
 | ||||
| 	for d in companies: | ||||
|  | ||||
| @ -397,6 +397,7 @@ def get_chart_data(filters, columns, data): | ||||
| 				{'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, | ||||
| 				{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} | ||||
| 			] | ||||
| 		} | ||||
| 		}, | ||||
| 		'type' : 'bar' | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g | ||||
| 		gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, | ||||
| 			gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, | ||||
| 			acc.account_name, acc.account_number | ||||
| 			from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s | ||||
| 			from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0 | ||||
| 			{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s | ||||
| 			order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions), | ||||
| 			{ | ||||
|  | ||||
| @ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = { | ||||
| 		{ | ||||
| 			"fieldname":"account", | ||||
| 			"label": __("Account"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"fieldtype": "MultiSelectList", | ||||
| 			"options": "Account", | ||||
| 			"get_query": function() { | ||||
| 				var company = frappe.query_report.get_filter_value('company'); | ||||
| 				return { | ||||
| 					"doctype": "Account", | ||||
| 					"filters": { | ||||
| 						"company": company, | ||||
| 					} | ||||
| 				} | ||||
| 			get_data: function(txt) { | ||||
| 				return frappe.db.get_link_options('Account', txt, { | ||||
| 					company: frappe.query_report.get_filter_value("company") | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| @ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = { | ||||
| 			"label": __("Cost Center"), | ||||
| 			"fieldtype": "MultiSelectList", | ||||
| 			get_data: function(txt) { | ||||
| 				return frappe.db.get_link_options('Cost Center', txt); | ||||
| 				return frappe.db.get_link_options('Cost Center', txt, { | ||||
| 					company: frappe.query_report.get_filter_value("company") | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| @ -143,7 +141,9 @@ frappe.query_reports["General Ledger"] = { | ||||
| 			"label": __("Project"), | ||||
| 			"fieldtype": "MultiSelectList", | ||||
| 			get_data: function(txt) { | ||||
| 				return frappe.db.get_link_options('Project', txt); | ||||
| 				return frappe.db.get_link_options('Project', txt, { | ||||
| 					company: frappe.query_report.get_filter_value("company") | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
|  | ||||
| @ -49,8 +49,12 @@ def validate_filters(filters, account_details): | ||||
| 	if not filters.get("from_date") and not filters.get("to_date"): | ||||
| 		frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) | ||||
| 
 | ||||
| 	if filters.get("account") and not account_details.get(filters.account): | ||||
| 		frappe.throw(_("Account {0} does not exists").format(filters.account)) | ||||
| 	for account in filters.account: | ||||
| 		if not account_details.get(account): | ||||
| 			frappe.throw(_("Account {0} does not exists").format(account)) | ||||
| 			 | ||||
| 	if filters.get('account'): | ||||
| 		filters.account = frappe.parse_json(filters.get('account')) | ||||
| 
 | ||||
| 	if (filters.get("account") and filters.get("group_by") == _('Group by Account') | ||||
| 		and account_details[filters.account].is_group == 0): | ||||
| @ -87,7 +91,19 @@ def set_account_currency(filters): | ||||
| 		account_currency = None | ||||
| 
 | ||||
| 		if filters.get("account"): | ||||
| 			account_currency = get_account_currency(filters.account) | ||||
| 			if len(filters.get("account")) == 1:	 | ||||
| 				account_currency = get_account_currency(filters.account[0]) | ||||
| 			else: | ||||
| 				currency = get_account_currency(filters.account[0]) | ||||
| 				is_same_account_currency = True | ||||
| 				for account in filters.get("account"): | ||||
| 					if get_account_currency(account) != currency: | ||||
| 						is_same_account_currency = False | ||||
| 						break | ||||
| 
 | ||||
| 				if is_same_account_currency: | ||||
| 					account_currency = currency | ||||
| 
 | ||||
| 		elif filters.get("party"): | ||||
| 			gle_currency = frappe.db.get_value( | ||||
| 				"GL Entry", { | ||||
| @ -205,10 +221,10 @@ def get_gl_entries(filters, accounting_dimensions): | ||||
| 
 | ||||
| def get_conditions(filters): | ||||
| 	conditions = [] | ||||
| 	if filters.get("account") and not filters.get("include_dimensions"): | ||||
| 		lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) | ||||
| 		conditions.append("""account in (select name from tabAccount | ||||
| 			where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) | ||||
| 
 | ||||
| 	if filters.get("account"): | ||||
| 		filters.account = get_accounts_with_children(filters.account) | ||||
| 		conditions.append("account in %(account)s") | ||||
| 
 | ||||
| 	if filters.get("cost_center"): | ||||
| 		filters.cost_center = get_cost_centers_with_children(filters.cost_center) | ||||
| @ -266,6 +282,20 @@ def get_conditions(filters): | ||||
| 
 | ||||
| 	return "and {}".format(" and ".join(conditions)) if conditions else "" | ||||
| 
 | ||||
| def get_accounts_with_children(accounts): | ||||
| 	if not isinstance(accounts, list): | ||||
| 		accounts = [d.strip() for d in accounts.strip().split(',') if d] | ||||
| 
 | ||||
| 	all_accounts = [] | ||||
| 	for d in accounts: | ||||
| 		if frappe.db.exists("Account", d): | ||||
| 			lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"]) | ||||
| 			children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]}) | ||||
| 			all_accounts += [c.name for c in children] | ||||
| 		else: | ||||
| 			frappe.throw(_("Account: {0} does not exist").format(d)) | ||||
| 
 | ||||
| 	return list(set(all_accounts)) | ||||
| 
 | ||||
| def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): | ||||
| 	data = [] | ||||
|  | ||||
| @ -82,24 +82,46 @@ frappe.ui.form.on('Asset', { | ||||
| 			if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { | ||||
| 				frm.add_custom_button("Transfer Asset", function() { | ||||
| 					erpnext.asset.transfer_asset(frm); | ||||
| 				}); | ||||
| 				}, __("Manage")); | ||||
| 
 | ||||
| 				frm.add_custom_button("Scrap Asset", function() { | ||||
| 					erpnext.asset.scrap_asset(frm); | ||||
| 				}); | ||||
| 				}, __("Manage")); | ||||
| 
 | ||||
| 				frm.add_custom_button("Sell Asset", function() { | ||||
| 					frm.trigger("make_sales_invoice"); | ||||
| 				}); | ||||
| 				}, __("Manage")); | ||||
| 
 | ||||
| 			} else if (frm.doc.status=='Scrapped') { | ||||
| 				frm.add_custom_button("Restore Asset", function() { | ||||
| 					erpnext.asset.restore_asset(frm); | ||||
| 				}); | ||||
| 				}, __("Manage")); | ||||
| 			} | ||||
| 
 | ||||
| 			if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { | ||||
| 				frm.add_custom_button(__("Maintain Asset"), function() { | ||||
| 					frm.trigger("create_asset_maintenance"); | ||||
| 				}, __("Manage")); | ||||
| 			} | ||||
| 		 | ||||
| 			frm.add_custom_button(__("Repair Asset"), function() { | ||||
| 				frm.trigger("create_asset_repair"); | ||||
| 			}, __("Manage")); | ||||
| 			 | ||||
| 			if (frm.doc.status != 'Fully Depreciated') { | ||||
| 				frm.add_custom_button(__("Adjust Asset Value"), function() { | ||||
| 					frm.trigger("create_asset_adjustment"); | ||||
| 				}, __("Manage")); | ||||
| 			} | ||||
| 
 | ||||
| 			if (!frm.doc.calculate_depreciation) { | ||||
| 				frm.add_custom_button(__("Create Depreciation Entry"), function() { | ||||
| 					frm.trigger("make_journal_entry"); | ||||
| 				}, __("Manage")); | ||||
| 			} | ||||
| 
 | ||||
| 			if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { | ||||
| 				frm.add_custom_button("General Ledger", function() { | ||||
| 				frm.add_custom_button("View General Ledger", function() { | ||||
| 					frappe.route_options = { | ||||
| 						"voucher_no": frm.doc.name, | ||||
| 						"from_date": frm.doc.available_for_use_date, | ||||
| @ -107,27 +129,9 @@ frappe.ui.form.on('Asset', { | ||||
| 						"company": frm.doc.company | ||||
| 					}; | ||||
| 					frappe.set_route("query-report", "General Ledger"); | ||||
| 				}); | ||||
| 				}, __("Manage")); | ||||
| 			} | ||||
| 
 | ||||
| 			if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { | ||||
| 				frm.add_custom_button(__("Asset Maintenance"), function() { | ||||
| 					frm.trigger("create_asset_maintenance"); | ||||
| 				}, __('Create')); | ||||
| 			} | ||||
| 			if (frm.doc.status != 'Fully Depreciated') { | ||||
| 				frm.add_custom_button(__("Asset Value Adjustment"), function() { | ||||
| 					frm.trigger("create_asset_adjustment"); | ||||
| 				}, __('Create')); | ||||
| 			} | ||||
| 
 | ||||
| 			if (!frm.doc.calculate_depreciation) { | ||||
| 				frm.add_custom_button(__("Depreciation Entry"), function() { | ||||
| 					frm.trigger("make_journal_entry"); | ||||
| 				}, __('Create')); | ||||
| 			} | ||||
| 
 | ||||
| 			frm.page.set_inner_btn_group_as_primary(__('Create')); | ||||
| 			frm.trigger("setup_chart"); | ||||
| 		} | ||||
| 
 | ||||
| @ -304,6 +308,20 @@ frappe.ui.form.on('Asset', { | ||||
| 		}) | ||||
| 	}, | ||||
| 
 | ||||
| 	create_asset_repair: function(frm) { | ||||
| 		frappe.call({ | ||||
| 			args: { | ||||
| 				"asset": frm.doc.name, | ||||
| 				"asset_name": frm.doc.asset_name | ||||
| 			}, | ||||
| 			method: "erpnext.assets.doctype.asset.asset.create_asset_repair", | ||||
| 			callback: function(r) { | ||||
| 				var doclist = frappe.model.sync(r.message); | ||||
| 				frappe.set_route("Form", doclist[0].doctype, doclist[0].name); | ||||
| 			} | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	create_asset_adjustment: function(frm) { | ||||
| 		frappe.call({ | ||||
| 			args: { | ||||
|  | ||||
| @ -502,7 +502,7 @@ | ||||
|    "link_fieldname": "asset" | ||||
|   } | ||||
|  ], | ||||
|  "modified": "2021-01-22 12:38:59.091510", | ||||
|  "modified": "2021-06-24 14:58:51.097908", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Assets", | ||||
|  "name": "Asset", | ||||
|  | ||||
| @ -168,17 +168,24 @@ class Asset(AccountsController): | ||||
| 				d.precision("rate_of_depreciation")) | ||||
| 
 | ||||
| 	def make_depreciation_schedule(self): | ||||
| 		if 'Manual' not in [d.depreciation_method for d in self.finance_books]: | ||||
| 		if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules: | ||||
| 			self.schedules = [] | ||||
| 
 | ||||
| 		if self.get("schedules") or not self.available_for_use_date: | ||||
| 		if not self.available_for_use_date: | ||||
| 			return | ||||
| 
 | ||||
| 		for d in self.get('finance_books'): | ||||
| 			self.validate_asset_finance_books(d) | ||||
| 			 | ||||
| 			start = self.clear_depreciation_schedule() | ||||
| 
 | ||||
| 			value_after_depreciation = (flt(self.gross_purchase_amount) - | ||||
| 				flt(self.opening_accumulated_depreciation)) | ||||
| 			# value_after_depreciation - current Asset value | ||||
| 			if d.value_after_depreciation: | ||||
| 				value_after_depreciation = (flt(d.value_after_depreciation) - | ||||
| 					flt(self.opening_accumulated_depreciation))  | ||||
| 			else: | ||||
| 				value_after_depreciation = (flt(self.gross_purchase_amount) - | ||||
| 					flt(self.opening_accumulated_depreciation))  | ||||
| 
 | ||||
| 			d.value_after_depreciation = value_after_depreciation | ||||
| 
 | ||||
| @ -191,7 +198,7 @@ class Asset(AccountsController): | ||||
| 				number_of_pending_depreciations += 1 | ||||
| 
 | ||||
| 			skip_row = False | ||||
| 			for n in range(number_of_pending_depreciations): | ||||
| 			for n in range(start, number_of_pending_depreciations): | ||||
| 				# If depreciation is already completed (for double declining balance) | ||||
| 				if skip_row: continue | ||||
| 
 | ||||
| @ -216,11 +223,13 @@ class Asset(AccountsController): | ||||
| 
 | ||||
| 				# For last row | ||||
| 				elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: | ||||
| 					to_date = add_months(self.available_for_use_date, | ||||
| 						n * cint(d.frequency_of_depreciation)) | ||||
| 					if not self.flags.increase_in_asset_life: | ||||
| 						# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission | ||||
| 						self.to_date = add_months(self.available_for_use_date, | ||||
| 							n * cint(d.frequency_of_depreciation)) | ||||
| 
 | ||||
| 					depreciation_amount, days, months = self.get_pro_rata_amt(d, | ||||
| 						depreciation_amount, schedule_date, to_date) | ||||
| 						depreciation_amount, schedule_date, self.to_date) | ||||
| 
 | ||||
| 					monthly_schedule_date = add_months(schedule_date, 1) | ||||
| 
 | ||||
| @ -284,10 +293,23 @@ class Asset(AccountsController): | ||||
| 							"finance_book_id": d.idx | ||||
| 						}) | ||||
| 
 | ||||
| 	# used when depreciation schedule needs to be modified due to increase in asset life | ||||
| 	def clear_depreciation_schedule(self): | ||||
| 		start = 0 | ||||
| 		for n in range(len(self.schedules)): | ||||
| 			if not self.schedules[n].journal_entry: | ||||
| 				del self.schedules[n:] | ||||
| 				start = n | ||||
| 				break | ||||
| 		return start | ||||
| 
 | ||||
| 
 | ||||
| 	# if it returns True, depreciation_amount will not be equal for the first and last rows | ||||
| 	def check_is_pro_rata(self, row): | ||||
| 		has_pro_rata = False | ||||
| 
 | ||||
| 		days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 | ||||
| 
 | ||||
| 		# if frequency_of_depreciation is 12 months, total_days = 365 | ||||
| 		total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) | ||||
| 
 | ||||
| 		if days < total_days: | ||||
| @ -346,11 +368,12 @@ class Asset(AccountsController): | ||||
| 			if d.finance_book_id not in finance_books: | ||||
| 				accumulated_depreciation = flt(self.opening_accumulated_depreciation) | ||||
| 				value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id)) | ||||
| 				finance_books.append(d.finance_book_id) | ||||
| 				finance_books.append(int(d.finance_book_id)) | ||||
| 
 | ||||
| 			depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) | ||||
| 			value_after_depreciation -= flt(depreciation_amount) | ||||
| 
 | ||||
| 			# for the last row, if depreciation method = Straight Line | ||||
| 			if straight_line_idx and i == max(straight_line_idx) - 1: | ||||
| 				book = self.get('finance_books')[cint(d.finance_book_id) - 1] | ||||
| 				depreciation_amount += flt(value_after_depreciation - | ||||
| @ -625,9 +648,18 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan | ||||
| 	}) | ||||
| 	return asset_maintenance | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def create_asset_repair(asset, asset_name): | ||||
| 	asset_repair = frappe.new_doc("Asset Repair") | ||||
| 	asset_repair.update({ | ||||
| 		"asset": asset, | ||||
| 		"asset_name": asset_name | ||||
| 	}) | ||||
| 	return asset_repair | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def create_asset_adjustment(asset, asset_category, company): | ||||
| 	asset_maintenance = frappe.new_doc("Asset Value Adjustment") | ||||
| 	asset_maintenance = frappe.get_doc("Asset Value Adjustment") | ||||
| 	asset_maintenance.update({ | ||||
| 		"asset": asset, | ||||
| 		"company": company, | ||||
| @ -757,8 +789,15 @@ def get_depreciation_amount(asset, depreciable_value, row): | ||||
| 	depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) | ||||
| 
 | ||||
| 	if row.depreciation_method in ("Straight Line", "Manual"): | ||||
| 		depreciation_amount = (flt(row.value_after_depreciation) - | ||||
| 			flt(row.expected_value_after_useful_life)) / depreciation_left | ||||
| 		# if the Depreciation Schedule is being prepared for the first time | ||||
| 		if not asset.flags.increase_in_asset_life: | ||||
| 			depreciation_amount = (flt(row.value_after_depreciation) - | ||||
| 				flt(row.expected_value_after_useful_life)) / depreciation_left | ||||
| 
 | ||||
| 		# if the Depreciation Schedule is being modified after Asset Repair | ||||
| 		else: | ||||
| 			depreciation_amount = (flt(row.value_after_depreciation) - | ||||
| 				flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) | ||||
| 	else: | ||||
| 		depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) | ||||
| 
 | ||||
|  | ||||
| @ -125,7 +125,6 @@ class TestAsset(unittest.TestCase): | ||||
| 			"frequency_of_depreciation": 12, | ||||
| 			"depreciation_start_date": "2030-12-31" | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		self.assertEqual(asset.status, "Draft") | ||||
| 		asset.save() | ||||
| 		expected_schedules = [ | ||||
| @ -154,9 +153,8 @@ class TestAsset(unittest.TestCase): | ||||
| 			"frequency_of_depreciation": 12, | ||||
| 			"depreciation_start_date": '2030-12-31' | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		self.assertEqual(asset.status, "Draft") | ||||
| 		asset.save() | ||||
| 		self.assertEqual(asset.status, "Draft") | ||||
| 
 | ||||
| 		expected_schedules = [ | ||||
| 			['2030-12-31', 66667.00, 66667.00], | ||||
| @ -185,7 +183,7 @@ class TestAsset(unittest.TestCase): | ||||
| 			"frequency_of_depreciation": 12, | ||||
| 			"depreciation_start_date": "2030-12-31" | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		asset.save() | ||||
| 		self.assertEqual(asset.status, "Draft") | ||||
| 
 | ||||
| 		expected_schedules = [ | ||||
| @ -216,7 +214,6 @@ class TestAsset(unittest.TestCase): | ||||
| 			"depreciation_start_date": "2030-12-31" | ||||
| 		}) | ||||
| 
 | ||||
| 		asset.insert() | ||||
| 		asset.save() | ||||
| 
 | ||||
| 		expected_schedules = [ | ||||
| @ -247,7 +244,6 @@ class TestAsset(unittest.TestCase): | ||||
| 			"frequency_of_depreciation": 10, | ||||
| 			"depreciation_start_date": "2020-12-31" | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		asset.submit() | ||||
| 		asset.load_from_db() | ||||
| 		self.assertEqual(asset.status, "Submitted") | ||||
| @ -350,7 +346,6 @@ class TestAsset(unittest.TestCase): | ||||
| 			"frequency_of_depreciation": 10, | ||||
| 			"depreciation_start_date": "2020-12-31" | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		asset.submit() | ||||
| 		post_depreciation_entries(date="2021-01-01") | ||||
| 
 | ||||
| @ -380,7 +375,6 @@ class TestAsset(unittest.TestCase): | ||||
| 			"total_number_of_depreciations": 10, | ||||
| 			"frequency_of_depreciation": 1 | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		asset.submit() | ||||
| 
 | ||||
| 		post_depreciation_entries(date=add_months('2020-01-01', 4)) | ||||
| @ -424,7 +418,6 @@ class TestAsset(unittest.TestCase): | ||||
| 			"frequency_of_depreciation": 10, | ||||
| 			"depreciation_start_date": "2020-12-31" | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		asset.submit() | ||||
| 		post_depreciation_entries(date="2021-01-01") | ||||
| 
 | ||||
| @ -468,7 +461,7 @@ class TestAsset(unittest.TestCase): | ||||
| 			"total_number_of_depreciations": 3, | ||||
| 			"frequency_of_depreciation": 10 | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		asset.save() | ||||
| 		accumulated_depreciation_after_full_schedule = \ | ||||
| 			max(d.accumulated_depreciation_amount for d in asset.get("schedules")) | ||||
| 
 | ||||
| @ -699,7 +692,7 @@ def create_asset(**args): | ||||
| 		"item_code": args.item_code or "Macbook Pro", | ||||
| 		"company": args.company or"_Test Company", | ||||
| 		"purchase_date": "2015-01-01", | ||||
| 		"calculate_depreciation": 0, | ||||
| 		"calculate_depreciation": args.calculate_depreciation or 0, | ||||
| 		"gross_purchase_amount": 100000, | ||||
| 		"purchase_receipt_amount": 100000, | ||||
| 		"expected_value_after_useful_life": 10000, | ||||
| @ -707,9 +700,16 @@ def create_asset(**args): | ||||
| 		"available_for_use_date": "2020-06-06", | ||||
| 		"location": "Test Location", | ||||
| 		"asset_owner": "Company", | ||||
| 		"is_existing_asset": args.is_existing_asset or 0 | ||||
| 		"is_existing_asset": 1 | ||||
| 	}) | ||||
| 
 | ||||
| 	if asset.calculate_depreciation: | ||||
| 		asset.append("finance_books", { | ||||
| 			"depreciation_method": "Straight Line", | ||||
| 			"frequency_of_depreciation": 12, | ||||
| 			"total_number_of_depreciations": 5 | ||||
| 		}) | ||||
| 
 | ||||
| 	try: | ||||
| 		asset.save() | ||||
| 	except frappe.DuplicateEntryError: | ||||
|  | ||||
| @ -67,7 +67,6 @@ | ||||
|   { | ||||
|    "fieldname": "value_after_depreciation", | ||||
|    "fieldtype": "Currency", | ||||
|    "hidden": 1, | ||||
|    "label": "Value After Depreciation", | ||||
|    "no_copy": 1, | ||||
|    "options": "Company:company:default_currency", | ||||
| @ -85,7 +84,7 @@ | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-11-05 16:30:09.213479", | ||||
|  "modified": "2021-06-17 12:59:05.743683", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Assets", | ||||
|  "name": "Asset Finance Book", | ||||
|  | ||||
| @ -2,6 +2,45 @@ | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('Asset Repair', { | ||||
| 	setup: function(frm) { | ||||
| 		frm.fields_dict.cost_center.get_query = function(doc) { | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					'is_group': 0, | ||||
| 					'company': doc.company | ||||
| 				} | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		frm.fields_dict.project.get_query = function(doc) { | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					'company': doc.company | ||||
| 				} | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		frm.fields_dict.warehouse.get_query = function(doc) { | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					'is_group': 0, | ||||
| 					'company': doc.company | ||||
| 				} | ||||
| 			}; | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	refresh: function(frm) { | ||||
| 		if (frm.doc.docstatus) { | ||||
| 			frm.add_custom_button("View General Ledger", function() { | ||||
| 				frappe.route_options = { | ||||
| 					"voucher_no": frm.doc.name | ||||
| 				}; | ||||
| 				frappe.set_route("query-report", "General Ledger"); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	repair_status: (frm) => { | ||||
| 		if (frm.doc.completion_date && frm.doc.repair_status == "Completed") { | ||||
| 			frappe.call ({ | ||||
| @ -17,5 +56,16 @@ frappe.ui.form.on('Asset Repair', { | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		if (frm.doc.repair_status == "Completed") { | ||||
| 			frm.set_value('completion_date', frappe.datetime.now_datetime()); | ||||
| 		}				 | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| frappe.ui.form.on('Asset Repair Consumed Item', { | ||||
| 	consumed_quantity: function(frm, cdt, cdn) { | ||||
| 		var row = locals[cdt][cdn]; | ||||
| 		frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); | ||||
| 	}, | ||||
| }); | ||||
| @ -7,38 +7,43 @@ | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "naming_series", | ||||
|   "asset_name", | ||||
|   "asset", | ||||
|   "company", | ||||
|   "column_break_2", | ||||
|   "item_code", | ||||
|   "item_name", | ||||
|   "asset_name", | ||||
|   "naming_series", | ||||
|   "section_break_5", | ||||
|   "failure_date", | ||||
|   "assign_to", | ||||
|   "assign_to_name", | ||||
|   "repair_status", | ||||
|   "column_break_6", | ||||
|   "completion_date", | ||||
|   "repair_status", | ||||
|   "accounting_dimensions_section", | ||||
|   "cost_center", | ||||
|   "column_break_14", | ||||
|   "project", | ||||
|   "accounting_details", | ||||
|   "repair_cost", | ||||
|   "capitalize_repair_cost", | ||||
|   "stock_consumption", | ||||
|   "column_break_8", | ||||
|   "purchase_invoice", | ||||
|   "stock_consumption_details_section", | ||||
|   "warehouse", | ||||
|   "stock_items", | ||||
|   "total_repair_cost", | ||||
|   "stock_entry", | ||||
|   "asset_depreciation_details_section", | ||||
|   "increase_in_asset_life", | ||||
|   "section_break_9", | ||||
|   "description", | ||||
|   "column_break_9", | ||||
|   "actions_performed", | ||||
|   "section_break_17", | ||||
|   "section_break_23", | ||||
|   "downtime", | ||||
|   "column_break_19", | ||||
|   "amended_from" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "columns": 1, | ||||
|    "fieldname": "asset_name", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Asset", | ||||
|    "options": "Asset", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "naming_series", | ||||
|    "fieldtype": "Select", | ||||
| @ -50,18 +55,6 @@ | ||||
|    "fieldname": "column_break_2", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "asset_name.item_code", | ||||
|    "fieldname": "item_code", | ||||
|    "fieldtype": "Read Only", | ||||
|    "label": "Item Code" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "asset_name.item_name", | ||||
|    "fieldname": "item_name", | ||||
|    "fieldtype": "Read Only", | ||||
|    "label": "Item Name" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_5", | ||||
|    "fieldtype": "Section Break", | ||||
| @ -74,33 +67,20 @@ | ||||
|    "label": "Failure Date", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "assign_to", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Assign To", | ||||
|    "options": "User" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fetch_from": "assign_to.full_name", | ||||
|    "fieldname": "assign_to_name", | ||||
|    "fieldtype": "Read Only", | ||||
|    "label": "Assign To Name" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_6", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "completion_date", | ||||
|    "fieldtype": "Datetime", | ||||
|    "label": "Completion Date" | ||||
|    "label": "Completion Date", | ||||
|    "no_copy": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "default": "Pending", | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "repair_status", | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Repair Status", | ||||
| @ -116,25 +96,18 @@ | ||||
|   { | ||||
|    "fieldname": "description", | ||||
|    "fieldtype": "Long Text", | ||||
|    "label": "Error Description", | ||||
|    "reqd": 1 | ||||
|    "label": "Error Description" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_9", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "actions_performed", | ||||
|    "fieldtype": "Long Text", | ||||
|    "label": "Actions performed" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_17", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "downtime", | ||||
|    "fieldtype": "Data", | ||||
|    "in_list_view": 1, | ||||
| @ -146,7 +119,7 @@ | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "default": "0", | ||||
|    "fieldname": "repair_cost", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Repair Cost" | ||||
| @ -159,12 +132,139 @@ | ||||
|    "options": "Asset Repair", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "columns": 1, | ||||
|    "fieldname": "asset", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Asset", | ||||
|    "options": "Asset", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "asset.asset_name", | ||||
|    "fieldname": "asset_name", | ||||
|    "fieldtype": "Read Only", | ||||
|    "label": "Asset Name" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_8", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "capitalize_repair_cost", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Capitalize Repair Cost" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "accounting_details", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Accounting Details" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "stock_items", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "Stock Items", | ||||
|    "mandatory_depends_on": "stock_consumption", | ||||
|    "options": "Asset Repair Consumed Item" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_23", | ||||
|    "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": "project", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Project", | ||||
|    "options": "Project" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_14", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "stock_consumption", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Stock Consumed During Repair" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "stock_consumption", | ||||
|    "fieldname": "stock_consumption_details_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Stock Consumption Details" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0", | ||||
|    "description": "Sum of Repair Cost and Value of Consumed Stock Items.", | ||||
|    "fieldname": "total_repair_cost", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Total Repair Cost", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "stock_consumption", | ||||
|    "fieldname": "warehouse", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Warehouse", | ||||
|    "options": "Warehouse" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "capitalize_repair_cost", | ||||
|    "fieldname": "asset_depreciation_details_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Asset Depreciation Details" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "increase_in_asset_life", | ||||
|    "fieldtype": "Int", | ||||
|    "label": "Increase In Asset Life(Months)", | ||||
|    "no_copy": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "purchase_invoice", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Purchase Invoice", | ||||
|    "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0", | ||||
|    "no_copy": 1, | ||||
|    "options": "Purchase Invoice" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "asset.company", | ||||
|    "fieldname": "company", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Company", | ||||
|    "options": "Company" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "stock_entry", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Stock Entry", | ||||
|    "options": "Stock Entry", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-01-22 15:08:12.495850", | ||||
|  "modified": "2021-06-25 13:14:38.307723", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Assets", | ||||
|  "name": "Asset Repair", | ||||
| @ -203,6 +303,7 @@ | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "title_field": "asset_name", | ||||
|  "track_changes": 1, | ||||
|  "track_seen": 1 | ||||
| } | ||||
| @ -5,16 +5,252 @@ | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.utils import time_diff_in_hours | ||||
| from frappe.model.document import Document | ||||
| from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint | ||||
| from erpnext.accounts.general_ledger import make_gl_entries | ||||
| from erpnext.assets.doctype.asset.asset import get_asset_account | ||||
| from erpnext.controllers.accounts_controller import AccountsController | ||||
| 
 | ||||
| class AssetRepair(Document): | ||||
| class AssetRepair(AccountsController): | ||||
| 	def validate(self): | ||||
| 		if self.repair_status == "Completed" and not self.completion_date: | ||||
| 			frappe.throw(_("Please select Completion Date for Completed Repair")) | ||||
| 		self.asset_doc = frappe.get_doc('Asset', self.asset) | ||||
| 		self.update_status() | ||||
| 
 | ||||
| 		if self.get('stock_items'): | ||||
| 			self.set_total_value() | ||||
| 		self.calculate_total_repair_cost() | ||||
| 		 | ||||
| 	def update_status(self): | ||||
| 		if self.repair_status == 'Pending': | ||||
| 			frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') | ||||
| 		else: | ||||
| 			self.asset_doc.set_status() | ||||
| 
 | ||||
| 	def set_total_value(self): | ||||
| 		for item in self.get('stock_items'): | ||||
| 			item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) | ||||
| 
 | ||||
| 	def calculate_total_repair_cost(self): | ||||
| 		self.total_repair_cost = flt(self.repair_cost) | ||||
| 
 | ||||
| 		total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() | ||||
| 		self.total_repair_cost += total_value_of_stock_consumed | ||||
| 
 | ||||
| 	def before_submit(self): | ||||
| 		self.check_repair_status() | ||||
| 
 | ||||
| 		if self.get('stock_consumption') or self.get('capitalize_repair_cost'): | ||||
| 			self.increase_asset_value() | ||||
| 		if self.get('stock_consumption'): | ||||
| 			self.check_for_stock_items_and_warehouse() | ||||
| 			self.decrease_stock_quantity() | ||||
| 		if self.get('capitalize_repair_cost'): | ||||
| 			self.make_gl_entries() | ||||
| 			if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: | ||||
| 				self.modify_depreciation_schedule() | ||||
| 
 | ||||
| 		self.asset_doc.flags.ignore_validate_update_after_submit = True | ||||
| 		self.asset_doc.prepare_depreciation_data() | ||||
| 		self.asset_doc.save() | ||||
| 
 | ||||
| 	def before_cancel(self): | ||||
| 		self.asset_doc = frappe.get_doc('Asset', self.asset) | ||||
| 
 | ||||
| 		if self.get('stock_consumption') or self.get('capitalize_repair_cost'): | ||||
| 			self.decrease_asset_value() | ||||
| 		if self.get('stock_consumption'): | ||||
| 			self.increase_stock_quantity() | ||||
| 		if self.get('capitalize_repair_cost'): | ||||
| 			self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') | ||||
| 			self.make_gl_entries(cancel=True) | ||||
| 			if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: | ||||
| 				self.revert_depreciation_schedule_on_cancellation() | ||||
| 
 | ||||
| 		self.asset_doc.flags.ignore_validate_update_after_submit = True | ||||
| 		self.asset_doc.prepare_depreciation_data() | ||||
| 		self.asset_doc.save() | ||||
| 
 | ||||
| 	def check_repair_status(self): | ||||
| 		if self.repair_status == "Pending": | ||||
| 			frappe.throw(_("Please update Repair Status.")) | ||||
| 
 | ||||
| 	def check_for_stock_items_and_warehouse(self): | ||||
| 		if not self.get('stock_items'): | ||||
| 			frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) | ||||
| 		if not self.warehouse: | ||||
| 			frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) | ||||
| 
 | ||||
| 	def increase_asset_value(self): | ||||
| 		total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() | ||||
| 
 | ||||
| 		if self.asset_doc.calculate_depreciation: | ||||
| 			for row in self.asset_doc.finance_books: | ||||
| 				row.value_after_depreciation += total_value_of_stock_consumed | ||||
| 
 | ||||
| 				if self.capitalize_repair_cost: | ||||
| 					row.value_after_depreciation += self.repair_cost | ||||
| 
 | ||||
| 	def decrease_asset_value(self): | ||||
| 		total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() | ||||
| 
 | ||||
| 		if self.asset_doc.calculate_depreciation: | ||||
| 			for row in self.asset_doc.finance_books: | ||||
| 				row.value_after_depreciation -= total_value_of_stock_consumed | ||||
| 
 | ||||
| 				if self.capitalize_repair_cost: | ||||
| 					row.value_after_depreciation -= self.repair_cost | ||||
| 		 | ||||
| 	def get_total_value_of_stock_consumed(self): | ||||
| 		total_value_of_stock_consumed = 0 | ||||
| 		if self.get('stock_consumption'): | ||||
| 			for item in self.get('stock_items'): | ||||
| 				total_value_of_stock_consumed += item.total_value | ||||
| 
 | ||||
| 		return total_value_of_stock_consumed | ||||
| 
 | ||||
| 	def decrease_stock_quantity(self): | ||||
| 		stock_entry = frappe.get_doc({ | ||||
| 			"doctype": "Stock Entry", | ||||
| 			"stock_entry_type": "Material Issue", | ||||
| 			"company": self.company | ||||
| 		}) | ||||
| 
 | ||||
| 		for stock_item in self.get('stock_items'): | ||||
| 			stock_entry.append('items', { | ||||
| 				"s_warehouse": self.warehouse, | ||||
| 				"item_code": stock_item.item, | ||||
| 				"qty": stock_item.consumed_quantity, | ||||
| 				"basic_rate": stock_item.valuation_rate | ||||
| 			}) | ||||
| 
 | ||||
| 		stock_entry.insert() | ||||
| 		stock_entry.submit() | ||||
| 
 | ||||
| 		self.db_set('stock_entry', stock_entry.name) | ||||
| 
 | ||||
| 	def increase_stock_quantity(self): | ||||
| 		stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) | ||||
| 		stock_entry.flags.ignore_links = True | ||||
| 		stock_entry.cancel() | ||||
| 
 | ||||
| 	def make_gl_entries(self, cancel=False): | ||||
| 		if flt(self.repair_cost) > 0: | ||||
| 			gl_entries = self.get_gl_entries() | ||||
| 			make_gl_entries(gl_entries, cancel) | ||||
| 
 | ||||
| 	def get_gl_entries(self): | ||||
| 		gl_entries = [] | ||||
| 		repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') | ||||
| 		fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company) | ||||
| 		expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account	 | ||||
| 
 | ||||
| 		gl_entries.append( | ||||
| 			self.get_gl_dict({ | ||||
| 				"account": expense_account, | ||||
| 				"credit": self.repair_cost, | ||||
| 				"credit_in_account_currency": self.repair_cost, | ||||
| 				"against": repair_and_maintenance_account, | ||||
| 				"voucher_type": self.doctype,		 | ||||
| 				"voucher_no": self.name, | ||||
| 				"cost_center": self.cost_center, | ||||
| 				"posting_date": getdate(), | ||||
| 				"company": self.company | ||||
| 			}, item=self) | ||||
| 		) | ||||
| 
 | ||||
| 		if self.get('stock_consumption'): | ||||
| 			# creating GL Entries for each row in Stock Items based on the Stock Entry created for it | ||||
| 			stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) | ||||
| 			for item in stock_entry.items: | ||||
| 				gl_entries.append( | ||||
| 					self.get_gl_dict({ | ||||
| 						"account": item.expense_account, | ||||
| 						"credit": item.amount, | ||||
| 						"credit_in_account_currency": item.amount, | ||||
| 						"against": repair_and_maintenance_account, | ||||
| 						"voucher_type": self.doctype,		 | ||||
| 						"voucher_no": self.name, | ||||
| 						"cost_center": self.cost_center, | ||||
| 						"posting_date": getdate(), | ||||
| 						"company": self.company | ||||
| 					}, item=self) | ||||
| 				) | ||||
| 
 | ||||
| 		gl_entries.append( | ||||
| 			self.get_gl_dict({ | ||||
| 				"account": fixed_asset_account, | ||||
| 				"debit": self.total_repair_cost, | ||||
| 				"debit_in_account_currency": self.total_repair_cost, | ||||
| 				"against": expense_account, | ||||
| 				"voucher_type": self.doctype, | ||||
| 				"voucher_no": self.name, | ||||
| 				"cost_center": self.cost_center, | ||||
| 				"posting_date": getdate(), | ||||
| 				"against_voucher_type": "Purchase Invoice", | ||||
| 				"against_voucher": self.purchase_invoice, | ||||
| 				"company": self.company | ||||
| 			}, item=self) | ||||
| 		) | ||||
| 
 | ||||
| 		return gl_entries | ||||
| 
 | ||||
| 	def modify_depreciation_schedule(self): | ||||
| 		for row in self.asset_doc.finance_books: | ||||
| 			row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation | ||||
| 
 | ||||
| 			self.asset_doc.flags.increase_in_asset_life = False | ||||
| 			extra_months = self.increase_in_asset_life % row.frequency_of_depreciation | ||||
| 			if extra_months != 0: | ||||
| 				self.calculate_last_schedule_date(self.asset_doc, row, extra_months) | ||||
| 
 | ||||
| 	# to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation | ||||
| 	def calculate_last_schedule_date(self, asset, row, extra_months): | ||||
| 		asset.flags.increase_in_asset_life = True | ||||
| 		number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ | ||||
| 			cint(asset.number_of_depreciations_booked) | ||||
| 
 | ||||
| 		# the Schedule Date in the final row of the old Depreciation Schedule | ||||
| 		last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date | ||||
| 
 | ||||
| 		# the Schedule Date in the final row of the new Depreciation Schedule | ||||
| 		asset.to_date = add_months(last_schedule_date, extra_months) | ||||
| 
 | ||||
| 		# the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations | ||||
| 		# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... | ||||
| 		schedule_date = add_months(row.depreciation_start_date, | ||||
| 			number_of_pending_depreciations * cint(row.frequency_of_depreciation)) | ||||
| 
 | ||||
| 		if asset.to_date > schedule_date: | ||||
| 			row.total_number_of_depreciations += 1 | ||||
| 
 | ||||
| 	def revert_depreciation_schedule_on_cancellation(self): | ||||
| 		for row in self.asset_doc.finance_books: | ||||
| 			row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation | ||||
| 
 | ||||
| 			self.asset_doc.flags.increase_in_asset_life = False | ||||
| 			extra_months = self.increase_in_asset_life % row.frequency_of_depreciation | ||||
| 			if extra_months != 0: | ||||
| 				self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months) | ||||
| 
 | ||||
| 	def calculate_last_schedule_date_before_modification(self, asset, row, extra_months): | ||||
| 		asset.flags.increase_in_asset_life = True | ||||
| 		number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ | ||||
| 			cint(asset.number_of_depreciations_booked) | ||||
| 
 | ||||
| 		# the Schedule Date in the final row of the modified Depreciation Schedule | ||||
| 		last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date | ||||
| 
 | ||||
| 		# the Schedule Date in the final row of the original Depreciation Schedule | ||||
| 		asset.to_date = add_months(last_schedule_date, -extra_months) | ||||
| 
 | ||||
| 		# the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations | ||||
| 		# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... | ||||
| 		schedule_date = add_months(row.depreciation_start_date, | ||||
| 			(number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation)) | ||||
| 
 | ||||
| 		if asset.to_date < schedule_date: | ||||
| 			row.total_number_of_depreciations -= 1 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_downtime(failure_date, completion_date): | ||||
| 	downtime = time_diff_in_hours(completion_date, failure_date) | ||||
| 	return round(downtime, 2) | ||||
| 	return round(downtime, 2) | ||||
|  | ||||
| @ -2,8 +2,167 @@ | ||||
| # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import frappe | ||||
| from frappe.utils import nowdate, flt | ||||
| import unittest | ||||
| from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company | ||||
| 
 | ||||
| class TestAssetRepair(unittest.TestCase): | ||||
| 	pass | ||||
| 	def setUp(self): | ||||
| 		set_depreciation_settings_in_company() | ||||
| 		create_asset_data() | ||||
| 		frappe.db.sql("delete from `tabTax Rule`") | ||||
| 
 | ||||
| 	def test_update_status(self): | ||||
| 		asset = create_asset() | ||||
| 		initial_status = asset.status | ||||
| 		asset_repair = create_asset_repair(asset = asset) | ||||
| 
 | ||||
| 		if asset_repair.repair_status == "Pending": | ||||
| 			asset.reload() | ||||
| 			self.assertEqual(asset.status, "Out of Order") | ||||
| 
 | ||||
| 		asset_repair.repair_status = "Completed" | ||||
| 		asset_repair.save() | ||||
| 		asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status") | ||||
| 		self.assertEqual(asset_status, initial_status) | ||||
| 
 | ||||
| 	def test_stock_item_total_value(self): | ||||
| 		asset_repair = create_asset_repair(stock_consumption = 1) | ||||
| 
 | ||||
| 		for item in asset_repair.stock_items: | ||||
| 			total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) | ||||
| 			self.assertEqual(item.total_value, total_value) | ||||
| 
 | ||||
| 	def test_total_repair_cost(self): | ||||
| 		asset_repair = create_asset_repair(stock_consumption = 1) | ||||
| 
 | ||||
| 		total_repair_cost = asset_repair.repair_cost | ||||
| 		self.assertEqual(total_repair_cost, asset_repair.repair_cost) | ||||
| 		for item in asset_repair.stock_items: | ||||
| 			total_repair_cost += item.total_value | ||||
| 			 | ||||
| 		self.assertEqual(total_repair_cost, asset_repair.total_repair_cost) | ||||
| 
 | ||||
| 	def test_repair_status_after_submit(self): | ||||
| 		asset_repair = create_asset_repair(submit = 1) | ||||
| 		self.assertNotEqual(asset_repair.repair_status, "Pending") | ||||
| 
 | ||||
| 	def test_stock_items(self): | ||||
| 		asset_repair = create_asset_repair(stock_consumption = 1) | ||||
| 		self.assertTrue(asset_repair.stock_consumption) | ||||
| 		self.assertTrue(asset_repair.stock_items) | ||||
| 
 | ||||
| 	def test_warehouse(self): | ||||
| 		asset_repair = create_asset_repair(stock_consumption = 1) | ||||
| 		self.assertTrue(asset_repair.stock_consumption) | ||||
| 		self.assertTrue(asset_repair.warehouse) | ||||
| 
 | ||||
| 	def test_decrease_stock_quantity(self): | ||||
| 		asset_repair = create_asset_repair(stock_consumption = 1, submit = 1) | ||||
| 		stock_entry = frappe.get_last_doc('Stock Entry') | ||||
| 
 | ||||
| 		self.assertEqual(stock_entry.stock_entry_type, "Material Issue") | ||||
| 		self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) | ||||
| 		self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) | ||||
| 		self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) | ||||
| 
 | ||||
| 	def test_increase_in_asset_value_due_to_stock_consumption(self): | ||||
| 		asset = create_asset(calculate_depreciation = 1) | ||||
| 		initial_asset_value = get_asset_value(asset) | ||||
| 		asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1) | ||||
| 		asset.reload() | ||||
| 
 | ||||
| 		increase_in_asset_value = get_asset_value(asset) - initial_asset_value | ||||
| 		self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) | ||||
| 
 | ||||
| 	def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): | ||||
| 		asset = create_asset(calculate_depreciation = 1) | ||||
| 		initial_asset_value = get_asset_value(asset) | ||||
| 		asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) | ||||
| 		asset.reload() | ||||
| 
 | ||||
| 		increase_in_asset_value = get_asset_value(asset) - initial_asset_value | ||||
| 		self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) | ||||
| 
 | ||||
| 	def test_purchase_invoice(self): | ||||
| 		asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) | ||||
| 		self.assertTrue(asset_repair.purchase_invoice) | ||||
| 
 | ||||
| 	def test_gl_entries(self): | ||||
| 		asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) | ||||
| 		gl_entry = frappe.get_last_doc('GL Entry') | ||||
| 		self.assertEqual(asset_repair.name, gl_entry.voucher_no) | ||||
| 
 | ||||
| 	def test_increase_in_asset_life(self): | ||||
| 		asset = create_asset(calculate_depreciation = 1) | ||||
| 		initial_num_of_depreciations = num_of_depreciations(asset) | ||||
| 		create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) | ||||
| 		asset.reload() | ||||
| 	 | ||||
| 		self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) | ||||
| 		self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation) | ||||
| 
 | ||||
| def get_asset_value(asset): | ||||
| 	return asset.finance_books[0].value_after_depreciation | ||||
| 
 | ||||
| def num_of_depreciations(asset): | ||||
| 	return asset.finance_books[0].total_number_of_depreciations | ||||
| 
 | ||||
| def create_asset_repair(**args): | ||||
| 	from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse | ||||
| 	from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice | ||||
| 
 | ||||
| 	args = frappe._dict(args) | ||||
| 
 | ||||
| 	if args.asset: | ||||
| 		asset = args.asset | ||||
| 	else: | ||||
| 		asset = create_asset(is_existing_asset = 1) | ||||
| 	asset_repair = frappe.new_doc("Asset Repair") | ||||
| 	asset_repair.update({ | ||||
| 		"asset": asset.name, | ||||
| 		"asset_name": asset.asset_name, | ||||
| 		"failure_date": nowdate(), | ||||
| 		"description": "Test Description", | ||||
| 		"repair_cost": 0, | ||||
| 		"company": asset.company | ||||
| 	}) | ||||
| 
 | ||||
| 	if args.stock_consumption: | ||||
| 		asset_repair.stock_consumption = 1 | ||||
| 		asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) | ||||
| 		asset_repair.append("stock_items", { | ||||
| 			"item": args.item or args.item_code or "_Test Item", | ||||
| 			"valuation_rate": args.rate if args.get("rate") is not None else 100, | ||||
| 			"consumed_quantity": args.qty or 1 | ||||
| 		}) | ||||
| 
 | ||||
| 	asset_repair.insert(ignore_if_duplicate=True) | ||||
| 	 | ||||
| 	if args.submit: | ||||
| 		asset_repair.repair_status = "Completed" | ||||
| 		asset_repair.cost_center = "_Test Cost Center - _TC" | ||||
| 
 | ||||
| 		if args.stock_consumption: | ||||
| 			stock_entry = frappe.get_doc({ | ||||
| 				"doctype": "Stock Entry", | ||||
| 				"stock_entry_type": "Material Receipt", | ||||
| 				"company": asset.company | ||||
| 			}) | ||||
| 			stock_entry.append('items', { | ||||
| 				"t_warehouse": asset_repair.warehouse, | ||||
| 				"item_code": asset_repair.stock_items[0].item, | ||||
| 				"qty": asset_repair.stock_items[0].consumed_quantity | ||||
| 			}) | ||||
| 			stock_entry.submit() | ||||
| 
 | ||||
| 		if args.capitalize_repair_cost: | ||||
| 			asset_repair.capitalize_repair_cost = 1 | ||||
| 			asset_repair.repair_cost = 1000 | ||||
| 			if asset.calculate_depreciation: | ||||
| 				asset_repair.increase_in_asset_life = 12 | ||||
| 			asset_repair.purchase_invoice = make_purchase_invoice().name | ||||
| 
 | ||||
| 		asset_repair.submit() | ||||
| 	return asset_repair | ||||
| @ -0,0 +1,55 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2021-05-12 02:41:54.161024", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "item", | ||||
|   "valuation_rate", | ||||
|   "consumed_quantity", | ||||
|   "total_value" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "item", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Item", | ||||
|    "options": "Item" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "item.valuation_rate", | ||||
|    "fieldname": "valuation_rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Valuation Rate", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "consumed_quantity", | ||||
|    "fieldtype": "Data", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Consumed Quantity" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "total_value", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Total Value", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-05-12 03:19:55.006300", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Assets", | ||||
|  "name": "Asset Repair Consumed Item", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -0,0 +1,8 @@ | ||||
| # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class AssetRepairConsumedItem(Document): | ||||
| 	pass | ||||
| @ -123,7 +123,7 @@ | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "issingle": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-06-23 19:40:00.120822", | ||||
|  "modified": "2021-06-24 10:38:28.934525", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Buying Settings", | ||||
|  | ||||
							
								
								
									
										72
									
								
								erpnext/change_log/v13/v13_6_0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								erpnext/change_log/v13/v13_6_0.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| # Version 13.6.0 Release Notes | ||||
| 
 | ||||
| ### Features & Enhancements | ||||
| 
 | ||||
| - Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523)) | ||||
| - Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044)) | ||||
| - Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184)) | ||||
| - Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878)) | ||||
| - Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705)) | ||||
| - Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030)) | ||||
| - Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696)) | ||||
| - Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891)) | ||||
| 
 | ||||
| ### Fixes | ||||
| 
 | ||||
| - Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046)) | ||||
| - User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176)) | ||||
| - Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092)) | ||||
| - Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978)) | ||||
| - Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073)) | ||||
| - Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245)) | ||||
| - Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230)) | ||||
| - Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125)) | ||||
| - Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134)) | ||||
| - Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196)) | ||||
| - Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083)) | ||||
| - Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941)) | ||||
| - Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945)) | ||||
| - Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011)) | ||||
| - Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070)) | ||||
| - Training event ([#26071](https://github.com/frappe/erpnext/pull/26071)) | ||||
| - Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122)) | ||||
| - Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220)) | ||||
| - Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003)) | ||||
| - Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229)) | ||||
| - Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269)) | ||||
| - Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045)) | ||||
| - Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170)) | ||||
| - Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032)) | ||||
| - Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095)) | ||||
| - Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023)) | ||||
| - Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191)) | ||||
| - Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188)) | ||||
| - Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217)) | ||||
| - Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152)) | ||||
| - Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108)) | ||||
| - Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202)) | ||||
| - Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906)) | ||||
| - Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894)) | ||||
| - Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997)) | ||||
| - POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051)) | ||||
| - Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043)) | ||||
| - Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143)) | ||||
| - Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211)) | ||||
| - Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126)) | ||||
| - Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192)) | ||||
| - Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081)) | ||||
| - Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187)) | ||||
| - Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195)) | ||||
| - Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947)) | ||||
| - Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951)) | ||||
| - Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968)) | ||||
| - Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037)) | ||||
| - Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198)) | ||||
| - Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100)) | ||||
| - Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098)) | ||||
| - Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062)) | ||||
| - Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031)) | ||||
| - Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203)) | ||||
| - Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185)) | ||||
| - Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934)) | ||||
| - Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201)) | ||||
| @ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate | ||||
| 
 | ||||
| import erpnext | ||||
| from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map | ||||
| from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year | ||||
| from erpnext.accounts.utils import get_fiscal_year | ||||
| from erpnext.controllers.accounts_controller import AccountsController | ||||
| from erpnext.stock import get_warehouse_account_map | ||||
| from erpnext.stock.stock_ledger import get_valuation_rate | ||||
| @ -356,42 +356,68 @@ class StockController(AccountsController): | ||||
| 		}, update_modified) | ||||
| 
 | ||||
| 	def validate_inspection(self): | ||||
| 		'''Checks if quality inspection is set for Items that require inspection. | ||||
| 		On submit, throw an exception''' | ||||
| 		inspection_required_fieldname = None | ||||
| 		if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: | ||||
| 			inspection_required_fieldname = "inspection_required_before_purchase" | ||||
| 		elif self.doctype in ["Delivery Note", "Sales Invoice"]: | ||||
| 			inspection_required_fieldname = "inspection_required_before_delivery" | ||||
| 		"""Checks if quality inspection is set/ is valid for Items that require inspection.""" | ||||
| 		inspection_fieldname_map = { | ||||
| 			"Purchase Receipt": "inspection_required_before_purchase", | ||||
| 			"Purchase Invoice": "inspection_required_before_purchase", | ||||
| 			"Sales Invoice": "inspection_required_before_delivery", | ||||
| 			"Delivery Note": "inspection_required_before_delivery" | ||||
| 		} | ||||
| 		inspection_required_fieldname = inspection_fieldname_map.get(self.doctype) | ||||
| 
 | ||||
| 		# return if inspection is not required on document level | ||||
| 		if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or | ||||
| 			(self.doctype == "Stock Entry" and not self.inspection_required) or | ||||
| 			(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): | ||||
| 				return | ||||
| 
 | ||||
| 		for d in self.get('items'): | ||||
| 			qa_required = False | ||||
| 			if (inspection_required_fieldname and not d.quality_inspection and | ||||
| 				frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)): | ||||
| 				qa_required = True | ||||
| 			elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: | ||||
| 				qa_required = True | ||||
| 			if self.docstatus == 1 and d.quality_inspection: | ||||
| 				qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) | ||||
| 				if qa_doc.docstatus == 0: | ||||
| 					link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) | ||||
| 					frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) | ||||
| 		for row in self.get('items'): | ||||
| 			qi_required = False | ||||
| 			if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)): | ||||
| 				qi_required = True | ||||
| 			elif self.doctype == "Stock Entry" and row.t_warehouse: | ||||
| 				qi_required = True # inward stock needs inspection | ||||
| 
 | ||||
| 				if qa_doc.status != 'Accepted': | ||||
| 					frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") | ||||
| 						.format(d.idx, d.item_code), QualityInspectionRejectedError) | ||||
| 			elif qa_required : | ||||
| 				action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted | ||||
| 				if self.docstatus==1 and action == 'Stop': | ||||
| 					frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)), | ||||
| 						exc=QualityInspectionRequiredError) | ||||
| 				else: | ||||
| 					frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code))) | ||||
| 			if qi_required: # validate row only if inspection is required on item level | ||||
| 				self.validate_qi_presence(row) | ||||
| 				if self.docstatus == 1: | ||||
| 					self.validate_qi_submission(row) | ||||
| 					self.validate_qi_rejection(row) | ||||
| 
 | ||||
| 	def validate_qi_presence(self, row): | ||||
| 		"""Check if QI is present on row level. Warn on save and stop on submit if missing.""" | ||||
| 		if not row.quality_inspection: | ||||
| 			msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}" | ||||
| 			if self.docstatus == 1: | ||||
| 				frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError) | ||||
| 			else: | ||||
| 				frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue") | ||||
| 
 | ||||
| 	def validate_qi_submission(self, row): | ||||
| 		"""Check if QI is submitted on row level, during submission""" | ||||
| 		action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") | ||||
| 		qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") | ||||
| 
 | ||||
| 		if not qa_docstatus == 1: | ||||
| 			link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) | ||||
| 			msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" | ||||
| 			if action == "Stop": | ||||
| 				frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) | ||||
| 			else: | ||||
| 				frappe.msgprint(_(msg), alert=True, indicator="orange") | ||||
| 
 | ||||
| 	def validate_qi_rejection(self, row): | ||||
| 		"""Check if QI is rejected on row level, during submission""" | ||||
| 		action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected") | ||||
| 		qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") | ||||
| 
 | ||||
| 		if qa_status == "Rejected": | ||||
| 			link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) | ||||
| 			msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" | ||||
| 			if action == "Stop": | ||||
| 				frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) | ||||
| 			else: | ||||
| 				frappe.msgprint(_(msg), alert=True, indicator="orange") | ||||
| 
 | ||||
| 	def update_blanket_order(self): | ||||
| 		blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) | ||||
| @ -497,9 +523,6 @@ class StockController(AccountsController): | ||||
| 		}) | ||||
| 		if future_sle_exists(args): | ||||
| 			create_repost_item_valuation_entry(args) | ||||
| 		elif not is_reposting_pending(): | ||||
| 			check_if_stock_and_account_balance_synced(self.posting_date, | ||||
| 				self.company, self.doctype, self.name) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_quality_inspections(doctype, docname, items): | ||||
|  | ||||
| @ -102,7 +102,7 @@ | ||||
|   } | ||||
|  ], | ||||
|  "links": [], | ||||
|  "modified": "2020-01-28 16:16:45.447213", | ||||
|  "modified": "2021-06-30 12:09:14.228756", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "CRM", | ||||
|  "name": "Appointment", | ||||
| @ -153,6 +153,18 @@ | ||||
|    "role": "Sales User", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Employee", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ], | ||||
|  "quick_entry": 1, | ||||
|  | ||||
| @ -168,12 +168,13 @@ class Lead(SellingController): | ||||
| 		if self.phone: | ||||
| 			contact.append("phone_nos", { | ||||
| 				"phone": self.phone, | ||||
| 				"is_primary": 1 | ||||
| 				"is_primary_phone": 1 | ||||
| 			}) | ||||
| 
 | ||||
| 		if self.mobile_no: | ||||
| 			contact.append("phone_nos", { | ||||
| 				"phone": self.mobile_no | ||||
| 				"phone": self.mobile_no, | ||||
| 				"is_primary_mobile_no":1 | ||||
| 			}) | ||||
| 
 | ||||
| 		contact.insert(ignore_permissions=True) | ||||
|  | ||||
| @ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program): | ||||
| 	student = get_current_student() | ||||
| 	course_enrollment = get_enrollment("course", course, student.name) | ||||
| 	if not course_enrollment: | ||||
| 		program_enrollment = get_enrollment('program', program, student.name) | ||||
| 		program_enrollment = get_enrollment('program', program.name, student.name) | ||||
| 		if not program_enrollment: | ||||
| 			frappe.throw(_("You are not enrolled in program {0}").format(program)) | ||||
| 			return | ||||
| 		return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name)) | ||||
| 		return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name)) | ||||
| 	else: | ||||
| 		return frappe.get_doc('Course Enrollment', course_enrollment) | ||||
| 
 | ||||
|  | ||||
| @ -6,7 +6,7 @@ from __future__ import unicode_literals | ||||
| import frappe | ||||
| import unittest | ||||
| import json | ||||
| from frappe.utils import getdate | ||||
| from frappe.utils import getdate, strip_html | ||||
| from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient | ||||
| 
 | ||||
| class TestPatientHistorySettings(unittest.TestCase): | ||||
| @ -44,9 +44,9 @@ class TestPatientHistorySettings(unittest.TestCase): | ||||
| 		self.assertTrue(medical_rec) | ||||
| 
 | ||||
| 		medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) | ||||
| 		expected_subject = "<b>Date: </b>{0}<br><b>Rating: </b>3<br><b>Feedback: </b>Test Patient History Settings<br>".format( | ||||
| 		expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format( | ||||
| 			frappe.utils.format_date(getdate())) | ||||
| 		self.assertEqual(medical_rec.subject, expected_subject) | ||||
| 		self.assertEqual(strip_html(medical_rec.subject), expected_subject) | ||||
| 		self.assertEqual(medical_rec.patient, patient) | ||||
| 		self.assertEqual(medical_rec.communication_date, getdate()) | ||||
| 
 | ||||
| @ -101,4 +101,4 @@ def create_doc(patient): | ||||
| 	}).insert() | ||||
| 	doc.submit() | ||||
| 
 | ||||
| 	return doc | ||||
| 	return doc | ||||
|  | ||||
| @ -158,6 +158,7 @@ website_route_rules = [ | ||||
| 			"parents": [{"label": _("Material Request"), "route": "material-requests"}] | ||||
| 		} | ||||
| 	}, | ||||
| 	{"from_route": "/project", "to_route": "Project"} | ||||
| ] | ||||
| 
 | ||||
| standard_portal_menu_items = [ | ||||
|  | ||||
| @ -110,7 +110,7 @@ | ||||
|    "label": "Allocation" | ||||
|   }, | ||||
|   { | ||||
|     "allow_on_submit": 1, | ||||
|    "allow_on_submit": 1, | ||||
|    "bold": 1, | ||||
|    "fieldname": "new_leaves_allocated", | ||||
|    "fieldtype": "Float", | ||||
|  | ||||
| @ -164,7 +164,6 @@ class TestLeaveAllocation(unittest.TestCase): | ||||
| 		leave_allocation.cancel() | ||||
| 		self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) | ||||
| 
 | ||||
| 
 | ||||
| 	def test_leave_addition_after_submit(self): | ||||
| 		frappe.db.sql("delete from `tabLeave Allocation`") | ||||
| 		frappe.db.sql("delete from `tabLeave Ledger Entry`") | ||||
| @ -179,7 +178,6 @@ class TestLeaveAllocation(unittest.TestCase): | ||||
| 	def test_leave_subtraction_after_submit(self): | ||||
| 		frappe.db.sql("delete from `tabLeave Allocation`") | ||||
| 		frappe.db.sql("delete from `tabLeave Ledger Entry`") | ||||
| 
 | ||||
| 		leave_allocation = create_leave_allocation() | ||||
| 		leave_allocation.submit() | ||||
| 		self.assertTrue(leave_allocation.total_leaves_allocated, 15) | ||||
| @ -199,8 +197,8 @@ class TestLeaveAllocation(unittest.TestCase): | ||||
| 			"doctype": 'Leave Application', | ||||
| 			"employee": employee.name, | ||||
| 			"leave_type": "_Test Leave Type", | ||||
| 			"from_date": nowdate(), | ||||
| 			"to_date": add_days(nowdate(), 10), | ||||
| 			"from_date": add_months(nowdate(), 2), | ||||
| 			"to_date": add_months(add_days(nowdate(), 10), 2), | ||||
| 			"company": erpnext.get_default_company() or "_Test Company", | ||||
| 			"docstatus": 1, | ||||
| 			"status": "Approved", | ||||
|  | ||||
| @ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', { | ||||
| 				frappe.set_route("List", "Training Feedback"); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| 		frm.events.set_employee_query(frm); | ||||
| 	}, | ||||
| 
 | ||||
| frappe.ui.form.on("Training Event Employee", { | ||||
| 	employee: function (frm) { | ||||
| 	set_employee_query: function(frm) { | ||||
| 		let emp = []; | ||||
| 		for (let d in frm.doc.employees) { | ||||
| 			if (frm.doc.employees[d].employee) { | ||||
| @ -34,9 +33,17 @@ frappe.ui.form.on("Training Event Employee", { | ||||
| 		frm.set_query("employee", "employees", function () { | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					name: ["NOT IN", emp] | ||||
| 					name: ["NOT IN", emp], | ||||
| 					status: "Active" | ||||
| 				} | ||||
| 			}; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| frappe.ui.form.on("Training Event Employee", { | ||||
| 	employee: function(frm) { | ||||
| 		frm.events.set_employee_query(frm); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -19,6 +19,7 @@ | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Employee", | ||||
|    "no_copy": 1, | ||||
|    "options": "Employee" | ||||
|   }, | ||||
|   { | ||||
| @ -68,7 +69,7 @@ | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-05-21 12:41:59.336237", | ||||
|  "modified": "2021-07-02 17:20:27.630176", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "HR", | ||||
|  "name": "Training Event Employee", | ||||
|  | ||||
| @ -71,7 +71,6 @@ frappe.ui.form.on("BOM", { | ||||
| 
 | ||||
| 	refresh: function(frm) { | ||||
| 		frm.toggle_enable("item", frm.doc.__islocal); | ||||
| 		toggle_operations(frm); | ||||
| 
 | ||||
| 		frm.set_indicator_formatter('item_code', | ||||
| 			function(doc) { | ||||
| @ -326,8 +325,7 @@ frappe.ui.form.on("BOM", { | ||||
| 			freeze: true, | ||||
| 			args: { | ||||
| 				update_parent: true, | ||||
| 				from_child_bom:false, | ||||
| 				save: frm.doc.docstatus === 1 ? true : false | ||||
| 				from_child_bom:false | ||||
| 			}, | ||||
| 			callback: function(r) { | ||||
| 				refresh_field("items"); | ||||
| @ -651,15 +649,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) { | ||||
| 	erpnext.bom.calculate_total(frm.doc); | ||||
| }); | ||||
| 
 | ||||
| var toggle_operations = function(frm) { | ||||
| 	frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1); | ||||
| 	frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1); | ||||
| 	frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1); | ||||
| }; | ||||
| 
 | ||||
| frappe.ui.form.on("BOM", "with_operations", function(frm) { | ||||
| 	if(!cint(frm.doc.with_operations)) { | ||||
| 		frm.set_value("operations", []); | ||||
| 	} | ||||
| 	toggle_operations(frm); | ||||
| }); | ||||
|  | ||||
| @ -193,6 +193,7 @@ | ||||
|   }, | ||||
|   { | ||||
|    "default": "Work Order", | ||||
|    "depends_on": "with_operations", | ||||
|    "fieldname": "transfer_material_against", | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Transfer Material Against", | ||||
| @ -235,6 +236,7 @@ | ||||
|   { | ||||
|    "fieldname": "operations_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "hide_border": 1, | ||||
|    "oldfieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
| @ -245,6 +247,7 @@ | ||||
|    "options": "Routing" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "with_operations", | ||||
|    "fieldname": "operations", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "Operations", | ||||
| @ -517,7 +520,7 @@ | ||||
|  "image_field": "image", | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-21 12:29:32.634952", | ||||
|  "modified": "2021-03-16 12:25:09.081968", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "BOM", | ||||
|  | ||||
| @ -662,7 +662,7 @@ class BOM(WebsiteGenerator): | ||||
| 			self.get_routing() | ||||
| 
 | ||||
| 	def validate_operations(self): | ||||
| 		if self.with_operations and not self.get('operations'): | ||||
| 		if self.with_operations and not self.get('operations') and self.docstatus == 1: | ||||
| 			frappe.throw(_("Operations cannot be left blank")) | ||||
| 
 | ||||
| 		if self.with_operations: | ||||
| @ -1100,6 +1100,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): | ||||
| 		}, | ||||
| 		'BOM Item': { | ||||
| 			'doctype': 'BOM Item', | ||||
| 			# stop get_mapped_doc copying parent bom_no to children | ||||
| 			'field_no_map': ['bom_no'], | ||||
| 			'condition': lambda doc: doc.has_variants == 0 | ||||
| 		}, | ||||
| 	}, target_doc, postprocess) | ||||
|  | ||||
| @ -8,6 +8,7 @@ import frappe | ||||
| from frappe.utils import cstr, flt | ||||
| from frappe.test_runner import make_test_records | ||||
| from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation | ||||
| from erpnext.manufacturing.doctype.bom.bom import make_variant_bom | ||||
| from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost | ||||
| from erpnext.stock.doctype.item.test_item import make_item | ||||
| from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order | ||||
| @ -248,6 +249,37 @@ class TestBOM(unittest.TestCase): | ||||
| 		for reqd_item, created_item in zip(reqd_order, created_order): | ||||
| 			self.assertEqual(reqd_item, created_item.item_code) | ||||
| 
 | ||||
| 	def test_generated_variant_bom(self): | ||||
| 		from erpnext.controllers.item_variant import create_variant | ||||
| 
 | ||||
| 		template_item = make_item( | ||||
| 			"_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]} | ||||
| 		) | ||||
| 		variant = create_variant(template_item.item_code, {"Test Size": "Large"}) | ||||
| 		variant.insert(ignore_if_duplicate=True) | ||||
| 
 | ||||
| 		bom_tree = { | ||||
| 			template_item.item_code: { | ||||
| 				"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, | ||||
| 				"ChildPart5": {}, | ||||
| 			} | ||||
| 		} | ||||
| 		template_bom = create_nested_bom(bom_tree, prefix="") | ||||
| 		variant_bom = make_variant_bom( | ||||
| 			template_bom.name, template_bom.name, variant.item_code, variant_items=[] | ||||
| 		) | ||||
| 		variant_bom.save() | ||||
| 
 | ||||
| 		reqd_order = template_bom.get_tree_representation().level_order_traversal() | ||||
| 		created_order = variant_bom.get_tree_representation().level_order_traversal() | ||||
| 
 | ||||
| 		self.assertEqual(len(reqd_order), len(created_order)) | ||||
| 
 | ||||
| 		for reqd_item, created_item in zip(reqd_order, created_order): | ||||
| 			self.assertEqual(reqd_item.item_code, created_item.item_code) | ||||
| 			self.assertEqual(reqd_item.qty, created_item.qty) | ||||
| 			self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty) | ||||
| 
 | ||||
| 
 | ||||
| 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}) | ||||
|  | ||||
| @ -13,10 +13,10 @@ | ||||
|   "col_break1", | ||||
|   "hour_rate", | ||||
|   "time_in_mins", | ||||
|   "batch_size", | ||||
|   "operating_cost", | ||||
|   "base_hour_rate", | ||||
|   "base_operating_cost", | ||||
|   "batch_size", | ||||
|   "image" | ||||
|  ], | ||||
|  "fields": [ | ||||
| @ -61,6 +61,8 @@ | ||||
|   }, | ||||
|   { | ||||
|    "description": "In minutes", | ||||
|    "fetch_from": "operation.total_operation_time", | ||||
|    "fetch_if_empty": 1, | ||||
|    "fieldname": "time_in_mins", | ||||
|    "fieldtype": "Float", | ||||
|    "in_list_view": 1, | ||||
| @ -104,7 +106,8 @@ | ||||
|    "label": "Image" | ||||
|   }, | ||||
|   { | ||||
|    "default": "1", | ||||
|    "fetch_from": "operation.batch_size", | ||||
|    "fetch_if_empty": 1, | ||||
|    "fieldname": "batch_size", | ||||
|    "fieldtype": "Int", | ||||
|    "label": "Batch Size" | ||||
| @ -120,7 +123,7 @@ | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-10-13 18:14:10.018774", | ||||
|  "modified": "2021-01-12 14:48:09.596843", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "BOM Operation", | ||||
|  | ||||
| @ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', { | ||||
| 				} | ||||
| 			}; | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_indicator_formatter('sub_operation', | ||||
| 			function(doc) { | ||||
| 				if (doc.status == "Pending") { | ||||
| 					return "red"; | ||||
| 				} else { | ||||
| 					return doc.status === "Complete" ? "green" : "orange"; | ||||
| 				} | ||||
| 			} | ||||
| 		); | ||||
| 	}, | ||||
| 
 | ||||
| 	refresh: function(frm) { | ||||
| @ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', { | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) { | ||||
| 			frm.trigger('setup_corrective_job_card'); | ||||
| 		} | ||||
| 
 | ||||
| 		frm.set_query("quality_inspection", function() { | ||||
| 			return { | ||||
| 				query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", | ||||
| @ -43,12 +57,62 @@ frappe.ui.form.on('Job Card', { | ||||
| 
 | ||||
| 		frm.trigger("toggle_operation_number"); | ||||
| 
 | ||||
| 		if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) | ||||
| 		if (frm.doc.docstatus == 0 && !frm.is_new() && | ||||
| 			(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) | ||||
| 			&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { | ||||
| 			frm.trigger("prepare_timer_buttons"); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	setup_corrective_job_card: function(frm) { | ||||
| 		frm.add_custom_button(__('Corrective Job Card'), () => { | ||||
| 			let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation); | ||||
| 
 | ||||
| 			let fields = [ | ||||
| 				{ | ||||
| 					fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation', | ||||
| 					fieldname: 'operation', get_query() { | ||||
| 						return { | ||||
| 							filters: { | ||||
| 								"is_corrective_operation": 1 | ||||
| 							} | ||||
| 						}; | ||||
| 					} | ||||
| 				}, { | ||||
| 					fieldtype: 'Link', label: __('For Operation'), options: 'Operation', | ||||
| 					fieldname: 'for_operation', get_query() { | ||||
| 						return { | ||||
| 							filters: { | ||||
| 								"name": ["in", operations] | ||||
| 							} | ||||
| 						}; | ||||
| 					} | ||||
| 				} | ||||
| 			]; | ||||
| 
 | ||||
| 			frappe.prompt(fields, d => { | ||||
| 				frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); | ||||
| 			}, __("Select Corrective Operation")); | ||||
| 		}, __('Make')); | ||||
| 	}, | ||||
| 
 | ||||
| 	make_corrective_job_card: function(frm, operation, for_operation) { | ||||
| 		frappe.call({ | ||||
| 			method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card', | ||||
| 			args: { | ||||
| 				source_name: frm.doc.name, | ||||
| 				operation: operation, | ||||
| 				for_operation: for_operation | ||||
| 			}, | ||||
| 			callback: function(r) { | ||||
| 				if (r.message) { | ||||
| 					frappe.model.sync(r.message); | ||||
| 					frappe.set_route("Form", r.message.doctype, r.message.name); | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	operation: function(frm) { | ||||
| 		frm.trigger("toggle_operation_number"); | ||||
| 
 | ||||
| @ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', { | ||||
| 
 | ||||
| 	prepare_timer_buttons: function(frm) { | ||||
| 		frm.trigger("make_dashboard"); | ||||
| 		if (!frm.doc.job_started) { | ||||
| 			frm.add_custom_button(__("Start"), () => { | ||||
| 				if (!frm.doc.employee) { | ||||
| 					frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", | ||||
| 						fieldname: 'employee'}, d => { | ||||
| 						if (d.employee) { | ||||
| 							frm.set_value("employee", d.employee); | ||||
| 						} else { | ||||
| 							frm.events.start_job(frm); | ||||
| 						} | ||||
| 					}, __("Enter Value"), __("Start")); | ||||
| 
 | ||||
| 		if (!frm.doc.started_time && !frm.doc.current_time) { | ||||
| 			frm.add_custom_button(__("Start Job"), () => { | ||||
| 				if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) { | ||||
| 					frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), | ||||
| 						options: "Job Card Time Log", fieldname: 'employees'}, d => { | ||||
| 						frm.events.start_job(frm, "Work In Progress", d.employees); | ||||
| 					}, __("Assign Job to Employee")); | ||||
| 				} else { | ||||
| 					frm.events.start_job(frm); | ||||
| 					frm.events.start_job(frm, "Work In Progress", frm.doc.employee); | ||||
| 				} | ||||
| 			}).addClass("btn-primary"); | ||||
| 		} else if (frm.doc.status == "On Hold") { | ||||
| 			frm.add_custom_button(__("Resume"), () => { | ||||
| 				frappe.flags.resume_job = 1; | ||||
| 				frm.events.start_job(frm); | ||||
| 			frm.add_custom_button(__("Resume Job"), () => { | ||||
| 				frm.events.start_job(frm, "Resume Job", frm.doc.employee); | ||||
| 			}).addClass("btn-primary"); | ||||
| 		} else { | ||||
| 			frm.add_custom_button(__("Pause"), () => { | ||||
| 				frappe.flags.pause_job = 1; | ||||
| 				frm.set_value("status", "On Hold"); | ||||
| 				frm.events.complete_job(frm); | ||||
| 			frm.add_custom_button(__("Pause Job"), () => { | ||||
| 				frm.events.complete_job(frm, "On Hold"); | ||||
| 			}); | ||||
| 
 | ||||
| 			frm.add_custom_button(__("Complete"), () => { | ||||
| 				let completed_time = frappe.datetime.now_datetime(); | ||||
| 				frm.trigger("hide_timer"); | ||||
| 			frm.add_custom_button(__("Complete Job"), () => { | ||||
| 				var sub_operations = frm.doc.sub_operations; | ||||
| 
 | ||||
| 				if (frm.doc.for_quantity) { | ||||
| 				let set_qty = true; | ||||
| 				if (sub_operations && sub_operations.length > 1) { | ||||
| 					set_qty = false; | ||||
| 					let last_op_row = sub_operations[sub_operations.length - 2]; | ||||
| 
 | ||||
| 					if (last_op_row.status == 'Complete') { | ||||
| 						set_qty = true; | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				if (set_qty) { | ||||
| 					frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), | ||||
| 						fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { | ||||
| 							frm.events.complete_job(frm, completed_time, data.qty); | ||||
| 						}, __("Enter Value"), __("Complete")); | ||||
| 						fieldname: 'qty', default: frm.doc.for_quantity}, data => { | ||||
| 						frm.events.complete_job(frm, "Complete", data.qty); | ||||
| 					}, __("Enter Value")); | ||||
| 				} else { | ||||
| 					frm.events.complete_job(frm, completed_time, 0); | ||||
| 					frm.events.complete_job(frm, "Complete", 0.0); | ||||
| 				} | ||||
| 			}).addClass("btn-primary"); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	start_job: function(frm) { | ||||
| 		let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); | ||||
| 		row.from_time = frappe.datetime.now_datetime(); | ||||
| 		frm.set_value('job_started', 1); | ||||
| 		frm.set_value('started_time' , row.from_time); | ||||
| 		frm.set_value("status", "Work In Progress"); | ||||
| 
 | ||||
| 		if (!frappe.flags.resume_job) { | ||||
| 			frm.set_value('current_time' , 0); | ||||
| 		} | ||||
| 
 | ||||
| 		frm.save(); | ||||
| 	start_job: function(frm, status, employee) { | ||||
| 		const args = { | ||||
| 			job_card_id: frm.doc.name, | ||||
| 			start_time: frappe.datetime.now_datetime(), | ||||
| 			employees: employee, | ||||
| 			status: status | ||||
| 		}; | ||||
| 		frm.events.make_time_log(frm, args); | ||||
| 	}, | ||||
| 
 | ||||
| 	complete_job: function(frm, completed_time, completed_qty) { | ||||
| 		frm.doc.time_logs.forEach(d => { | ||||
| 			if (d.from_time && !d.to_time) { | ||||
| 				d.to_time = completed_time || frappe.datetime.now_datetime(); | ||||
| 				d.completed_qty = completed_qty || 0; | ||||
| 	complete_job: function(frm, status, completed_qty) { | ||||
| 		const args = { | ||||
| 			job_card_id: frm.doc.name, | ||||
| 			complete_time: frappe.datetime.now_datetime(), | ||||
| 			status: status, | ||||
| 			completed_qty: completed_qty | ||||
| 		}; | ||||
| 		frm.events.make_time_log(frm, args); | ||||
| 	}, | ||||
| 
 | ||||
| 				if(frappe.flags.pause_job) { | ||||
| 					let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; | ||||
| 					frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0)); | ||||
| 				} else { | ||||
| 					frm.set_value('started_time' , ''); | ||||
| 					frm.set_value('job_started', 0); | ||||
| 					frm.set_value('current_time' , 0); | ||||
| 				} | ||||
| 	make_time_log: function(frm, args) { | ||||
| 		frm.events.update_sub_operation(frm, args); | ||||
| 
 | ||||
| 				frm.save(); | ||||
| 		frappe.call({ | ||||
| 			method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log", | ||||
| 			args: { | ||||
| 				args: args | ||||
| 			}, | ||||
| 			freeze: true, | ||||
| 			callback: function () { | ||||
| 				frm.reload_doc(); | ||||
| 				frm.trigger("make_dashboard"); | ||||
| 			} | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	update_sub_operation: function(frm, args) { | ||||
| 		if (frm.doc.sub_operations && frm.doc.sub_operations.length) { | ||||
| 			let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete'); | ||||
| 			if (sub_operations && sub_operations.length) { | ||||
| 				args["sub_operation"] = sub_operations[0].sub_operation; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	validate: function(frm) { | ||||
| 		if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) { | ||||
| 			frm.trigger("reset_timer"); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	employee: function(frm) { | ||||
| 		if (frm.doc.job_started && !frm.doc.current_time) { | ||||
| 			frm.trigger("reset_timer"); | ||||
| 		} else { | ||||
| 			frm.events.start_job(frm); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	reset_timer: function(frm) { | ||||
| 		frm.set_value('started_time' , ''); | ||||
| 		frm.set_value('job_started', 0); | ||||
| 		frm.set_value('current_time' , 0); | ||||
| 	}, | ||||
| 
 | ||||
| 	make_dashboard: function(frm) { | ||||
| @ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', { | ||||
| 	}, | ||||
| 
 | ||||
| 	to_time: function(frm) { | ||||
| 		frm.set_value('job_started', 0); | ||||
| 		frm.set_value('started_time', ''); | ||||
| 	} | ||||
| }) | ||||
| @ -9,38 +9,49 @@ | ||||
|   "naming_series", | ||||
|   "work_order", | ||||
|   "bom_no", | ||||
|   "workstation", | ||||
|   "operation", | ||||
|   "operation_row_number", | ||||
|   "column_break_4", | ||||
|   "posting_date", | ||||
|   "company", | ||||
|   "remarks", | ||||
|   "production_section", | ||||
|   "production_item", | ||||
|   "item_name", | ||||
|   "for_quantity", | ||||
|   "quality_inspection", | ||||
|   "wip_warehouse", | ||||
|   "serial_no", | ||||
|   "column_break_12", | ||||
|   "employee", | ||||
|   "employee_name", | ||||
|   "status", | ||||
|   "wip_warehouse", | ||||
|   "quality_inspection", | ||||
|   "project", | ||||
|   "batch_no", | ||||
|   "operation_section_section", | ||||
|   "operation", | ||||
|   "operation_row_number", | ||||
|   "column_break_18", | ||||
|   "workstation", | ||||
|   "employee", | ||||
|   "section_break_21", | ||||
|   "sub_operations", | ||||
|   "timing_detail", | ||||
|   "time_logs", | ||||
|   "section_break_13", | ||||
|   "total_completed_qty", | ||||
|   "total_time_in_mins", | ||||
|   "column_break_15", | ||||
|   "total_time_in_mins", | ||||
|   "section_break_8", | ||||
|   "items", | ||||
|   "corrective_operation_section", | ||||
|   "for_job_card", | ||||
|   "is_corrective_job_card", | ||||
|   "column_break_33", | ||||
|   "hour_rate", | ||||
|   "for_operation", | ||||
|   "more_information", | ||||
|   "operation_id", | ||||
|   "sequence_id", | ||||
|   "transferred_qty", | ||||
|   "requested_qty", | ||||
|   "status", | ||||
|   "column_break_20", | ||||
|   "remarks", | ||||
|   "barcode", | ||||
|   "job_started", | ||||
|   "started_time", | ||||
| @ -117,13 +128,6 @@ | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Timing Detail" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "employee", | ||||
|    "fieldtype": "Link", | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Employee", | ||||
|    "options": "Employee" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 1, | ||||
|    "fieldname": "time_logs", | ||||
| @ -133,9 +137,11 @@ | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_13", | ||||
|    "fieldtype": "Section Break" | ||||
|    "fieldtype": "Section Break", | ||||
|    "hide_border": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "total_completed_qty", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Total Completed Qty", | ||||
| @ -160,8 +166,7 @@ | ||||
|    "fieldname": "items", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "Items", | ||||
|    "options": "Job Card Item", | ||||
|    "read_only": 1 | ||||
|    "options": "Job Card Item" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
| @ -251,12 +256,7 @@ | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "employee.employee_name", | ||||
|    "fieldname": "employee_name", | ||||
|    "fieldtype": "Read Only", | ||||
|    "label": "Employee Name" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "production_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Production" | ||||
| @ -314,11 +314,89 @@ | ||||
|    "label": "Quality Inspection", | ||||
|    "no_copy": 1, | ||||
|    "options": "Quality Inspection" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 1, | ||||
|    "fieldname": "sub_operations", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "Sub Operations", | ||||
|    "options": "Job Card Operation", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "operation_section_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Operation Section" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_18", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_21", | ||||
|    "fieldtype": "Section Break", | ||||
|    "hide_border": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "is_corrective_job_card", | ||||
|    "fieldname": "hour_rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Hour Rate" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "depends_on": "is_corrective_job_card", | ||||
|    "fieldname": "corrective_operation_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Corrective Operation" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "is_corrective_job_card", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Is Corrective Job Card", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_33", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "for_job_card", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "For Job Card", | ||||
|    "options": "Job Card", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "for_job_card.operation", | ||||
|    "fetch_if_empty": 1, | ||||
|    "fieldname": "for_operation", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "For Operation", | ||||
|    "options": "Operation" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "employee", | ||||
|    "fieldtype": "Table MultiSelect", | ||||
|    "label": "Employee", | ||||
|    "options": "Job Card Time Log" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "serial_no", | ||||
|    "fieldtype": "Small Text", | ||||
|    "label": "Serial No" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "batch_no", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Batch No", | ||||
|    "options": "Batch" | ||||
|   } | ||||
|  ], | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-11-19 18:26:50.531664", | ||||
|  "modified": "2021-03-16 15:59:32.766484", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Job Card", | ||||
|  | ||||
| @ -5,11 +5,12 @@ | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| import datetime | ||||
| import json | ||||
| from frappe import _, bold | ||||
| from frappe.model.mapper import get_mapped_doc | ||||
| from frappe.model.document import Document | ||||
| from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, | ||||
| 	get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form) | ||||
| 	get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds) | ||||
| 
 | ||||
| from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations | ||||
| 
 | ||||
| @ -25,10 +26,21 @@ class JobCard(Document): | ||||
| 		self.set_status() | ||||
| 		self.validate_operation_id() | ||||
| 		self.validate_sequence_id() | ||||
| 		self.get_sub_operations() | ||||
| 		self.update_sub_operation_status() | ||||
| 
 | ||||
| 	def get_sub_operations(self): | ||||
| 		if self.operation: | ||||
| 			self.sub_operations = [] | ||||
| 			for row in frappe.get_all("Sub Operation", | ||||
| 				filters = {"parent": self.operation}, fields=["operation", "idx"]): | ||||
| 				row.status = "Pending" | ||||
| 				row.sub_operation = row.operation | ||||
| 				self.append("sub_operations", row) | ||||
| 
 | ||||
| 	def validate_time_logs(self): | ||||
| 		self.total_completed_qty = 0.0 | ||||
| 		self.total_time_in_mins = 0.0 | ||||
| 		self.total_completed_qty = 0.0 | ||||
| 
 | ||||
| 		if self.get('time_logs'): | ||||
| 			for d in self.get('time_logs'): | ||||
| @ -44,11 +56,14 @@ class JobCard(Document): | ||||
| 					d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 | ||||
| 					self.total_time_in_mins += d.time_in_mins | ||||
| 
 | ||||
| 				if d.completed_qty: | ||||
| 				if d.completed_qty and not self.sub_operations: | ||||
| 					self.total_completed_qty += d.completed_qty | ||||
| 
 | ||||
| 			self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) | ||||
| 
 | ||||
| 		for row in self.sub_operations: | ||||
| 			self.total_completed_qty += row.completed_qty | ||||
| 
 | ||||
| 	def get_overlap_for(self, args, check_next_available_slot=False): | ||||
| 		production_capacity = 1 | ||||
| 
 | ||||
| @ -57,7 +72,7 @@ class JobCard(Document): | ||||
| 				self.workstation, 'production_capacity') or 1 | ||||
| 			validate_overlap_for = " and jc.workstation = %(workstation)s " | ||||
| 
 | ||||
| 		if self.employee: | ||||
| 		if args.get("employee"): | ||||
| 			# override capacity for employee | ||||
| 			production_capacity = 1 | ||||
| 			validate_overlap_for = " and jc.employee = %(employee)s " | ||||
| @ -80,7 +95,7 @@ class JobCard(Document): | ||||
| 				"to_time": args.to_time, | ||||
| 				"name": args.name or "No Name", | ||||
| 				"parent": args.parent or "No Name", | ||||
| 				"employee": self.employee, | ||||
| 				"employee": args.get("employee"), | ||||
| 				"workstation": self.workstation | ||||
| 			}, as_dict=True) | ||||
| 
 | ||||
| @ -158,6 +173,100 @@ class JobCard(Document): | ||||
| 			row.planned_start_time = datetime.datetime.combine(start_date, | ||||
| 				get_time(workstation_doc.working_hours[0].start_time)) | ||||
| 
 | ||||
| 	def add_time_log(self, args): | ||||
| 		last_row = [] | ||||
| 		employees = args.employees | ||||
| 		if isinstance(employees, str): | ||||
| 			employees = json.loads(employees) | ||||
| 
 | ||||
| 		if self.time_logs and len(self.time_logs) > 0: | ||||
| 			last_row = self.time_logs[-1] | ||||
| 
 | ||||
| 		self.reset_timer_value(args) | ||||
| 		if last_row and args.get("complete_time"): | ||||
| 			for row in self.time_logs: | ||||
| 				if not row.to_time: | ||||
| 					row.update({ | ||||
| 						"to_time": get_datetime(args.get("complete_time")), | ||||
| 						"operation": args.get("sub_operation"), | ||||
| 						"completed_qty": args.get("completed_qty") or 0.0 | ||||
| 					}) | ||||
| 		elif args.get("start_time"): | ||||
| 			for name in employees: | ||||
| 				self.append("time_logs", { | ||||
| 					"from_time": get_datetime(args.get("start_time")), | ||||
| 					"employee": name.get('employee'), | ||||
| 					"operation": args.get("sub_operation"), | ||||
| 					"completed_qty": 0.0 | ||||
| 				}) | ||||
| 
 | ||||
| 		if not self.employee: | ||||
| 			self.set_employees(employees) | ||||
| 
 | ||||
| 		if self.status == "On Hold": | ||||
| 			self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) | ||||
| 
 | ||||
| 		self.save() | ||||
| 
 | ||||
| 	def set_employees(self, employees): | ||||
| 		for name in employees: | ||||
| 			self.append('employee', { | ||||
| 				'employee': name.get('employee'), | ||||
| 				'completed_qty': 0.0 | ||||
| 			}) | ||||
| 
 | ||||
| 	def reset_timer_value(self, args): | ||||
| 		self.started_time = None | ||||
| 
 | ||||
| 		if args.get("status") in ["Work In Progress", "Complete"]: | ||||
| 			self.current_time = 0.0 | ||||
| 
 | ||||
| 			if args.get("status") == "Work In Progress": | ||||
| 				self.started_time = get_datetime(args.get("start_time")) | ||||
| 
 | ||||
| 		if args.get("status") == "Resume Job": | ||||
| 			args["status"] = "Work In Progress" | ||||
| 
 | ||||
| 		if args.get("status"): | ||||
| 			self.status = args.get("status") | ||||
| 
 | ||||
| 	def update_sub_operation_status(self): | ||||
| 		if not (self.sub_operations and self.time_logs): | ||||
| 			return | ||||
| 
 | ||||
| 		operation_wise_completed_time = {} | ||||
| 		for time_log in self.time_logs: | ||||
| 			if time_log.operation not in operation_wise_completed_time: | ||||
| 				operation_wise_completed_time.setdefault(time_log.operation, | ||||
| 					frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []})) | ||||
| 
 | ||||
| 			op_row = operation_wise_completed_time[time_log.operation] | ||||
| 			op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" | ||||
| 			if self.status == 'On Hold': | ||||
| 				op_row.status = 'Pause' | ||||
| 
 | ||||
| 			op_row.employee.append(time_log.employee) | ||||
| 			if time_log.time_in_mins: | ||||
| 				op_row.completed_time += time_log.time_in_mins | ||||
| 				op_row.completed_qty += time_log.completed_qty | ||||
| 
 | ||||
| 		for row in self.sub_operations: | ||||
| 			operation_deatils = operation_wise_completed_time.get(row.sub_operation) | ||||
| 			if operation_deatils: | ||||
| 				if row.status != 'Complete': | ||||
| 					row.status = operation_deatils.status | ||||
| 
 | ||||
| 				row.completed_time = operation_deatils.completed_time | ||||
| 				if operation_deatils.employee: | ||||
| 					row.completed_time = row.completed_time / len(set(operation_deatils.employee)) | ||||
| 
 | ||||
| 					if operation_deatils.completed_qty: | ||||
| 						row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee)) | ||||
| 			else: | ||||
| 				row.status = 'Pending' | ||||
| 				row.completed_time = 0.0 | ||||
| 				row.completed_qty = 0.0 | ||||
| 
 | ||||
| 	def update_time_logs(self, row): | ||||
| 		self.append("time_logs", { | ||||
| 			"from_time": row.planned_start_time, | ||||
| @ -182,15 +291,18 @@ class JobCard(Document): | ||||
| 
 | ||||
| 			if self.get('operation') == d.operation: | ||||
| 				self.append('items', { | ||||
| 					'item_code': d.item_code, | ||||
| 					'source_warehouse': d.source_warehouse, | ||||
| 					'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'), | ||||
| 					'item_name': d.item_name, | ||||
| 					'description': d.description, | ||||
| 					'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty | ||||
| 					"item_code": d.item_code, | ||||
| 					"source_warehouse": d.source_warehouse, | ||||
| 					"uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'), | ||||
| 					"item_name": d.item_name, | ||||
| 					"description": d.description, | ||||
| 					"required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty, | ||||
| 					"rate": d.rate, | ||||
| 					"amount": d.amount | ||||
| 				}) | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		self.validate_transfer_qty() | ||||
| 		self.validate_job_card() | ||||
| 		self.update_work_order() | ||||
| 		self.set_transferred_qty() | ||||
| @ -199,7 +311,16 @@ class JobCard(Document): | ||||
| 		self.update_work_order() | ||||
| 		self.set_transferred_qty() | ||||
| 
 | ||||
| 	def validate_transfer_qty(self): | ||||
| 		if self.items and self.transferred_qty < self.for_quantity: | ||||
| 			frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}') | ||||
| 				.format(self.name)) | ||||
| 
 | ||||
| 	def validate_job_card(self): | ||||
| 		if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped': | ||||
| 			frappe.throw(_("Transaction not allowed against stopped Work Order {0}") | ||||
| 				.format(get_link_to_form('Work Order', self.work_order))) | ||||
| 
 | ||||
| 		if not self.time_logs: | ||||
| 			frappe.throw(_("Time logs are required for {0} {1}") | ||||
| 				.format(bold("Job Card"), get_link_to_form("Job Card", self.name))) | ||||
| @ -215,6 +336,10 @@ class JobCard(Document): | ||||
| 		if not self.work_order: | ||||
| 			return | ||||
| 
 | ||||
| 		if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings', | ||||
| 			'add_corrective_operation_cost_in_finished_good_valuation')): | ||||
| 			return | ||||
| 
 | ||||
| 		for_quantity, time_in_mins = 0, 0 | ||||
| 		from_time_list, to_time_list = [], [] | ||||
| 
 | ||||
| @ -225,10 +350,24 @@ class JobCard(Document): | ||||
| 			time_in_mins = flt(data[0].time_in_mins) | ||||
| 
 | ||||
| 		wo = frappe.get_doc('Work Order', self.work_order) | ||||
| 		if self.operation_id: | ||||
| 
 | ||||
| 		if self.is_corrective_job_card: | ||||
| 			self.update_corrective_in_work_order(wo) | ||||
| 
 | ||||
| 		elif self.operation_id: | ||||
| 			self.validate_produced_quantity(for_quantity, wo) | ||||
| 			self.update_work_order_data(for_quantity, time_in_mins, wo) | ||||
| 
 | ||||
| 	def update_corrective_in_work_order(self, wo): | ||||
| 		wo.corrective_operation_cost = 0.0 | ||||
| 		for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'], | ||||
| 			filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}): | ||||
| 			wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) | ||||
| 
 | ||||
| 		wo.calculate_operating_cost() | ||||
| 		wo.flags.ignore_validate_update_after_submit = True | ||||
| 		wo.save() | ||||
| 
 | ||||
| 	def validate_produced_quantity(self, for_quantity, wo): | ||||
| 		if self.docstatus < 2: return | ||||
| 
 | ||||
| @ -248,8 +387,8 @@ class JobCard(Document): | ||||
| 					min(from_time) as start_time, max(to_time) as end_time | ||||
| 				FROM `tabJob Card` jc, `tabJob Card Time Log` jctl | ||||
| 				WHERE | ||||
| 					jctl.parent = jc.name and jc.work_order = %s | ||||
| 					and jc.operation_id = %s and jc.docstatus = 1 | ||||
| 					jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s | ||||
| 					and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0 | ||||
| 			""", (self.work_order, self.operation_id), as_dict=1) | ||||
| 
 | ||||
| 		for data in wo.operations: | ||||
| @ -271,7 +410,8 @@ class JobCard(Document): | ||||
| 	def get_current_operation_data(self): | ||||
| 		return frappe.get_all('Job Card', | ||||
| 			fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], | ||||
| 			filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) | ||||
| 			filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id, | ||||
| 				"is_corrective_job_card": 0}) | ||||
| 
 | ||||
| 	def set_transferred_qty_in_job_card(self, ste_doc): | ||||
| 		for row in ste_doc.items: | ||||
| @ -354,7 +494,11 @@ class JobCard(Document): | ||||
| 				.format(bold(self.operation), work_order), OperationMismatchError) | ||||
| 
 | ||||
| 	def validate_sequence_id(self): | ||||
| 		if not (self.work_order and self.sequence_id): return | ||||
| 		if self.is_corrective_job_card: | ||||
| 			return | ||||
| 
 | ||||
| 		if not (self.work_order and self.sequence_id): | ||||
| 			return | ||||
| 
 | ||||
| 		current_operation_qty = 0.0 | ||||
| 		data = self.get_current_operation_data() | ||||
| @ -376,6 +520,17 @@ class JobCard(Document): | ||||
| 				frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") | ||||
| 					.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_time_log(args): | ||||
| 	if isinstance(args, str): | ||||
| 		args = json.loads(args) | ||||
| 
 | ||||
| 	args = frappe._dict(args) | ||||
| 	doc = frappe.get_doc("Job Card", args.job_card_id) | ||||
| 	doc.validate_sequence_id() | ||||
| 	doc.add_time_log(args) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_operation_details(work_order, operation): | ||||
| 	if work_order and operation: | ||||
| @ -511,3 +666,28 @@ def get_job_details(start, end, filters=None): | ||||
| 			events.append(job_card_data) | ||||
| 
 | ||||
| 	return events | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None): | ||||
| 	def set_missing_values(source, target): | ||||
| 		target.is_corrective_job_card = 1 | ||||
| 		target.operation = operation | ||||
| 		target.for_operation = for_operation | ||||
| 
 | ||||
| 		target.set('time_logs', []) | ||||
| 		target.set('employee', []) | ||||
| 		target.set('items', []) | ||||
| 		target.get_sub_operations() | ||||
| 		target.get_required_items() | ||||
| 		target.validate_time_logs() | ||||
| 
 | ||||
| 	doclist = get_mapped_doc("Job Card", source_name, { | ||||
| 		"Job Card": { | ||||
| 			"doctype": "Job Card", | ||||
| 			"field_map": { | ||||
| 				"name": "for_job_card", | ||||
| 			}, | ||||
| 		} | ||||
| 	}, target_doc, set_missing_values) | ||||
| 
 | ||||
| 	return doclist | ||||
| @ -25,8 +25,7 @@ | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Item Code", | ||||
|    "options": "Item", | ||||
|    "read_only": 1 | ||||
|    "options": "Item" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "source_warehouse", | ||||
| @ -67,8 +66,7 @@ | ||||
|    "fieldname": "required_qty", | ||||
|    "fieldtype": "Float", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Required Qty", | ||||
|    "read_only": 1 | ||||
|    "label": "Required Qty" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_9", | ||||
| @ -107,7 +105,7 @@ | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-02-11 13:50:13.804108", | ||||
|  "modified": "2021-04-22 18:50:00.003444", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Job Card Item", | ||||
|  | ||||
| @ -0,0 +1,59 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2020-12-07 16:58:38.449041", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "sub_operation", | ||||
|   "completed_time", | ||||
|   "status", | ||||
|   "completed_qty" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "default": "Pending", | ||||
|    "fieldname": "status", | ||||
|    "fieldtype": "Select", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Status", | ||||
|    "options": "Complete\nPause\nPending\nWork In Progress", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "description": "In mins", | ||||
|    "fieldname": "completed_time", | ||||
|    "fieldtype": "Data", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Completed Time", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "sub_operation", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Operation", | ||||
|    "options": "Operation", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "completed_qty", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Completed Qty", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-03-16 18:24:35.399593", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Job Card Operation", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class JobCardOperation(Document): | ||||
| 	pass | ||||
| @ -1,14 +1,17 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2019-03-08 23:56:43.187569", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "employee", | ||||
|   "from_time", | ||||
|   "to_time", | ||||
|   "column_break_2", | ||||
|   "time_in_mins", | ||||
|   "completed_qty" | ||||
|   "completed_qty", | ||||
|   "operation" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
| @ -41,10 +44,27 @@ | ||||
|    "in_list_view": 1, | ||||
|    "label": "Completed Qty", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "employee", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Employee", | ||||
|    "options": "Employee" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "operation", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Operation", | ||||
|    "no_copy": 1, | ||||
|    "options": "Operation", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "modified": "2019-12-03 12:56:02.285448", | ||||
|  "links": [], | ||||
|  "modified": "2020-12-23 14:30:00.970916", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Job Card Time Log", | ||||
|  | ||||
| @ -26,7 +26,10 @@ | ||||
|   "column_break_16", | ||||
|   "overproduction_percentage_for_work_order", | ||||
|   "other_settings_section", | ||||
|   "update_bom_costs_automatically" | ||||
|   "update_bom_costs_automatically", | ||||
|   "add_corrective_operation_cost_in_finished_good_valuation", | ||||
|   "column_break_23", | ||||
|   "make_serial_no_batch_from_work_order" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
| @ -155,13 +158,30 @@ | ||||
|   { | ||||
|    "fieldname": "column_break_5", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_23", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order", | ||||
|    "fieldname": "make_serial_no_batch_from_work_order", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Make Serial No / Batch from Work Order" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "add_corrective_operation_cost_in_finished_good_valuation", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Add Corrective Operation Cost in Finished Good Valuation" | ||||
|   } | ||||
|  ], | ||||
|  "icon": "icon-wrench", | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "issingle": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-10-13 10:55:43.996581", | ||||
|  "modified": "2021-03-16 15:54:38.967341", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Manufacturing Settings", | ||||
| @ -178,4 +198,4 @@ | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| } | ||||
| @ -2,7 +2,13 @@ | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('Operation', { | ||||
| 	refresh: function(frm) { | ||||
| 
 | ||||
| 	setup: function(frm) { | ||||
| 		frm.set_query('operation', 'sub_operations', function() { | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					'name': ['not in', [frm.doc.name]] | ||||
| 				} | ||||
| 			}; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| }); | ||||
| @ -1,167 +1,132 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_import": 1,  | ||||
|  "allow_rename": 1,  | ||||
|  "autoname": "Prompt",  | ||||
|  "beta": 0,  | ||||
|  "creation": "2014-11-07 16:20:30.683186",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "Setup",  | ||||
|  "editable_grid": 0,  | ||||
|  "engine": "InnoDB",  | ||||
|  "actions": [], | ||||
|  "allow_import": 1, | ||||
|  "allow_rename": 1, | ||||
|  "autoname": "Prompt", | ||||
|  "creation": "2014-11-07 16:20:30.683186", | ||||
|  "doctype": "DocType", | ||||
|  "document_type": "Setup", | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "workstation", | ||||
|   "data_2", | ||||
|   "is_corrective_operation", | ||||
|   "job_card_section", | ||||
|   "create_job_card_based_on_batch_size", | ||||
|   "column_break_6", | ||||
|   "batch_size", | ||||
|   "sub_operations_section", | ||||
|   "sub_operations", | ||||
|   "total_operation_time", | ||||
|   "section_break_4", | ||||
|   "description" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "workstation",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "in_standard_filter": 1,  | ||||
|    "label": "Default Workstation",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Workstation",  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "workstation", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Default Workstation", | ||||
|    "options": "Workstation" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "section_break_4",  | ||||
|    "fieldtype": "Section Break",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "section_break_4", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Operation Description" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "description",  | ||||
|    "fieldtype": "Text",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Description",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|    "fieldname": "description", | ||||
|    "fieldtype": "Text", | ||||
|    "label": "Description" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "sub_operations_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Sub Operations" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "sub_operations", | ||||
|    "fieldtype": "Table", | ||||
|    "options": "Sub Operation" | ||||
|   }, | ||||
|   { | ||||
|    "description": "Time in mins.", | ||||
|    "fieldname": "total_operation_time", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Total Operation Time", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "data_2", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "1", | ||||
|    "depends_on": "create_job_card_based_on_batch_size", | ||||
|    "fieldname": "batch_size", | ||||
|    "fieldtype": "Int", | ||||
|    "label": "Batch Size", | ||||
|    "mandatory_depends_on": "create_job_card_based_on_batch_size" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "create_job_card_based_on_batch_size", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Create Job Card based on Batch Size" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "job_card_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Job Card" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_6", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "is_corrective_operation", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Is Corrective Operation" | ||||
|   } | ||||
|  ],  | ||||
|  "hide_heading": 0,  | ||||
|  "hide_toolbar": 0,  | ||||
|  "icon": "fa fa-wrench",  | ||||
|  "idx": 0,  | ||||
|  "image_view": 0,  | ||||
|  "in_create": 0,  | ||||
| 
 | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 0,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2016-11-07 05:28:27.462413",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Manufacturing",  | ||||
|  "name": "Operation",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  ], | ||||
|  "icon": "fa fa-wrench", | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-01-12 15:09:23.593338", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Operation", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "amend": 0,  | ||||
|    "apply_user_permissions": 0,  | ||||
|    "cancel": 0,  | ||||
|    "create": 1,  | ||||
|    "delete": 1,  | ||||
|    "email": 0,  | ||||
|    "export": 1,  | ||||
|    "if_owner": 0,  | ||||
|    "import": 1,  | ||||
|    "is_custom": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print": 0,  | ||||
|    "read": 1,  | ||||
|    "report": 0,  | ||||
|    "role": "Manufacturing User",  | ||||
|    "set_user_permissions": 0,  | ||||
|    "share": 1,  | ||||
|    "submit": 0,  | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "export": 1, | ||||
|    "import": 1, | ||||
|    "read": 1, | ||||
|    "role": "Manufacturing User", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   },  | ||||
|   }, | ||||
|   { | ||||
|    "amend": 0,  | ||||
|    "apply_user_permissions": 0,  | ||||
|    "cancel": 0,  | ||||
|    "create": 1,  | ||||
|    "delete": 1,  | ||||
|    "email": 0,  | ||||
|    "export": 1,  | ||||
|    "if_owner": 0,  | ||||
|    "import": 1,  | ||||
|    "is_custom": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print": 0,  | ||||
|    "read": 1,  | ||||
|    "report": 1,  | ||||
|    "role": "Manufacturing Manager",  | ||||
|    "set_user_permissions": 0,  | ||||
|    "share": 1,  | ||||
|    "submit": 0,  | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "export": 1, | ||||
|    "import": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Manufacturing Manager", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ],  | ||||
|  "quick_entry": 1,  | ||||
|  "read_only": 0,  | ||||
|  "read_only_onload": 0,  | ||||
|  "sort_field": "modified",  | ||||
|  "sort_order": "DESC",  | ||||
|  "track_seen": 0 | ||||
|  ], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -2,9 +2,34 @@ | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class Operation(Document): | ||||
| 	def validate(self): | ||||
| 		if not self.description: | ||||
| 			self.description = self.name | ||||
| 
 | ||||
| 		self.duplicate_sub_operation() | ||||
| 		self.set_total_time() | ||||
| 
 | ||||
| 	def duplicate_sub_operation(self): | ||||
| 		operation_list = [] | ||||
| 		for row in self.sub_operations: | ||||
| 			if row.operation in operation_list: | ||||
| 				frappe.throw(_("The operation {0} can not add multiple times") | ||||
| 					.format(frappe.bold(row.operation))) | ||||
| 
 | ||||
| 			if self.name == row.operation: | ||||
| 				frappe.throw(_("The operation {0} can not be the sub operation") | ||||
| 					.format(frappe.bold(row.operation))) | ||||
| 
 | ||||
| 			operation_list.append(row.operation) | ||||
| 
 | ||||
| 	def set_total_time(self): | ||||
| 		self.total_operation_time = 0.0 | ||||
| 
 | ||||
| 		for row in self.sub_operations: | ||||
| 			self.total_operation_time += row.time_in_mins | ||||
|  | ||||
| @ -0,0 +1,8 @@ | ||||
| // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('Sub Operation', { | ||||
| 	// refresh: function(frm) {
 | ||||
| 
 | ||||
| 	// }
 | ||||
| }); | ||||
| @ -0,0 +1,51 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2020-12-07 15:39:47.488519", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "operation", | ||||
|   "time_in_mins", | ||||
|   "column_break_5", | ||||
|   "description" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "operation", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Operation", | ||||
|    "options": "Operation" | ||||
|   }, | ||||
|   { | ||||
|    "description": "Time in mins", | ||||
|    "fieldname": "time_in_mins", | ||||
|    "fieldtype": "Float", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Operation Time" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_5", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "description", | ||||
|    "fieldtype": "Small Text", | ||||
|    "label": "Description" | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-12-07 18:09:18.005578", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Sub Operation", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
							
								
								
									
										10
									
								
								erpnext/manufacturing/doctype/sub_operation/sub_operation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								erpnext/manufacturing/doctype/sub_operation/sub_operation.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class SubOperation(Document): | ||||
| 	pass | ||||
| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| # import frappe | ||||
| import unittest | ||||
| 
 | ||||
| class TestSubOperation(unittest.TestCase): | ||||
| 	pass | ||||
| @ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase): | ||||
| 		ste.submit() | ||||
| 		stock_entries.append(ste) | ||||
| 
 | ||||
| 		job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) | ||||
| 		job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc') | ||||
| 		self.assertEqual(len(job_cards), len(bom.operations)) | ||||
| 
 | ||||
| 		for i, job_card in enumerate(job_cards): | ||||
| 			doc = frappe.get_doc("Job Card", job_card) | ||||
| 			doc.append("time_logs", { | ||||
| 				"from_time": add_to_date(None, i), | ||||
| 				"hours": 1, | ||||
| 				"to_time": add_to_date(None, i + 1), | ||||
| 				"completed_qty": doc.for_quantity | ||||
| 			}) | ||||
| 			doc.time_logs[0].completed_qty = 1 | ||||
| 			doc.submit() | ||||
| 
 | ||||
| 		ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) | ||||
|  | ||||
| @ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", { | ||||
| 		} | ||||
| 
 | ||||
| 		if (frm.doc.docstatus === 1 | ||||
| 			&& frm.doc.operations && frm.doc.operations.length | ||||
| 			&& frm.doc.qty != frm.doc.material_transferred_for_manufacturing) { | ||||
| 			&& frm.doc.operations && frm.doc.operations.length) { | ||||
| 
 | ||||
| 			const not_completed = frm.doc.operations.filter(d => { | ||||
| 				if(d.status != 'Completed') { | ||||
| @ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", { | ||||
| 		const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), | ||||
| 			fields: [ | ||||
| 				{ | ||||
| 					fieldtype:'Link', | ||||
| 					fieldname:'operation', | ||||
| 					fieldtype: 'Link', | ||||
| 					fieldname: 'operation', | ||||
| 					label: __('Operation'), | ||||
| 					read_only:1, | ||||
| 					in_list_view:1 | ||||
| 					read_only: 1, | ||||
| 					in_list_view: 1 | ||||
| 				}, | ||||
| 				{ | ||||
| 					fieldtype:'Link', | ||||
| 					fieldname:'workstation', | ||||
| 					fieldtype: 'Link', | ||||
| 					fieldname: 'workstation', | ||||
| 					label: __('Workstation'), | ||||
| 					read_only:1, | ||||
| 					in_list_view:1 | ||||
| 					read_only: 1, | ||||
| 					in_list_view: 1 | ||||
| 				}, | ||||
| 				{ | ||||
| 					fieldtype:'Data', | ||||
| 					fieldname:'name', | ||||
| 					fieldtype: 'Data', | ||||
| 					fieldname: 'name', | ||||
| 					label: __('Operation Id') | ||||
| 				}, | ||||
| 				{ | ||||
| 					fieldtype:'Float', | ||||
| 					fieldname:'pending_qty', | ||||
| 					fieldtype: 'Float', | ||||
| 					fieldname: 'pending_qty', | ||||
| 					label: __('Pending Qty'), | ||||
| 				}, | ||||
| 				{ | ||||
| 					fieldtype:'Float', | ||||
| 					fieldname:'qty', | ||||
| 					fieldtype: 'Float', | ||||
| 					fieldname: 'qty', | ||||
| 					label: __('Quantity to Manufacture'), | ||||
| 					read_only:0, | ||||
| 					in_list_view:1, | ||||
| 					read_only: 0, | ||||
| 					in_list_view: 1, | ||||
| 				}, | ||||
| 				{ | ||||
| 					fieldtype: 'Float', | ||||
| 					fieldname: 'batch_size', | ||||
| 					label: __('Batch Size'), | ||||
| 					read_only: 1 | ||||
| 				}, | ||||
| 			], | ||||
| 			data: operations_data, | ||||
| @ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", { | ||||
| 		}, function(data) { | ||||
| 			frappe.call({ | ||||
| 				method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", | ||||
| 				freeze: true, | ||||
| 				args: { | ||||
| 					work_order: frm.doc.name, | ||||
| 					operations: data.operations, | ||||
| 				}, | ||||
| 				callback: function() { | ||||
| 					frm.reload_doc(); | ||||
| 				} | ||||
| 			}); | ||||
| 		}, __("Job Card"), __("Create")); | ||||
| @ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", { | ||||
| 			if(data.completed_qty != frm.doc.qty) { | ||||
| 				pending_qty = frm.doc.qty - flt(data.completed_qty); | ||||
| 
 | ||||
| 				dialog.fields_dict.operations.df.data.push({ | ||||
| 					'name': data.name, | ||||
| 					'operation': data.operation, | ||||
| 					'workstation': data.workstation, | ||||
| 					'qty': pending_qty, | ||||
| 					'pending_qty': pending_qty, | ||||
| 				}); | ||||
| 				if (pending_qty) { | ||||
| 					dialog.fields_dict.operations.df.data.push({ | ||||
| 						'name': data.name, | ||||
| 						'operation': data.operation, | ||||
| 						'workstation': data.workstation, | ||||
| 						'batch_size': data.batch_size, | ||||
| 						'qty': pending_qty, | ||||
| 						'pending_qty': pending_qty | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 		dialog.fields_dict.operations.grid.refresh(); | ||||
|  | ||||
| @ -21,6 +21,12 @@ | ||||
|   "produced_qty", | ||||
|   "sales_order", | ||||
|   "project", | ||||
|   "serial_no_and_batch_for_finished_good_section", | ||||
|   "has_serial_no", | ||||
|   "has_batch_no", | ||||
|   "column_break_17", | ||||
|   "serial_no", | ||||
|   "batch_size", | ||||
|   "settings_section", | ||||
|   "allow_alternative_item", | ||||
|   "use_multi_level_bom", | ||||
| @ -52,6 +58,7 @@ | ||||
|   "actual_operating_cost", | ||||
|   "additional_operating_cost", | ||||
|   "column_break_24", | ||||
|   "corrective_operation_cost", | ||||
|   "total_operating_cost", | ||||
|   "more_info", | ||||
|   "description", | ||||
| @ -488,6 +495,57 @@ | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Lead Time", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "serial_no_and_batch_for_finished_good_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Serial No and Batch for Finished Good" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_17", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fetch_from": "production_item.has_serial_no", | ||||
|    "fieldname": "has_serial_no", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Has Serial No", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fetch_from": "production_item.has_batch_no", | ||||
|    "fieldname": "has_batch_no", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Has Batch No", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "has_serial_no", | ||||
|    "fieldname": "serial_no", | ||||
|    "fieldtype": "Small Text", | ||||
|    "label": "Serial Nos", | ||||
|    "no_copy": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "depends_on": "has_batch_no", | ||||
|    "fieldname": "batch_size", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Batch Size" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "description": "From Corrective Job Card", | ||||
|    "fieldname": "corrective_operation_cost", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Corrective Operation Cost", | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-cogs", | ||||
| @ -495,7 +553,7 @@ | ||||
|  "image_field": "image", | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-03-16 13:27:51.116484", | ||||
|  "modified": "2021-06-20 15:19:14.902699", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Work Order", | ||||
|  | ||||
| @ -18,14 +18,16 @@ from frappe.utils.csvutils import getlink | ||||
| from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty | ||||
| from erpnext.utilities.transaction_base import validate_uom_is_integer | ||||
| from frappe.model.mapper import get_mapped_doc | ||||
| from erpnext.stock.doctype.batch.batch import make_batch | ||||
| from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos | ||||
| 
 | ||||
| class OverProductionError(frappe.ValidationError): pass | ||||
| class CapacityError(frappe.ValidationError): pass | ||||
| class StockOverProductionError(frappe.ValidationError): pass | ||||
| class OperationTooLongError(frappe.ValidationError): pass | ||||
| class ItemHasVariantError(frappe.ValidationError): pass | ||||
| 
 | ||||
| from six import string_types | ||||
| class SerialNoQtyError(frappe.ValidationError): | ||||
| 	pass | ||||
| 
 | ||||
| 
 | ||||
| class WorkOrder(Document): | ||||
| @ -123,7 +125,9 @@ class WorkOrder(Document): | ||||
| 
 | ||||
| 		variable_cost = self.actual_operating_cost if self.actual_operating_cost \ | ||||
| 			else self.planned_operating_cost | ||||
| 		self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost) | ||||
| 
 | ||||
| 		self.total_operating_cost = (flt(self.additional_operating_cost) | ||||
| 			+ flt(variable_cost) + flt(self.corrective_operation_cost)) | ||||
| 
 | ||||
| 	def validate_work_order_against_so(self): | ||||
| 		# already ordered qty | ||||
| @ -231,12 +235,15 @@ class WorkOrder(Document): | ||||
| 
 | ||||
| 		production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) | ||||
| 
 | ||||
| 	def before_submit(self): | ||||
| 		self.create_serial_no_batch_no() | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		if not self.wip_warehouse: | ||||
| 			frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) | ||||
| 		if not self.fg_warehouse: | ||||
| 			frappe.throw(_("For Warehouse is required before Submit")) | ||||
| 		 | ||||
| 
 | ||||
| 		if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): | ||||
| 			self.update_work_order_qty_in_combined_so() | ||||
| 		else: | ||||
| @ -256,12 +263,76 @@ class WorkOrder(Document): | ||||
| 			self.update_work_order_qty_in_combined_so() | ||||
| 		else: | ||||
| 			self.update_work_order_qty_in_so() | ||||
| 			 | ||||
| 
 | ||||
| 		self.delete_job_card() | ||||
| 		self.update_completed_qty_in_material_request() | ||||
| 		self.update_planned_qty() | ||||
| 		self.update_ordered_qty() | ||||
| 		self.update_reserved_qty_for_production() | ||||
| 		self.delete_auto_created_batch_and_serial_no() | ||||
| 
 | ||||
| 	def create_serial_no_batch_no(self): | ||||
| 		if not (self.has_serial_no or self.has_batch_no): | ||||
| 			return | ||||
| 
 | ||||
| 		if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): | ||||
| 			return | ||||
| 
 | ||||
| 		if self.has_batch_no: | ||||
| 			self.create_batch_for_finished_good() | ||||
| 
 | ||||
| 		args = { | ||||
| 			"item_code": self.production_item, | ||||
| 			"work_order": self.name | ||||
| 		} | ||||
| 
 | ||||
| 		if self.has_serial_no: | ||||
| 			self.make_serial_nos(args) | ||||
| 
 | ||||
| 	def create_batch_for_finished_good(self): | ||||
| 		total_qty = self.qty | ||||
| 		if not self.batch_size: | ||||
| 			self.batch_size = total_qty | ||||
| 
 | ||||
| 		while total_qty > 0: | ||||
| 			qty = self.batch_size | ||||
| 			if self.batch_size >= total_qty: | ||||
| 				qty = total_qty | ||||
| 
 | ||||
| 			if total_qty > self.batch_size: | ||||
| 				total_qty -= self.batch_size | ||||
| 			else: | ||||
| 				qty = total_qty | ||||
| 				total_qty = 0 | ||||
| 
 | ||||
| 			make_batch(frappe._dict({ | ||||
| 				"item": self.production_item, | ||||
| 				"qty_to_produce": qty, | ||||
| 				"reference_doctype": self.doctype, | ||||
| 				"reference_name": self.name | ||||
| 			})) | ||||
| 
 | ||||
| 	def delete_auto_created_batch_and_serial_no(self): | ||||
| 		for row in frappe.get_all("Serial No", filters = {"work_order": self.name}): | ||||
| 			frappe.delete_doc("Serial No", row.name) | ||||
| 			self.db_set("serial_no", "") | ||||
| 
 | ||||
| 		for row in frappe.get_all("Batch", filters = {"reference_name": self.name}): | ||||
| 			frappe.delete_doc("Batch", row.name) | ||||
| 
 | ||||
| 	def make_serial_nos(self, args): | ||||
| 		serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") | ||||
| 		if serial_no_series: | ||||
| 			self.serial_no = get_auto_serial_nos(serial_no_series, self.qty) | ||||
| 
 | ||||
| 		if self.serial_no: | ||||
| 			args.update({"serial_no": self.serial_no, "actual_qty": self.qty}) | ||||
| 			auto_make_serial_nos(args) | ||||
| 
 | ||||
| 		serial_nos_length = len(get_serial_nos(self.serial_no)) | ||||
| 		if serial_nos_length != self.qty: | ||||
| 			frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.") | ||||
| 				.format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError) | ||||
| 
 | ||||
| 	def create_job_card(self): | ||||
| 		manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") | ||||
| @ -269,32 +340,40 @@ class WorkOrder(Document): | ||||
| 		enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) | ||||
| 		plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 | ||||
| 
 | ||||
| 		for i, row in enumerate(self.operations): | ||||
| 			self.set_operation_start_end_time(i, row) | ||||
| 
 | ||||
| 			if not row.workstation: | ||||
| 				frappe.throw(_("Row {0}: select the workstation against the operation {1}") | ||||
| 					.format(row.idx, row.operation)) | ||||
| 
 | ||||
| 			original_start_time = row.planned_start_time | ||||
| 			job_card_doc = create_job_card(self, row, | ||||
| 				enable_capacity_planning=enable_capacity_planning, auto_create=True) | ||||
| 
 | ||||
| 			if enable_capacity_planning and job_card_doc: | ||||
| 				row.planned_start_time = job_card_doc.time_logs[-1].from_time | ||||
| 				row.planned_end_time = job_card_doc.time_logs[-1].to_time | ||||
| 
 | ||||
| 				if date_diff(row.planned_start_time, original_start_time) > plan_days: | ||||
| 					frappe.message_log.pop() | ||||
| 					frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") | ||||
| 						.format(plan_days, row.operation), CapacityError) | ||||
| 
 | ||||
| 				row.db_update() | ||||
| 		for index, row in enumerate(self.operations): | ||||
| 			qty = self.qty | ||||
| 			while qty > 0: | ||||
| 				qty = split_qty_based_on_batch_size(self, row, qty) | ||||
| 				if row.job_card_qty > 0: | ||||
| 					self.prepare_data_for_job_card(row, index, | ||||
| 						plan_days, enable_capacity_planning) | ||||
| 
 | ||||
| 		planned_end_date = self.operations and self.operations[-1].planned_end_time | ||||
| 		if planned_end_date: | ||||
| 			self.db_set("planned_end_date", planned_end_date) | ||||
| 
 | ||||
| 	def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): | ||||
| 		self.set_operation_start_end_time(index, row) | ||||
| 
 | ||||
| 		if not row.workstation: | ||||
| 			frappe.throw(_("Row {0}: select the workstation against the operation {1}") | ||||
| 				.format(row.idx, row.operation)) | ||||
| 
 | ||||
| 		original_start_time = row.planned_start_time | ||||
| 		job_card_doc = create_job_card(self, row, auto_create=True, | ||||
| 			enable_capacity_planning=enable_capacity_planning) | ||||
| 
 | ||||
| 		if enable_capacity_planning and job_card_doc: | ||||
| 			row.planned_start_time = job_card_doc.time_logs[-1].from_time | ||||
| 			row.planned_end_time = job_card_doc.time_logs[-1].to_time | ||||
| 
 | ||||
| 			if date_diff(row.planned_start_time, original_start_time) > plan_days: | ||||
| 				frappe.message_log.pop() | ||||
| 				frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") | ||||
| 					.format(plan_days, row.operation), CapacityError) | ||||
| 
 | ||||
| 			row.db_update() | ||||
| 
 | ||||
| 	def set_operation_start_end_time(self, idx, row): | ||||
| 		"""Set start and end time for given operation. If first operation, set start as | ||||
| 		`planned_start_date`, else add time diff to end time of earlier operation.""" | ||||
| @ -361,7 +440,7 @@ class WorkOrder(Document): | ||||
| 		work_order_qty = qty[0][0] if qty and qty[0][0] else 0 | ||||
| 		frappe.db.set_value('Sales Order Item', | ||||
| 			self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) | ||||
| 		 | ||||
| 
 | ||||
| 	def update_work_order_qty_in_combined_so(self): | ||||
| 		total_bundle_qty = 1 | ||||
| 		if self.product_bundle_item: | ||||
| @ -374,7 +453,7 @@ class WorkOrder(Document): | ||||
| 
 | ||||
| 		prod_plan = frappe.get_doc('Production Plan', self.production_plan) | ||||
| 		item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item') | ||||
| 		 | ||||
| 
 | ||||
| 		for plan_reference in prod_plan.prod_plan_references: | ||||
| 			work_order_qty = 0.0 | ||||
| 			if plan_reference.item_reference == item_reference: | ||||
| @ -382,7 +461,7 @@ class WorkOrder(Document): | ||||
| 					work_order_qty = flt(plan_reference.qty) / total_bundle_qty | ||||
| 				frappe.db.set_value('Sales Order Item', | ||||
| 					plan_reference.sales_order_item, 'work_order_qty', work_order_qty) | ||||
| 	 | ||||
| 
 | ||||
| 	def update_completed_qty_in_material_request(self): | ||||
| 		if self.material_request: | ||||
| 			frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) | ||||
| @ -666,6 +745,17 @@ class WorkOrder(Document): | ||||
| 		bom.set_bom_material_details() | ||||
| 		return bom | ||||
| 
 | ||||
| 	def update_batch_produced_qty(self, stock_entry_doc): | ||||
| 		if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): | ||||
| 			return | ||||
| 
 | ||||
| 		for row in stock_entry_doc.items: | ||||
| 			if row.batch_no and (row.is_finished_item or row.is_scrap_item): | ||||
| 				qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1}, | ||||
| 					or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0] | ||||
| 
 | ||||
| 				frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty)) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| @frappe.validate_and_sanitize_search_inputs | ||||
| def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): | ||||
| @ -743,7 +833,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): | ||||
| 	return wo_doc | ||||
| 
 | ||||
| def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): | ||||
| 	if isinstance(variant_items, string_types): | ||||
| 	if isinstance(variant_items, str): | ||||
| 		variant_items = json.loads(variant_items) | ||||
| 
 | ||||
| 	for item in variant_items: | ||||
| @ -823,6 +913,7 @@ def make_stock_entry(work_order_id, purpose, qty=None): | ||||
| 
 | ||||
| 	stock_entry.set_stock_entry_type() | ||||
| 	stock_entry.get_items() | ||||
| 	stock_entry.set_serial_no_batch_for_finished_good() | ||||
| 	return stock_entry.as_dict() | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| @ -864,13 +955,47 @@ def query_sales_order(production_item): | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_job_card(work_order, operations): | ||||
| 	if isinstance(operations, string_types): | ||||
| 	if isinstance(operations, str): | ||||
| 		operations = json.loads(operations) | ||||
| 
 | ||||
| 	work_order = frappe.get_doc('Work Order', work_order) | ||||
| 	for row in operations: | ||||
| 		row = frappe._dict(row) | ||||
| 		validate_operation_data(row) | ||||
| 		create_job_card(work_order, row, row.get("qty"), auto_create=True) | ||||
| 		qty = row.get("qty") | ||||
| 		while qty > 0: | ||||
| 			qty = split_qty_based_on_batch_size(work_order, row, qty) | ||||
| 			if row.job_card_qty > 0: | ||||
| 				create_job_card(work_order, row, auto_create=True) | ||||
| 
 | ||||
| def split_qty_based_on_batch_size(wo_doc, row, qty): | ||||
| 	if not cint(frappe.db.get_value("Operation", | ||||
| 		row.operation, "create_job_card_based_on_batch_size")): | ||||
| 		row.batch_size = row.get("qty") or wo_doc.qty | ||||
| 
 | ||||
| 	row.job_card_qty = row.batch_size | ||||
| 	if row.batch_size and qty >= row.batch_size: | ||||
| 		qty -= row.batch_size | ||||
| 	elif qty > 0: | ||||
| 		row.job_card_qty = qty | ||||
| 		qty = 0 | ||||
| 
 | ||||
| 	get_serial_nos_for_job_card(row, wo_doc) | ||||
| 
 | ||||
| 	return qty | ||||
| 
 | ||||
| def get_serial_nos_for_job_card(row, wo_doc): | ||||
| 	if not wo_doc.serial_no: | ||||
| 		return | ||||
| 
 | ||||
| 	serial_nos = get_serial_nos(wo_doc.serial_no) | ||||
| 	used_serial_nos = [] | ||||
| 	for d in frappe.get_all('Job Card', fields=['serial_no'], | ||||
| 		filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}): | ||||
| 		used_serial_nos.extend(get_serial_nos(d.serial_no)) | ||||
| 
 | ||||
| 	serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos))) | ||||
| 	row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty]) | ||||
| 
 | ||||
| def validate_operation_data(row): | ||||
| 	if row.get("qty") <= 0: | ||||
| @ -889,20 +1014,22 @@ def validate_operation_data(row): | ||||
| 			) | ||||
| 		) | ||||
| 
 | ||||
| def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): | ||||
| def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False): | ||||
| 	doc = frappe.new_doc("Job Card") | ||||
| 	doc.update({ | ||||
| 		'work_order': work_order.name, | ||||
| 		'operation': row.get("operation"), | ||||
| 		'workstation': row.get("workstation"), | ||||
| 		'posting_date': nowdate(), | ||||
| 		'for_quantity': qty or work_order.get('qty', 0), | ||||
| 		'for_quantity': row.job_card_qty or work_order.get('qty', 0), | ||||
| 		'operation_id': row.get("name"), | ||||
| 		'bom_no': work_order.bom_no, | ||||
| 		'project': work_order.project, | ||||
| 		'company': work_order.company, | ||||
| 		'sequence_id': row.get("sequence_id"), | ||||
| 		'wip_warehouse': work_order.wip_warehouse | ||||
| 		'wip_warehouse': work_order.wip_warehouse, | ||||
| 		'hour_rate': row.get("hour_rate"), | ||||
| 		'serial_no': row.get("serial_no") | ||||
| 	}) | ||||
| 
 | ||||
| 	if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: | ||||
|  | ||||
| @ -4,10 +4,17 @@ from frappe import _ | ||||
| def get_data(): | ||||
| 	return { | ||||
| 		'fieldname': 'work_order', | ||||
| 		'non_standard_fieldnames': { | ||||
| 			'Batch': 'reference_name' | ||||
| 		}, | ||||
| 		'transactions': [ | ||||
| 			{ | ||||
| 				'label': _('Transactions'), | ||||
| 				'items': ['Stock Entry', 'Job Card', 'Pick List'] | ||||
| 			}, | ||||
| 			{ | ||||
| 				'label': _('Reference'), | ||||
| 				'items': ['Serial No', 'Batch'] | ||||
| 			} | ||||
| 		] | ||||
| 	} | ||||
| @ -7,8 +7,9 @@ | ||||
|   "details", | ||||
|   "operation", | ||||
|   "bom", | ||||
|   "sequence_id", | ||||
|   "column_break_4", | ||||
|   "description", | ||||
|   "sequence_id", | ||||
|   "col_break1", | ||||
|   "completed_qty", | ||||
|   "status", | ||||
| @ -198,6 +199,10 @@ | ||||
|    "label": "Sequence ID", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_4", | ||||
|    "fieldtype": "Column Break" | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  | ||||
| @ -0,0 +1,105 @@ | ||||
| // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| /* eslint-disable */ | ||||
| 
 | ||||
| frappe.query_reports["Cost of Poor Quality Report"] = { | ||||
| 	"filters": [ | ||||
| 		{ | ||||
| 			label: __("Company"), | ||||
| 			fieldname: "company", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Company", | ||||
| 			default: frappe.defaults.get_user_default("Company"), | ||||
| 			reqd: 1 | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("From Date"), | ||||
| 			fieldname:"from_date", | ||||
| 			fieldtype: "Datetime", | ||||
| 			default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), | ||||
| 			reqd: 1 | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("To Date"), | ||||
| 			fieldname:"to_date", | ||||
| 			fieldtype: "Datetime", | ||||
| 			default: frappe.datetime.now_datetime(), | ||||
| 			reqd: 1, | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("Job Card"), | ||||
| 			fieldname: "name", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Job Card", | ||||
| 			get_query: function() { | ||||
| 				return { | ||||
| 					filters: { | ||||
| 						is_corrective_job_card: 1, | ||||
| 						docstatus: 1 | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("Work Order"), | ||||
| 			fieldname: "work_order", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Work Order" | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("Operation"), | ||||
| 			fieldname: "operation", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Operation", | ||||
| 			get_query: function() { | ||||
| 				return { | ||||
| 					filters: { | ||||
| 						is_corrective_operation: 1 | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("Workstation"), | ||||
| 			fieldname: "workstation", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Workstation" | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("Item"), | ||||
| 			fieldname: "production_item", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Item" | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("Serial No"), | ||||
| 			fieldname: "serial_no", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Serial No", | ||||
| 			depends_on: "eval: doc.production_item", | ||||
| 			get_query: function() { | ||||
| 				var item_code = frappe.query_report.get_filter_value('production_item'); | ||||
| 				return { | ||||
| 					filters: { | ||||
| 						item_code: item_code | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| 			label: __("Batch No"), | ||||
| 			fieldname: "batch_no", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Batch No", | ||||
| 			depends_on: "eval: doc.production_item", | ||||
| 			get_query: function() { | ||||
| 				var item_code = frappe.query_report.get_filter_value('production_item'); | ||||
| 				return { | ||||
| 					filters: { | ||||
| 						item: item_code | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 	] | ||||
| }; | ||||
| @ -0,0 +1,33 @@ | ||||
| { | ||||
|  "add_total_row": 0, | ||||
|  "columns": [], | ||||
|  "creation": "2021-01-11 11:10:58.292896", | ||||
|  "disable_prepared_report": 0, | ||||
|  "disabled": 0, | ||||
|  "docstatus": 0, | ||||
|  "doctype": "Report", | ||||
|  "filters": [], | ||||
|  "idx": 0, | ||||
|  "is_standard": "Yes", | ||||
|  "json": "{}", | ||||
|  "modified": "2021-01-11 11:11:03.594242", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Cost of Poor Quality Report", | ||||
|  "owner": "Administrator", | ||||
|  "prepared_report": 0, | ||||
|  "ref_doctype": "Job Card", | ||||
|  "report_name": "Cost of Poor Quality Report", | ||||
|  "report_type": "Script Report", | ||||
|  "roles": [ | ||||
|   { | ||||
|    "role": "System Manager" | ||||
|   }, | ||||
|   { | ||||
|    "role": "Manufacturing User" | ||||
|   }, | ||||
|   { | ||||
|    "role": "Manufacturing Manager" | ||||
|   } | ||||
|  ] | ||||
| } | ||||
| @ -0,0 +1,127 @@ | ||||
| # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.utils import flt | ||||
| 
 | ||||
| def execute(filters=None): | ||||
| 	columns, data = [], [] | ||||
| 
 | ||||
| 	columns = get_columns(filters) | ||||
| 	data = get_data(filters) | ||||
| 
 | ||||
| 	return columns, data | ||||
| 
 | ||||
| def get_data(report_filters): | ||||
| 	data = [] | ||||
| 	operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1}) | ||||
| 	if operations: | ||||
| 		operations = [d.name for d in operations] | ||||
| 		fields = ["production_item as item_code", "item_name", "work_order", "operation", | ||||
| 			"workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"] | ||||
| 
 | ||||
| 		filters = get_filters(report_filters, operations) | ||||
| 
 | ||||
| 		job_cards = frappe.get_all("Job Card", fields = fields, | ||||
| 			filters = filters) | ||||
| 
 | ||||
| 		for row in job_cards: | ||||
| 			row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) | ||||
| 			update_raw_material_cost(row, report_filters) | ||||
| 			data.append(row) | ||||
| 
 | ||||
| 	return data | ||||
| 
 | ||||
| def get_filters(report_filters, operations): | ||||
| 	filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1} | ||||
| 	for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]: | ||||
| 		if report_filters.get(field): | ||||
| 			if field != 'serial_no': | ||||
| 				filters[field] = report_filters.get(field) | ||||
| 			else: | ||||
| 				filters[field] = ('like', '% {} %'.format(report_filters.get(field))) | ||||
| 
 | ||||
| 	return filters | ||||
| 
 | ||||
| def update_raw_material_cost(row, filters): | ||||
| 	row.rm_cost = 0.0 | ||||
| 	for data in frappe.get_all("Job Card Item", fields = ["amount"], | ||||
| 		filters={"parent": row.name, "docstatus": 1}): | ||||
| 		row.rm_cost += data.amount | ||||
| 
 | ||||
| def get_columns(filters): | ||||
| 	return [ | ||||
| 		{ | ||||
| 			"label": _("Job Card"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"fieldname": "name", | ||||
| 			"options": "Job Card", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Work Order"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"fieldname": "work_order", | ||||
| 			"options": "Work Order", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Item Code"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"fieldname": "item_code", | ||||
| 			"options": "Item", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Item Name"), | ||||
| 			"fieldtype": "Data", | ||||
| 			"fieldname": "item_name", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Operation"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"fieldname": "operation", | ||||
| 			"options": "Operation", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Serial No"), | ||||
| 			"fieldtype": "Data", | ||||
| 			"fieldname": "serial_no", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Batch No"), | ||||
| 			"fieldtype": "Data", | ||||
| 			"fieldname": "batch_no", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Workstation"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"fieldname": "workstation", | ||||
| 			"options": "Workstation", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Operating Cost"), | ||||
| 			"fieldtype": "Currency", | ||||
| 			"fieldname": "operating_cost", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Raw Material Cost"), | ||||
| 			"fieldtype": "Currency", | ||||
| 			"fieldname": "rm_cost", | ||||
| 			"width": "100" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Total Time (in Mins)"), | ||||
| 			"fieldtype": "Float", | ||||
| 			"fieldname": "total_time_in_mins", | ||||
| 			"width": "100" | ||||
| 		} | ||||
| 	] | ||||
| @ -290,3 +290,5 @@ erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021 | ||||
| erpnext.patches.v13_0.set_training_event_attendance | ||||
| erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice | ||||
| erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold | ||||
| erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice | ||||
| erpnext.patches.v13_0.update_job_card_details | ||||
|  | ||||
							
								
								
									
										16
									
								
								erpnext/patches/v13_0/update_job_card_details.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								erpnext/patches/v13_0/update_job_card_details.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| # Copyright (c) 2019, Frappe and Contributors | ||||
| # License: GNU General Public License v3. See license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| 
 | ||||
| def execute(): | ||||
| 	frappe.reload_doc("manufacturing", "doctype", "job_card") | ||||
| 	frappe.reload_doc("manufacturing", "doctype", "job_card_item") | ||||
| 	frappe.reload_doc("manufacturing", "doctype", "work_order_operation") | ||||
| 
 | ||||
| 	frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo | ||||
| 		SET	jc.hour_rate =  wo.hour_rate | ||||
| 		WHERE | ||||
| 			jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0 | ||||
| 	""") | ||||
| @ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', { | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query('employee', 'employees', () => { | ||||
| 			if (!frm.doc.company) { | ||||
| 				frappe.msgprint(__("Please set a Company")); | ||||
| 				return []; | ||||
| 			let error_fields = []; | ||||
| 			let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date']; | ||||
| 
 | ||||
| 			let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]); | ||||
| 
 | ||||
| 			mandatory_fields.forEach(field => { | ||||
| 				if (!frm.doc[field]) { | ||||
| 					error_fields.push(frappe.unscrub(field)); | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			if (error_fields && error_fields.length) { | ||||
| 				message = message + '<br><br><ul><li>' + error_fields.join('</li><li>') + "</ul>"; | ||||
| 				frappe.throw({ | ||||
| 					message: message, | ||||
| 					indicator: 'red', | ||||
| 					title: __('Missing Fields') | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
| 			return { | ||||
| 				query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query", | ||||
| 				filters: frm.events.get_employee_filters(frm) | ||||
| @ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', { | ||||
| 
 | ||||
| 	get_employee_filters: function (frm) { | ||||
| 		let filters = {}; | ||||
| 		filters['company'] = frm.doc.company; | ||||
| 		filters['start_date'] = frm.doc.start_date; | ||||
| 		filters['end_date'] = frm.doc.end_date; | ||||
| 		filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet; | ||||
| 		filters['payroll_frequency'] = frm.doc.payroll_frequency; | ||||
| 		filters['payroll_payable_account'] = frm.doc.payroll_payable_account; | ||||
| 		filters['currency'] = frm.doc.currency; | ||||
| 
 | ||||
| 		if (frm.doc.department) { | ||||
| 			filters['department'] = frm.doc.department; | ||||
| 		} | ||||
| 		if (frm.doc.branch) { | ||||
| 			filters['branch'] = frm.doc.branch; | ||||
| 		} | ||||
| 		if (frm.doc.designation) { | ||||
| 			filters['designation'] = frm.doc.designation; | ||||
| 		} | ||||
| 		let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account', | ||||
| 			'currency', 'department', 'branch', 'designation']; | ||||
| 
 | ||||
| 		fields.forEach(field => { | ||||
| 			if (frm.doc[field]) { | ||||
| 				filters[field] = frm.doc[field]; | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		if (frm.doc.employees) { | ||||
| 			filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee); | ||||
| 			let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee); | ||||
| 			if (employees && employees.length) { | ||||
| 				filters['employees'] = employees; | ||||
| 			} | ||||
| 		} | ||||
| 		return filters; | ||||
| 	}, | ||||
|  | ||||
| @ -117,7 +117,6 @@ class PayrollEntry(Document): | ||||
| 			Creates salary slip for selected employees if already not created | ||||
| 		""" | ||||
| 		self.check_permission('write') | ||||
| 		self.created = 1 | ||||
| 		employees = [emp.employee for emp in self.employees] | ||||
| 		if employees: | ||||
| 			args = frappe._dict({ | ||||
| @ -680,9 +679,13 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): | ||||
| 	conditions = [] | ||||
| 	include_employees = [] | ||||
| 	emp_cond = '' | ||||
| 
 | ||||
| 	if not filters.payroll_frequency: | ||||
| 		frappe.throw(_('Select Payroll Frequency.')) | ||||
| 
 | ||||
| 	if filters.start_date and filters.end_date: | ||||
| 		employee_list = get_employee_list(filters) | ||||
| 		emp = filters.get('employees') | ||||
| 		emp = filters.get('employees') or [] | ||||
| 		include_employees = [employee.employee for employee in employee_list if employee.employee not in emp] | ||||
| 		filters.pop('start_date') | ||||
| 		filters.pop('end_date') | ||||
|  | ||||
| @ -991,7 +991,7 @@ frappe.help.help_links["Form/BOM"] = [ | ||||
| 		label: "Nested BOM Structure", | ||||
| 		url: | ||||
| 			docsUrl + | ||||
| 			"user/manual/en/manufacturing/articles/nested-bom-structure", | ||||
| 			"user/manual/en/manufacturing/articles/managing-multi-level-bom", | ||||
| 	}, | ||||
| ]; | ||||
| 
 | ||||
|  | ||||
| @ -35,6 +35,7 @@ frappe.ui.form.on('GST Settings', { | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					company: row.company, | ||||
| 					account_type: "Tax", | ||||
| 					is_group: 0 | ||||
| 				} | ||||
| 			}; | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| erpnext.setup_einvoice_actions = (doctype) => { | ||||
| 	frappe.ui.form.on(doctype, { | ||||
| 		async refresh(frm) { | ||||
| 			if (frm.doc.docstatus == 2) return; | ||||
| 
 | ||||
| 			const res = await frappe.call({ | ||||
| 				method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility', | ||||
| 				args: { doc: frm.doc } | ||||
| @ -111,7 +113,7 @@ erpnext.setup_einvoice_actions = (doctype) => { | ||||
| 
 | ||||
| 			if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { | ||||
| 				const action = () => { | ||||
| 					let message = __('Cancellation of e-way bill is currently not supported. '); | ||||
| 					let message = __('Cancellation of e-way bill is currently not supported.') + ' '; | ||||
| 					message += '<br><br>'; | ||||
| 					message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); | ||||
| 
 | ||||
|  | ||||
| @ -42,7 +42,10 @@ def validate_eligibility(doc): | ||||
| 	invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) | ||||
| 	invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] | ||||
| 	company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') | ||||
| 	no_taxes_applied = not doc.get('taxes') | ||||
| 
 | ||||
| 	# if export invoice, then taxes can be empty | ||||
| 	# invoice can only be ineligible if no taxes applied and is not an export invoice | ||||
| 	no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' | ||||
| 	has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) | ||||
| 
 | ||||
| 	if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item: | ||||
| @ -188,9 +191,10 @@ def get_item_list(invoice): | ||||
| 
 | ||||
| 		item.qty = abs(item.qty) | ||||
| 
 | ||||
| 		item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) | ||||
| 		item.gross_amount = abs(item.taxable_value) + item.discount_amount | ||||
| 		item.unit_rate = abs(item.taxable_value / item.qty) | ||||
| 		item.gross_amount = abs(item.taxable_value) | ||||
| 		item.taxable_value = abs(item.taxable_value) | ||||
| 		item.discount_amount = 0 | ||||
| 
 | ||||
| 		item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None | ||||
| 		item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None | ||||
|  | ||||
| @ -834,8 +834,16 @@ def get_depreciation_amount(asset, depreciable_value, row): | ||||
| 	depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) | ||||
| 
 | ||||
| 	if row.depreciation_method in ("Straight Line", "Manual"): | ||||
| 		depreciation_amount = (flt(row.value_after_depreciation) - | ||||
| 			flt(row.expected_value_after_useful_life)) / depreciation_left | ||||
| 		# if the Depreciation Schedule is being prepared for the first time | ||||
| 		if not asset.flags.increase_in_asset_life: | ||||
| 			depreciation_amount = (flt(row.value_after_depreciation) - | ||||
| 				flt(row.expected_value_after_useful_life)) / depreciation_left | ||||
| 
 | ||||
| 		# if the Depreciation Schedule is being modified after Asset Repair | ||||
| 		else: | ||||
| 			depreciation_amount = (flt(row.value_after_depreciation) - | ||||
| 				flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) | ||||
| 		 | ||||
| 	else: | ||||
| 		rate_of_depreciation = row.rate_of_depreciation | ||||
| 		# if its the first depreciation | ||||
|  | ||||
| @ -4,6 +4,8 @@ | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| 
 | ||||
| from frappe.utils import get_link_to_form | ||||
| 
 | ||||
| from frappe import _ | ||||
| 
 | ||||
| from frappe.model.document import Document | ||||
| @ -18,6 +20,27 @@ class ProductBundle(Document): | ||||
| 		from erpnext.utilities.transaction_base import validate_uom_is_integer | ||||
| 		validate_uom_is_integer(self, "uom", "qty") | ||||
| 
 | ||||
| 	def on_trash(self): | ||||
| 		linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice", | ||||
| 			"Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"] | ||||
| 
 | ||||
| 		invoice_links = [] | ||||
| 		for doctype in linked_doctypes: | ||||
| 			item_doctype = doctype + " Item" | ||||
| 
 | ||||
| 			if doctype == "Stock Entry": | ||||
| 				item_doctype = doctype + " Detail" | ||||
| 
 | ||||
| 			invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"]) | ||||
| 
 | ||||
| 			for invoice in invoices: | ||||
| 				invoice_links.append(get_link_to_form(doctype, invoice['parent'])) | ||||
| 
 | ||||
| 		if len(invoice_links): | ||||
| 			frappe.throw( | ||||
| 				"This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" | ||||
| 				.format(", ".join(invoice_links)), title=_("Not Allowed")) | ||||
| 
 | ||||
| 	def validate_main_item(self): | ||||
| 		"""Validates, main Item is not a stock item""" | ||||
| 		if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"): | ||||
|  | ||||
| @ -472,12 +472,7 @@ erpnext.PointOfSale.ItemCart = class { | ||||
| 		const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; | ||||
| 		this.render_grand_total(grand_total); | ||||
| 
 | ||||
| 		const taxes = frm.doc.taxes.map(t => { | ||||
| 			return { | ||||
| 				description: t.description, rate: t.rate | ||||
| 			}; | ||||
| 		}); | ||||
| 		this.render_taxes(frm.doc.total_taxes_and_charges, taxes); | ||||
| 		this.render_taxes(frm.doc.taxes); | ||||
| 	} | ||||
| 
 | ||||
| 	render_net_total(value) { | ||||
| @ -502,14 +497,14 @@ erpnext.PointOfSale.ItemCart = class { | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	render_taxes(value, taxes) { | ||||
| 	render_taxes(taxes) { | ||||
| 		if (taxes.length) { | ||||
| 			const currency = this.events.get_frm().doc.currency; | ||||
| 			const taxes_html = taxes.map(t => { | ||||
| 				const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; | ||||
| 				return `<div class="tax-row">
 | ||||
| 					<div class="tax-label">${description}</div> | ||||
| 					<div class="tax-value">${format_currency(value, currency)}</div> | ||||
| 					<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div> | ||||
| 				</div>`; | ||||
| 			}).join(''); | ||||
| 			this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html); | ||||
|  | ||||
| @ -56,7 +56,7 @@ erpnext.PointOfSale.Payment = class { | ||||
| 				); | ||||
| 				let df_events = { | ||||
| 					onchange: function() { | ||||
| 						frm.set_value(this.df.fieldname, this.value); | ||||
| 						frm.set_value(this.df.fieldname, this.get_value()); | ||||
| 					} | ||||
| 				}; | ||||
| 				if (df.fieldtype == "Button") { | ||||
|  | ||||
| @ -74,7 +74,7 @@ | ||||
|   "stock_received_but_not_billed", | ||||
|   "service_received_but_not_billed", | ||||
|   "expenses_included_in_valuation", | ||||
|   "fixed_asset_depreciation_settings", | ||||
|   "fixed_asset_defaults", | ||||
|   "accumulated_depreciation_account", | ||||
|   "depreciation_expense_account", | ||||
|   "series_for_depreciation_entry", | ||||
| @ -83,6 +83,7 @@ | ||||
|   "disposal_account", | ||||
|   "depreciation_cost_center", | ||||
|   "capital_work_in_progress_account", | ||||
|   "repair_and_maintenance_account", | ||||
|   "asset_received_but_not_billed", | ||||
|   "budget_detail", | ||||
|   "exception_budget_approver_role", | ||||
| @ -519,12 +520,6 @@ | ||||
|    "no_copy": 1, | ||||
|    "options": "Account" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "fixed_asset_depreciation_settings", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Fixed Asset Depreciation Settings" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "accumulated_depreciation_account", | ||||
|    "fieldtype": "Link", | ||||
| @ -734,6 +729,18 @@ | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Default Payment Discount Account", | ||||
|    "options": "Account" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "fixed_asset_defaults", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Fixed Asset Defaults" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "repair_and_maintenance_account", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Repair and Maintenance Account", | ||||
|    "options": "Account" | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-building", | ||||
| @ -741,7 +748,7 @@ | ||||
|  "image_field": "company_logo", | ||||
|  "is_tree": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-05-07 03:11:28.189740", | ||||
|  "modified": "2021-05-12 16:51:08.187233", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Setup", | ||||
|  "name": "Company", | ||||
|  | ||||
| @ -28,21 +28,21 @@ def before_tests(): | ||||
| 	from frappe.desk.page.setup_wizard.setup_wizard import setup_complete | ||||
| 	if not frappe.get_list("Company"): | ||||
| 		setup_complete({ | ||||
| 			"currency"			:"USD", | ||||
| 			"full_name"			:"Test User", | ||||
| 			"company_name"		:"Wind Power LLC", | ||||
| 			"timezone"			:"America/New_York", | ||||
| 			"company_abbr"		:"WP", | ||||
| 			"industry"			:"Manufacturing", | ||||
| 			"country"			:"United States", | ||||
| 			"fy_start_date"		:"2011-01-01", | ||||
| 			"fy_end_date"		:"2011-12-31", | ||||
| 			"language"			:"english", | ||||
| 			"company_tagline"	:"Testing", | ||||
| 			"email"				:"test@erpnext.com", | ||||
| 			"password"			:"test", | ||||
| 			"currency"          :"USD", | ||||
| 			"full_name"         :"Test User", | ||||
| 			"company_name"      :"Wind Power LLC", | ||||
| 			"timezone"          :"America/New_York", | ||||
| 			"company_abbr"      :"WP", | ||||
| 			"industry"          :"Manufacturing", | ||||
| 			"country"           :"United States", | ||||
| 			"fy_start_date"     :"2021-01-01", | ||||
| 			"fy_end_date"       :"2021-12-31", | ||||
| 			"language"          :"english", | ||||
| 			"company_tagline"   :"Testing", | ||||
| 			"email"             :"test@erpnext.com", | ||||
| 			"password"          :"test", | ||||
| 			"chart_of_accounts" : "Standard", | ||||
| 			"domains"			: ["Manufacturing"], | ||||
| 			"domains"           : ["Manufacturing"], | ||||
| 		}) | ||||
| 
 | ||||
| 	frappe.db.sql("delete from `tabLeave Allocation`") | ||||
|  | ||||
| @ -11,10 +11,11 @@ | ||||
|  "hide_custom": 0, | ||||
|  "icon": "settings", | ||||
|  "idx": 0, | ||||
|  "is_default": 0, | ||||
|  "is_standard": 1, | ||||
|  "label": "ERPNext Settings", | ||||
|  "links": [], | ||||
|  "modified": "2020-12-01 13:38:37.759596", | ||||
|  "modified": "2021-06-12 01:58:11.399566", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Setup", | ||||
|  "name": "ERPNext Settings", | ||||
| @ -109,6 +110,13 @@ | ||||
|    "label": "Domain Settings", | ||||
|    "link_to": "Domain Settings", | ||||
|    "type": "DocType" | ||||
|   }, | ||||
|   { | ||||
|    "doc_view": "", | ||||
|    "icon": "retail", | ||||
|    "label": "Products Settings", | ||||
|    "link_to": "Products Settings", | ||||
|    "type": "DocType" | ||||
|   } | ||||
|  ] | ||||
| } | ||||
| } | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user